Compare commits
No commits in common. "master" and "v0.8.5" have entirely different histories.
313 changed files with 26064 additions and 47597 deletions
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[env]
|
||||
PCRE2_SYS_STATIC = "1"
|
|
@ -1,37 +0,0 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
#
|
||||
# Makefile for: "Build and run Tests" workflow, in .gitea/workflows/build.yaml
|
||||
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_INCREMENTAL ?= 0
|
||||
CARGO_NET_RETRY ?= 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL ?= sparse
|
||||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0
|
||||
RUSTUP_MAX_RETRIES ?= 10
|
||||
RUST_BACKTRACE ?= short
|
||||
|
||||
.PHONY: all
|
||||
all: cargo-check cargo-test-compiles cargo-test rustdoc-build rustdoc-test
|
||||
@printf "All completed.\n"
|
||||
|
||||
.PHONY: cargo-check
|
||||
cargo-check:
|
||||
@printf "cargo-check\n"
|
||||
cargo check --all-features --all --tests --examples --benches --bins
|
||||
.PHONY: cargo-test-compiles
|
||||
cargo-test-compiles:
|
||||
@printf "cargo-test-compiles\n"
|
||||
cargo test --all --no-fail-fast --all-features --no-run --locked
|
||||
.PHONY: cargo-test
|
||||
cargo-test:
|
||||
@printf "cargo-test\n"
|
||||
cargo nextest run --all --no-fail-fast --all-features --future-incompat-report -E 'not (test(smtp::test::test_smtp))'
|
||||
.PHONY: rustdoc-build
|
||||
rustdoc-build:
|
||||
@printf "rustdoc-build\n"
|
||||
env DISPLAY= WAYLAND_DISPLAY= make build-rustdoc
|
||||
.PHONY: rustdoc-test
|
||||
rustdoc-test:
|
||||
@printf "rustdoc-test\n"
|
||||
make test-docs
|
|
@ -1,61 +0,0 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
#
|
||||
# Makefile for: "Run cargo lints" workflow, in .gitea/workflows/lints.yaml
|
||||
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_INCREMENTAL ?= 0
|
||||
CARGO_NET_RETRY ?= 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL ?= sparse
|
||||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0
|
||||
RUSTUP_MAX_RETRIES ?= 10
|
||||
RUST_BACKTRACE ?= short
|
||||
NIGHTLY_EXISTS=`((cargo +nightly 2> /dev/null 1> /dev/null) && echo 0)|| echo 1)`
|
||||
GIT=env GIT_CONFIG_GLOBAL="" GIT_CONFIG_SYSTEM="" GIT_CONFIG_NOSYSTEM=1 git
|
||||
|
||||
.PHONY: all
|
||||
all: cargo-msrv rustfmt clippy cargo-derivefmt-melib cargo-derivefmt-meli cargo-derivefmt-tools
|
||||
@printf "All checks completed.\n"
|
||||
|
||||
# Check both melib and meli in the same Make target, because if melib does not
|
||||
# satisfy MSRV then meli won't either, since it depends on melib.
|
||||
.PHONY: cargo-msrv
|
||||
cargo-msrv:
|
||||
@printf "cargo-msrv\n"
|
||||
cargo msrv --output-format json --log-level trace --log-target stdout --path meli verify -- cargo check --all-targets
|
||||
cargo msrv --output-format json --log-level trace --log-target stdout --path melib verify -- cargo check --all-targets
|
||||
|
||||
.PHONY: rustfmt
|
||||
rustfmt:
|
||||
@printf "rustfmt\n"
|
||||
@((if [ "${NIGHTLY_EXISTS}" -eq 0 ]; then printf "running rustfmt with nightly toolchain\n"; else printf "running rustfmt with active toolchain\n"; fi))
|
||||
@((if [ "${NIGHTLY_EXISTS}" -eq 0 ]; then cargo +nightly fmt --check --all; else cargo fmt --check --all; fi))
|
||||
|
||||
.PHONY: clippy
|
||||
clippy:
|
||||
@printf "clippy\n"
|
||||
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: cargo-derivefmt-melib
|
||||
cargo-derivefmt-melib:
|
||||
@printf "cargo-derivefmt-melib\n"
|
||||
@printf "Checking that derives are sorted alphabetically...\n"
|
||||
cargo derivefmt --manifest-path ./melib/Cargo.toml
|
||||
@$(GIT) checkout --quiet meli/src/conf/overrides.rs
|
||||
@($(GIT) diff --quiet ./melib && $(GIT) diff --cached --quiet ./melib && printf "All ./melib derives are sorted alphabetically.\n") || (printf "Some derives in the ./melib crate are not sorted alphabetically, see diff:\n"; $(GIT) diff HEAD; exit 1)
|
||||
|
||||
.PHONY: cargo-derivefmt-meli
|
||||
cargo-derivefmt-meli:
|
||||
@printf "cargo-derivefmt-meli\n"
|
||||
@printf "Checking that derives are sorted alphabetically...\n"
|
||||
cargo derivefmt --manifest-path ./meli/Cargo.toml
|
||||
@$(GIT) checkout --quiet meli/src/conf/overrides.rs
|
||||
@($(GIT) diff --quiet ./meli && $(GIT) diff --cached --quiet ./meli && printf "All ./meli derives are sorted alphabetically.\n") || (printf "Some derives in the ./meli crate are not sorted alphabetically, see diff:\n"; $(GIT) diff HEAD; exit 1)
|
||||
|
||||
.PHONY: cargo-derivefmt-tools
|
||||
cargo-derivefmt-tools:
|
||||
@printf "cargo-derivefmt-tools\n"
|
||||
@printf "Checking that derives are sorted alphabetically...\n"
|
||||
cargo derivefmt --manifest-path ./tools/Cargo.toml
|
||||
@$(GIT) checkout --quiet meli/src/conf/overrides.rs
|
||||
@($(GIT) diff --quiet ./tools && $(GIT) diff --cached --quiet ./tools && printf "All ./tools derives are sorted alphabetically.\n") || (printf "Some derives in the ./tools crate are not sorted alphabetically, see diff:\n"; $(GIT) diff HEAD; exit 1)
|
|
@ -1,28 +0,0 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
#
|
||||
# Makefile for: "Build and run Tests" workflow, in .gitea/workflows/build.yaml
|
||||
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_INCREMENTAL ?= 0
|
||||
CARGO_NET_RETRY ?= 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL ?= sparse
|
||||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0
|
||||
RUSTUP_MAX_RETRIES ?= 10
|
||||
RUST_BACKTRACE ?= short
|
||||
|
||||
.PHONY: all
|
||||
all: cargo-sort check-debian-changelog
|
||||
@printf "All checks completed.\n"
|
||||
|
||||
.PHONY: cargo-sort
|
||||
cargo-sort:
|
||||
@printf "cargo-sort\n"
|
||||
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace fuzz
|
||||
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace tools
|
||||
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace --workspace
|
||||
|
||||
.PHONY: check-debian-changelog
|
||||
check-debian-changelog:
|
||||
@printf "Check debian/changelog is up-to-date.\n"
|
||||
./scripts/check_debian_changelog.sh
|
|
@ -1,41 +1,25 @@
|
|||
---
|
||||
|
||||
name: "Pull Request"
|
||||
about: "Basic pull request template"
|
||||
about: "Standard pull request template."
|
||||
title: "WIP: "
|
||||
ref: "master"
|
||||
|
||||
---
|
||||
|
||||
<!-- If your PR is not ready to merge/review, you can add a `WIP: ` prefix to the title. -->
|
||||
<!--
|
||||
This template is just a suggestion, and is commented out using HTML comment syntax.
|
||||
It will not show up in your PR text unless you remove the comment markers.
|
||||
-->
|
||||
<!-- If your PR is ready to merge/review, remove the `WIP: ` prefix from the title. -->
|
||||
|
||||
<!--
|
||||
### Summary of the PR
|
||||
|
||||
## Summary of the PR
|
||||
<!-- Changes introduced in this PR. -->
|
||||
|
||||
Changes introduced in this PR.
|
||||
### Requirements
|
||||
|
||||
Before submitting your PR, please make sure you have addressed the following requirements:
|
||||
|
||||
## Requirements
|
||||
|
||||
Before submitting your PR, please make sure you have addressed the following
|
||||
requirements:
|
||||
|
||||
* [ ] All commits in this PR are signed (with `git commit -s`), and the commit
|
||||
has a message describing the motivation behind the change, if
|
||||
appropriate.
|
||||
* [ ] All added/changed public-facing functionality, especially configuration
|
||||
options, are documented in the manual pages.
|
||||
* [ ] All commits in this PR are signed (with `git commit -s`), and the commit has a message describing the motivation behind the change, if appropriate.
|
||||
* [ ] All added/changed public-facing functionality, especially configuration options, are documented in the manual pages.
|
||||
* [ ] Any newly added `unsafe` code is properly documented.
|
||||
* [ ] Each commit has been formatted with `rustfmt`. Run `make fmt` in the
|
||||
project root.
|
||||
* [ ] Each commit has been linted with `clippy`. Run `make lint` in the project
|
||||
root.
|
||||
* [ ] Each commit does not break any test. Run `make test` in the project root.
|
||||
If you have `cargo-nextest` installed, you can run `cargo nextest run
|
||||
--all --no-fail-fast --all-features --future-incompat-report` instead.
|
||||
|
||||
-->
|
||||
* [ ] Each commit has been formatted with `rustfmt`. Run `make fmt` in the project root.
|
||||
* [ ] Each commit has been linted with `clippy`. Run `make lint` in the project root.
|
||||
* [ ] Each commit does not break any test. Run `make test` in the project root. If you have `cargo-nextest` installed, you can run `cargo nextest run --all --no-fail-fast --all-features --future-incompat-report` instead.
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
# SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
# Lint with shellcheck -s sh -S style check_dco.sh
|
||||
|
||||
# Notes:
|
||||
# ======
|
||||
#
|
||||
# - We need to make sure git commands do not read from any existing configs to
|
||||
# prevent surprises like default trailers being added.
|
||||
# - we need to pass `--always` to `git-format-patch` to check even empty
|
||||
# commits despite them not being something we would merge. This tripped me up
|
||||
# when debugging this workflow because I tested it with empty commits. My
|
||||
# fault.
|
||||
|
||||
export GIT_CONFIG_GLOBAL=""
|
||||
export GIT_CONFIG_SYSTEM=""
|
||||
export GIT_CONFIG_NOSYSTEM=1
|
||||
|
||||
ensure_env_var() {
|
||||
set | grep -q "^${1}=" || (printf "Environment variable %s missing from process environment, exiting.\n" "${1}"; exit "${2}")
|
||||
}
|
||||
|
||||
ensure_env_var "GITHUB_BASE_REF" 1 || exit $?
|
||||
ensure_env_var "GITHUB_HEAD_REF" 2 || exit $?
|
||||
|
||||
# contains_correct_signoff() {
|
||||
# author=$(git log --author="$1" --pretty="%an <%ae>" -1)
|
||||
# git format-patch --always --stdout "${1}^..${1}" | git interpret-trailers --parse | grep -q "^Signed-off-by: ${author}"
|
||||
# }
|
||||
contains_signoff() {
|
||||
GIT_CONFIG_GLOBAL="" git format-patch --always -1 --stdout "${1}" | git interpret-trailers --parse | grep -q "^Signed-off-by: "
|
||||
}
|
||||
|
||||
get_commit_sha() {
|
||||
if OUT=$(git rev-parse "${1}"); then
|
||||
printf "%s" "${OUT}"
|
||||
return
|
||||
fi
|
||||
printf "Could not git-rev-parse %s, falling back to HEAD...\n" "${1}" 1>&2
|
||||
git rev-parse HEAD
|
||||
}
|
||||
|
||||
echo "Debug workflow info:"
|
||||
echo "Base ref GITHUB_BASE_REF=${GITHUB_BASE_REF}"
|
||||
echo "Head ref GITHUB_HEAD_REF=${GITHUB_HEAD_REF}"
|
||||
BASE_REF=$(get_commit_sha "${GITHUB_BASE_REF}")
|
||||
HEAD_REF=$(get_commit_sha "${GITHUB_HEAD_REF}")
|
||||
echo "Processed base ref BASE_REF=${BASE_REF}"
|
||||
echo "Processed head ref HEAD_REF=${HEAD_REF}"
|
||||
|
||||
RANGE="${BASE_REF}..${HEAD_REF}"
|
||||
echo "Range to examine is RANGE=${RANGE}"
|
||||
|
||||
if ! SHA_LIST=$(git rev-list "${RANGE}"); then
|
||||
printf "Could not get commit range %s with git rev-list, bailing out...\n" "${RANGE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "SHA list to examine is SHA_LIST="
|
||||
echo "---------------------------------------------------------------------"
|
||||
echo "${SHA_LIST}"
|
||||
echo "---------------------------------------------------------------------"
|
||||
echo ""
|
||||
echo "Starting checks..."
|
||||
|
||||
output=$(printf "%s" "${SHA_LIST}" | while read -r commit_sha; do
|
||||
contains_signoff_result=""
|
||||
|
||||
contains_signoff "${commit_sha}"; contains_signoff_result="$?"
|
||||
if [ "${contains_signoff_result}" -ne 0 ]; then
|
||||
printf "Commit does not contain Signed-off-by git trailer: %s\n\n" "${commit_sha}"
|
||||
echo "patch was:"
|
||||
echo "---------------------------------------------------------------------"
|
||||
GIT_CONFIG_GLOBAL="" git format-patch --always -1 --stdout "${commit_sha}"
|
||||
echo "---------------------------------------------------------------------"
|
||||
echo "trailers were:"
|
||||
echo "---------------------------------------------------------------------"
|
||||
GIT_CONFIG_GLOBAL="" git format-patch --always -1 --stdout "${commit_sha}" | git interpret-trailers --parse
|
||||
echo "---------------------------------------------------------------------"
|
||||
echo "commit was:"
|
||||
echo "---------------------------------------------------------------------"
|
||||
git log --no-decorate --pretty=oneline --abbrev-commit -n 1 "${commit_sha}"
|
||||
echo "---------------------------------------------------------------------"
|
||||
fi
|
||||
done)
|
||||
|
||||
if [ "${output}" = "" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "One or more of your commits in this Pull Request lack the Developer Certificate of Origin "
|
||||
echo "which is more commonly known as DCO or the \"Signed-off-by: \" trailer line in the "
|
||||
echo "git commit message."
|
||||
echo "For information, documentation, help, check: https://wiki.linuxfoundation.org/dco"
|
||||
|
||||
echo "The reported errors were:"
|
||||
printf "%s\n" "${output}" 1>&2
|
||||
|
||||
echo ""
|
||||
echo "Solution:"
|
||||
echo ""
|
||||
echo "- end all your commits with a 'Signed-off-by: User <user@localhost>' line, "
|
||||
echo " with your own display name and email address."
|
||||
echo "- Make sure the signoff is separated by the commit message body with an empty line."
|
||||
echo "- Make sure the signoff is the last line in your commit message."
|
||||
echo "- Lastly, make sure the signoff matches your git commit author name and email identity."
|
||||
|
||||
exit 1
|
|
@ -1,123 +0,0 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Build and run Tests
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.gitea/**'
|
||||
- 'melib/src/**'
|
||||
- 'melib/Cargo.toml'
|
||||
- 'meli/src/**'
|
||||
- 'meli/Cargo.toml'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, linux-arm64]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
arch: amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- build: linux-arm64
|
||||
arch: arm64
|
||||
os: ubuntu-latest-arm64
|
||||
rust: stable
|
||||
target: aarch64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev make
|
||||
- name: Cache rustup
|
||||
id: cache-rustup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/
|
||||
~/.cargo/env
|
||||
~/.cargo/config.toml
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: build-workflow-${{ matrix.build }}-rustup
|
||||
- id: rustup-setup
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
name: Install rustup and toolchains
|
||||
shell: bash
|
||||
run: |
|
||||
if ! command -v rustup &>/dev/null; then
|
||||
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
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
|
||||
fi
|
||||
- name: Source .cargo/env
|
||||
shell: bash
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
- name: Setup Rust target
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- name: Add test dependencies
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cargo install --quiet --version 0.9.54 --target "${{ matrix.target }}" cargo-nextest
|
||||
- name: Restore build artifacts cache in target dir
|
||||
id: cache-deps
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/
|
||||
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: cargo-check
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build cargo-check
|
||||
- if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
name: Save build artifacts in target dir
|
||||
id: save-cache-deps
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: target/
|
||||
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: cargo-test-compiles
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build cargo-test-compiles
|
||||
- name: cargo-test
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build cargo-test
|
||||
- name: rustdoc build
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build rustdoc-build
|
||||
- name: rustdoc tests
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build rustdoc-test
|
|
@ -16,13 +16,13 @@ on:
|
|||
- v*
|
||||
|
||||
jobs:
|
||||
build-debian:
|
||||
name: Create debian package
|
||||
build:
|
||||
name: Package for debian on ${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, linux-arm64]
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
arch: amd64
|
||||
|
@ -30,12 +30,6 @@ jobs:
|
|||
rust: stable
|
||||
artifact_name: 'linux-amd64'
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- build: linux-arm64
|
||||
arch: arm64
|
||||
os: ubuntu-latest-arm64
|
||||
rust: stable
|
||||
artifact_name: 'linux-arm64'
|
||||
target: aarch64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Build release binaries
|
||||
name: Build release binary
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
@ -17,25 +17,18 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
name: Build release binary
|
||||
name: Build on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, linux-arm64]
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
arch: amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
artifact_name: 'meli-linux-amd64'
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- build: linux-arm64
|
||||
arch: arm64
|
||||
os: ubuntu-latest-arm64
|
||||
rust: stable
|
||||
artifact_name: 'meli-linux-arm64'
|
||||
target: aarch64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
|
@ -43,6 +36,13 @@ jobs:
|
|||
run: |
|
||||
apt-get update
|
||||
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
|
||||
#- id: cache-rustup
|
||||
# name: Cache Rust toolchain
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.rustup
|
||||
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
|
||||
- id: rustup-setup
|
||||
name: Install rustup and toolchains
|
||||
shell: bash
|
||||
|
@ -51,9 +51,22 @@ 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
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
|
||||
fi
|
||||
- name: Configure cargo data directory
|
||||
# After this point, all cargo registry and crate data is stored in
|
||||
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
|
||||
# that are needed during the build process. Additionally, this works
|
||||
# around a bug in the 'cache' action that causes directories outside of
|
||||
# the workspace dir to be saved/restored incorrectly.
|
||||
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
|
||||
#- id: cache-cargo
|
||||
# name: Cache cargo configuration and installations
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.CARGO_HOME }}
|
||||
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
- name: Setup Rust target
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
|
@ -63,17 +76,15 @@ jobs:
|
|||
EOF
|
||||
- name: Build binary
|
||||
run: |
|
||||
VERSION=$(grep -m1 version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
make
|
||||
mkdir artifacts
|
||||
mv target/*/release/* target/ || true
|
||||
mv target/release/* target/ || true
|
||||
mv target/meli artifacts/meli-${VERSION}-${{ matrix.target }}
|
||||
mv target/meli artifacts/
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}-${{ env.VERSION }}
|
||||
path: artifacts/meli-${{ env.VERSION }}-${{ matrix.target }}
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: artifacts/meli
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
|
@ -1,26 +0,0 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Verify DCO
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Verify DCO signoff on commit messages
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: check-dco
|
||||
shell: sh
|
||||
name: Check that commit messages end with a Signed-off-by git trailer
|
||||
run: |
|
||||
env GITHUB_BASE_REF="origin/${{env.GITHUB_BASE_REF}}" GITHUB_HEAD_REF="origin/${{env.GITHUB_HEAD_REF}}" sh ./.gitea/check_dco.sh
|
|
@ -22,8 +22,8 @@ on:
|
|||
- 'Cargo.lock'
|
||||
|
||||
jobs:
|
||||
lints:
|
||||
name: Run lints
|
||||
test:
|
||||
name: Lint on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@ -41,25 +41,14 @@ jobs:
|
|||
run: |
|
||||
apt-get update
|
||||
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
|
||||
- name: Find meli MSRV from meli/Cargo.toml.
|
||||
run: |
|
||||
echo MELI_MSRV=$(grep -m1 rust-version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1) >> $GITHUB_ENV
|
||||
printf "Rust MSRV is %s\n" $(grep -m1 rust-version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
|
||||
- name: Cache rustup
|
||||
id: cache-rustup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/
|
||||
~/.cargo/env
|
||||
~/.cargo/config.toml
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: lints-workflow-${{ matrix.build }}-rustup
|
||||
#- id: cache-rustup
|
||||
# name: Cache Rust toolchain
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.rustup
|
||||
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
|
||||
- id: rustup-setup
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
name: Install Rustup and toolchains
|
||||
shell: bash
|
||||
run: |
|
||||
|
@ -67,74 +56,56 @@ 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
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
rustup toolchain install --profile minimal --component "rustfmt" --target "${{ matrix.target }}" -- "${{ env.MELI_MSRV }}"
|
||||
rustup component add rustfmt --toolchain ${{ env.MELI_MSRV }}-${{ matrix.target }}
|
||||
rustup toolchain install --profile minimal --component clippy,rustfmt --target "${{ matrix.target }}" -- "${{ matrix.rust }}"
|
||||
rustup component add rustfmt --toolchain ${{ matrix.rust }}-${{ matrix.target }}
|
||||
rustup toolchain install --profile minimal --component clippy,rustfmt --target ${{ matrix.target }} -- "${{ matrix.rust }}"
|
||||
rustup default ${{ matrix.rust }}
|
||||
fi
|
||||
- name: Source .cargo/env
|
||||
shell: bash
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
- name: Configure cargo data directory
|
||||
# After this point, all cargo registry and crate data is stored in
|
||||
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
|
||||
# that are needed during the build process. Additionally, this works
|
||||
# around a bug in the 'cache' action that causes directories outside of
|
||||
# the workspace dir to be saved/restored incorrectly.
|
||||
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
|
||||
#- id: cache-cargo
|
||||
# name: Cache cargo configuration and installations
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.CARGO_HOME }}
|
||||
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
- name: Setup Rust target
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- name: Add lint dependencies
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
name: Add lint dependencies
|
||||
run: |
|
||||
cargo install --version 0.15.1 --target "${{ matrix.target }}" cargo-msrv
|
||||
# "This package is currently implemented using rust-analyzer internals, so cannot be published on crates.io."
|
||||
RUSTFLAGS="" cargo install --locked --target "${{ matrix.target }}" --git https://github.com/dcchut/cargo-derivefmt --rev 95da8eee343de4adb25850893873b979258aed7f --bin cargo-derivefmt
|
||||
- name: Restore build artifacts cache in target dir
|
||||
id: cache-deps
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/
|
||||
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: clippy
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint clippy
|
||||
- if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
name: Save build artifacts in target dir
|
||||
id: save-cache-deps
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: target/
|
||||
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: cargo-msrv verify melib MSRV
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f ./.gitea/Makefile.lint cargo-msrv
|
||||
cargo install --quiet --version 1.0.9 --target "${{ matrix.target }}" cargo-sort
|
||||
RUSTFLAGS="" cargo install --locked --target "${{ matrix.target }}" --git https://github.com/dcchut/cargo-derivefmt --rev 2ff93de7fb418180458dd1ba27e5655607c23ab6 --bin cargo-derivefmt
|
||||
- name: rustfmt
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint rustfmt
|
||||
cargo fmt --check --all
|
||||
- name: clippy
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
- name: cargo-derivefmt melib
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint cargo-derivefmt-melib
|
||||
cargo derivefmt --manifest-path ./melib/Cargo.toml
|
||||
- name: cargo-derivefmt meli
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint cargo-derivefmt-meli
|
||||
cargo derivefmt --manifest-path ./meli/Cargo.toml
|
||||
- name: cargo-derivefmt fuzz
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo derivefmt --manifest-path ./fuzz/Cargo.toml
|
||||
- name: cargo-derivefmt tools
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint cargo-derivefmt-tools
|
||||
cargo derivefmt --manifest-path ./tools/Cargo.toml
|
||||
|
|
|
@ -24,7 +24,7 @@ on:
|
|||
|
||||
jobs:
|
||||
manifest_lint:
|
||||
name: Run Cargo manifest etc lints
|
||||
name: Lint Cargo manifests on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@ -42,19 +42,8 @@ jobs:
|
|||
run: |
|
||||
apt-get update
|
||||
apt-get install -y mandoc
|
||||
- name: Cache rustup
|
||||
id: cache-rustup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/
|
||||
~/.cargo/env
|
||||
~/.cargo/config.toml
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: manifest_lints-workflow-${{ matrix.build }}-rustup
|
||||
- name: Find meli MSRV from meli/Cargo.toml.
|
||||
run: echo MELI_MSRV=$(grep -m1 rust-version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1) >> $GITHUB_ENV
|
||||
- id: rustup-setup
|
||||
name: Install Rustup and toolchains
|
||||
shell: bash
|
||||
|
@ -64,35 +53,39 @@ jobs:
|
|||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
rustup toolchain install --profile minimal --component "rustfmt" --target "${{ matrix.target }}" -- "${{ env.MELI_MSRV }}"
|
||||
rustup component add rustfmt --toolchain ${{ env.MELI_MSRV }}-${{ matrix.target }}
|
||||
rustup toolchain install --profile minimal --component "rustfmt" --target "${{ matrix.target }}" -- "${{ matrix.rust }}"
|
||||
rustup component add rustfmt --toolchain ${{ matrix.rust }}-${{ matrix.target }}
|
||||
rustup default ${{ matrix.rust }}
|
||||
fi
|
||||
- name: Source .cargo/env
|
||||
shell: bash
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
- name: Setup Rust target
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- name: Add manifest lint dependencies
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
name: Add manifest lint dependencies
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
cargo install --quiet --version 1.0.9 --target "${{ matrix.target }}" cargo-sort
|
||||
cargo install --quiet --version 0.15.1 --target "${{ matrix.target }}" cargo-msrv
|
||||
- name: cargo-msrv verify melib MSRV
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
cargo-msrv --output-format json --log-level trace --log-target stdout --path meli verify
|
||||
cargo-msrv --output-format json --log-level trace --log-target stdout --path melib verify
|
||||
- name: cargo-sort
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f ./.gitea/Makefile.manifest-lint cargo-sort
|
||||
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace fuzz
|
||||
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace tools
|
||||
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace --workspace
|
||||
- name: Check debian/changelog is up-to-date.
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.manifest-lint check-debian-changelog
|
||||
./scripts/check_debian_changelog.sh
|
||||
|
|
103
.gitea/workflows/test.yaml
Normal file
103
.gitea/workflows/test.yaml
Normal file
|
@ -0,0 +1,103 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Run Tests
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.gitea/**'
|
||||
- 'melib/src/**'
|
||||
- 'melib/Cargo.toml'
|
||||
- 'meli/src/**'
|
||||
- 'meli/Cargo.toml'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev make
|
||||
#- id: cache-rustup
|
||||
# name: Cache Rust toolchain
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.rustup
|
||||
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
|
||||
- id: rustup-setup
|
||||
name: Install rustup and toolchains
|
||||
shell: bash
|
||||
run: |
|
||||
if ! command -v rustup &>/dev/null; then
|
||||
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 ${{ matrix.rust }} --target ${{ matrix.target }}
|
||||
fi
|
||||
- name: Configure cargo data directory
|
||||
# After this point, all cargo registry and crate data is stored in
|
||||
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
|
||||
# that are needed during the build process. Additionally, this works
|
||||
# around a bug in the 'cache' action that causes directories outside of
|
||||
# the workspace dir to be saved/restored incorrectly.
|
||||
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
|
||||
#- id: cache-cargo
|
||||
# name: Cache cargo configuration and installations
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.CARGO_HOME }}
|
||||
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
- name: Setup Rust target
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
name: Add test dependencies
|
||||
run: |
|
||||
cargo install --quiet --version 0.9.54 --target "${{ matrix.target }}" cargo-nextest
|
||||
- name: cargo-check
|
||||
run: |
|
||||
cargo check --all-features --all --tests --examples --benches --bins
|
||||
- name: Compile
|
||||
if: success() || failure()
|
||||
run: cargo test --all --no-fail-fast --all-features --no-run --locked
|
||||
- name: cargo test
|
||||
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() # 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
|
8
.mailmap
8
.mailmap
|
@ -1,8 +0,0 @@
|
|||
# Copyright (c) 2024 Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
# Licensed under the EUPL-1.2-or-later.
|
||||
#
|
||||
# You may obtain a copy of the Licence at:
|
||||
# https://joinup.ec.europa.eu/software/page/eupl
|
||||
#
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
Manos Pitsidianakis <manos@pitsidianak.is> <el13635@mail.ntua.gr>
|
33
BUILD.md
33
BUILD.md
|
@ -9,7 +9,7 @@ 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.
|
||||
`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`.
|
||||
|
@ -20,13 +20,7 @@ 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.
|
||||
|
||||
Cargo features for `meli` are documented in its [`README.md`](./meli/README.md) file.
|
||||
|
||||
Cargo features for `melib` are documented in its [`README.md`](./melib/README.md) file.
|
||||
|
||||
The following list explains each feature's purpose:
|
||||
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)
|
||||
|
@ -34,8 +28,15 @@ The following list explains each feature's purpose:
|
|||
- `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`
|
||||
|
@ -55,22 +56,6 @@ To use the optional gpg feature, you must have `libgpgme` installed in your syst
|
|||
In Debian-like systems, install the `libgpgme11` package.
|
||||
`meli` detects the library's presence on runtime.
|
||||
|
||||
## Building and running on Android with `termux`
|
||||
|
||||
This is not a supported or stable setup so caveat emptor.
|
||||
|
||||
At the time of writing this, Android is not a stable Rust target.
|
||||
The packaged Rust from `termux` will be used.
|
||||
|
||||
The following steps should suffice to build and run `meli` on `termux`:
|
||||
|
||||
```console
|
||||
$ pkg install rust perl make m4 man
|
||||
$ cargo install meli # ensure .cargo/bin is in your PATH
|
||||
```
|
||||
|
||||
Exporting `EDITOR` and `PAGER` might be useful.
|
||||
|
||||
## Development
|
||||
|
||||
Development builds can be built and/or run with
|
||||
|
|
1726
CHANGELOG.md
1726
CHANGELOG.md
File diff suppressed because it is too large
Load diff
1612
Cargo.lock
generated
1612
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
84
Makefile
84
Makefile
|
@ -25,7 +25,7 @@ CARGO_ARGS ?=
|
|||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility
|
||||
CARGO_SORT_BIN = cargo-sort
|
||||
CARGO_HACK_BIN = cargo-hack
|
||||
PRINTF := `command -v printf`
|
||||
PRINTF = /usr/bin/printf
|
||||
|
||||
# Options
|
||||
PREFIX ?= /usr/local
|
||||
|
@ -36,7 +36,7 @@ MANDIR ?= ${EXPANDED_PREFIX}/share/man
|
|||
# Installation parameters
|
||||
DOCS_SUBDIR ?= meli/docs/
|
||||
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5 meli.7
|
||||
FEATURES != [ -z "$${MELI_FEATURES}" ] && ($(PRINTF) -- '--all-features') || ($(PRINTF) -- '--features %s' "$${MELI_FEATURES}")
|
||||
FEATURES ?= --features "${MELI_FEATURES}"
|
||||
|
||||
MANPATHS != ACCUM="";for m in `manpath 2> /dev/null | tr ':' ' '`; do if [ -d "$${m}" ]; then REAL_PATH=`cd $${m} && pwd` ACCUM="$${ACCUM}:$${REAL_PATH}";fi;done;echo $${ACCUM}'\c' | sed 's/^://'
|
||||
VERSION = `grep -m1 version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1`
|
||||
|
@ -55,7 +55,6 @@ YELLOW ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 3)
|
|||
|
||||
.PHONY: meli
|
||||
meli: check-deps
|
||||
@echo ${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --release --bin meli
|
||||
@${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release --bin meli
|
||||
|
||||
.PHONY: help
|
||||
|
@ -76,61 +75,50 @@ help:
|
|||
@echo " - ${BOLD}deb-dist${ANSI_RESET} (builds debian package in the parent directory)"
|
||||
@echo " - ${BOLD}distclean${ANSI_RESET} (cleans distribution build artifacts)"
|
||||
@echo " - ${BOLD}build-rustdoc${ANSI_RESET} (builds rustdoc documentation for all packages in \$$CARGO_TARGET_DIR)"
|
||||
@echo ""
|
||||
@echo "ENVIRONMENT variables of interest:"
|
||||
@$(PRINTF) "* MELI_FEATURES "
|
||||
@[ -z $${MELI_FEATURES+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${MELI_FEATURES}${ANSI_RESET}
|
||||
@$(PRINTF) "* PREFIX "
|
||||
@[ -z ${EXPANDED_PREFIX} ] && echo "unset" || echo "= ${UNDERLINE}"${EXPANDED_PREFIX}${ANSI_RESET}
|
||||
@$(PRINTF) "* BINDIR = %s\n" "${UNDERLINE}${BINDIR}${ANSI_RESET}"
|
||||
@$(PRINTF) "* MANDIR "
|
||||
@[ -z ${MANDIR} ] && echo "unset" || echo "= ${UNDERLINE}"${MANDIR}${ANSI_RESET}
|
||||
@$(PRINTF) "* MANPATH = "
|
||||
@[ $${MANPATH+x} ] && echo ${UNDERLINE}$${MANPATH}${ANSI_RESET} || echo "unset"
|
||||
@echo "\nENVIRONMENT variables of interest:"
|
||||
@echo "* PREFIX = ${UNDERLINE}${EXPANDED_PREFIX}${ANSI_RESET}"
|
||||
@echo "* MELI_FEATURES = ${UNDERLINE}\n"
|
||||
@[ -z $${MELI_FEATURES+x} ] && echo "unset\c" || echo ${MELI_FEATURES}'\c'
|
||||
@echo ${ANSI_RESET}
|
||||
@echo "* BINDIR = ${UNDERLINE}${BINDIR}${ANSI_RESET}"
|
||||
@echo "* MANDIR = ${UNDERLINE}${MANDIR}${ANSI_RESET}"
|
||||
@echo "* MANPATH = ${UNDERLINE}\c"
|
||||
@[ $${MANPATH+x} ] && echo $${MANPATH}'\c' || echo "unset\c"
|
||||
@echo ${ANSI_RESET}
|
||||
@echo "* (cleaned) output of manpath(1) = ${UNDERLINE}${MANPATHS}${ANSI_RESET}"
|
||||
@$(PRINTF) "* NO_MAN "
|
||||
@[ $${NO_MAN+x} ] && echo "set" || echo "unset"
|
||||
@$(PRINTF) "* NO_COLOR "
|
||||
@([ $${NO_COLOR+x} ] && [ "$${NO_COLOR}" != "" ] && echo "set") || echo "unset"
|
||||
@echo "* NO_MAN ${UNDERLINE}\c"
|
||||
@[ $${NO_MAN+x} ] && echo "set\c" || echo "unset\c"
|
||||
@echo ${ANSI_RESET}
|
||||
@echo "* NO_COLOR ${UNDERLINE}\c"
|
||||
@[ $${NO_COLOR+x} ] && echo "set\c" || echo "unset\c"
|
||||
@echo ${ANSI_RESET}
|
||||
@echo "* CARGO_BIN = ${UNDERLINE}${CARGO_BIN}${ANSI_RESET}"
|
||||
@$(PRINTF) "* CARGO_ARGS "
|
||||
@([ -z "${CARGO_ARGS}" ] && echo "unset") || echo = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}
|
||||
@$(PRINTF) "* RUSTFLAGS = "
|
||||
@([ -z "${RUSTFLAGS}" ] && echo "unset") || echo = ${UNDERLINE}${RUSTFLAGS}${ANSI_RESET}
|
||||
@$(PRINTF) "* AUTHOR (for deb-dist) "
|
||||
@[ -z $${AUTHOR+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${AUTHOR}${ANSI_RESET}
|
||||
@echo "* CARGO_ARGS = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}"
|
||||
@echo "* MIN_RUSTC = ${UNDERLINE}${MIN_RUSTC}${ANSI_RESET}"
|
||||
@echo "* VERSION = ${UNDERLINE}${VERSION}${ANSI_RESET}"
|
||||
@echo "* GIT_COMMIT = ${UNDERLINE}${GIT_COMMIT}${ANSI_RESET}"
|
||||
@echo "* CARGO_TARGET_DIR = ${CARGO_TARGET_DIR}"
|
||||
@echo ""
|
||||
@echo "Built-in/binary utilities"
|
||||
@echo "* PRINTF = ${UNDERLINE}${PRINTF}${ANSI_RESET}"
|
||||
@#@echo "* CARGO_COLOR = ${CARGO_COLOR}"
|
||||
|
||||
.PHONY: check
|
||||
check: check-tagrefs
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
$(CARGO_BIN) +nightly fmt --all || $(CARGO_BIN) fmt --all
|
||||
@OUT=$$($(CARGO_SORT_BIN) melib -w 2>&1 && $(CARGO_SORT_BIN) meli -w 2>&1) || $(PRINTF) "WARN: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
|
||||
@$(CARGO_BIN) +nightly fmt --all || $(CARGO_BIN) fmt --all
|
||||
@OUT=$$($(CARGO_SORT_BIN) -w 2>&1) || $(PRINTF) "WARN: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test
|
||||
test: test-docs
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test-docs
|
||||
test-docs:
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --doc
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --doc
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --doc
|
||||
|
||||
.PHONY: test-feature-permutations
|
||||
test-feature-permutations:
|
||||
|
@ -148,8 +136,8 @@ clean:
|
|||
|
||||
.PHONY: distclean
|
||||
distclean:
|
||||
rm -f meli-${VERSION}.tar.gz
|
||||
rm -rf .pc # rm debian stuff
|
||||
@rm -f meli-${VERSION}.tar.gz
|
||||
@rm -rf .pc # rm debian stuff
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
|
@ -179,16 +167,16 @@ install-doc:
|
|||
|
||||
.PHONY: install-bin
|
||||
install-bin: meli
|
||||
mkdir -p $(DESTDIR)${BINDIR}
|
||||
@mkdir -p $(DESTDIR)${BINDIR}
|
||||
@echo " - ${BOLD}Installing binary to ${ANSI_RESET}${GREEN}${DESTDIR}${BINDIR}/meli${ANSI_RESET}"
|
||||
@case ":${PATH}:" in \
|
||||
*:${DESTDIR}${BINDIR}:*) echo "\n";; \
|
||||
*) echo "\n${RED}${BOLD}WARNING${ANSI_RESET}: ${UNDERLINE}Path ${DESTDIR}${BINDIR} is not contained in your PATH variable.${ANSI_RESET} Consider adding it if necessary.\nPATH variable: ${PATH}";; \
|
||||
esac
|
||||
mkdir -p $(DESTDIR)${BINDIR}
|
||||
rm -f $(DESTDIR)${BINDIR}/meli
|
||||
cp ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
|
||||
chmod 755 $(DESTDIR)${BINDIR}/meli
|
||||
@mkdir -p $(DESTDIR)${BINDIR}
|
||||
@rm -f $(DESTDIR)${BINDIR}/meli
|
||||
@cp ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
|
||||
@chmod 755 $(DESTDIR)${BINDIR}/meli
|
||||
|
||||
|
||||
.PHONY: install
|
||||
|
@ -203,19 +191,17 @@ install: meli install-bin install-doc
|
|||
|
||||
.PHONY: dist
|
||||
dist:
|
||||
git archive --format=tar.gz --prefix=meli-${VERSION}/ HEAD >meli-${VERSION}.tar.gz
|
||||
@git archive --format=tar.gz --prefix=meli-${VERSION}/ HEAD >meli-${VERSION}.tar.gz
|
||||
@echo meli-${VERSION}.tar.gz
|
||||
|
||||
AUTHOR ?= grep -m1 authors meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1
|
||||
.PHONY: deb-dist
|
||||
deb-dist:
|
||||
@$(PRINTF) "Override AUTHOR environment variable to set package metadata.\n"
|
||||
dpkg-buildpackage -b -rfakeroot -us -uc --build-by="${AUTHOR}" --release-by="${AUTHOR}"
|
||||
@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_`dpkg --print-architecture`.deb
|
||||
|
||||
.PHONY: build-rustdoc
|
||||
build-rustdoc:
|
||||
@echo RUSTDOCFLAGS=\""--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}"\" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" --all-features --no-deps --workspace --document-private-items --open
|
||||
@RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open
|
||||
|
||||
.PHONY: check-tagrefs
|
||||
|
|
101
README.md
101
README.md
|
@ -1,17 +1,14 @@
|
|||
# meli   [](https://github.com/meli/meli/blob/master/COPYING) [](https://crates.io/crates/meli) [](ircs://irc.oftc.net:6697/%23meli)
|
||||
# meli   [](https://github.com/meli/meli/blob/master/COPYING) [](https://crates.io/crates/meli) [](ircs://irc.oftc.net:6697/%23meli)
|
||||
|
||||
**BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).**
|
||||
|
||||
Try an [old, outdated but online and interactive web demo](https://meli-email.org/wasm2.html "online interactive web demo") powered by WebAssembly!
|
||||
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/)
|
||||
* Main repository <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")<details><summary>Official git mirrors</summary>
|
||||
- <https://codeberg.org/meli/meli>
|
||||
- <https://github.com/meli/meli>
|
||||
- <https://ayllu-forge.org/meli/meli>
|
||||
- <https://gitlab.com/meli-project/meli>
|
||||
</details>
|
||||
* `#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**:
|
||||
|
||||
|
@ -26,56 +23,23 @@ Try an [old, outdated but online and interactive web demo](https://meli-email.or
|
|||
|
||||
## Install
|
||||
|
||||
<a href="https://repology.org/project/meli/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/meli.svg" alt="Packaging status table by repology.org" align="right">
|
||||
</a>
|
||||
|
||||
- `cargo install meli` or `cargo install --git https://git.meli-email.org/meli/meli.git meli` [crates.io link](https://crates.io/crates/meli)
|
||||
- Official Debian packages <https://packages.debian.org/trixie/meli>
|
||||
- AUR (archlinux) <https://aur.archlinux.org/packages/meli>
|
||||
- NetBSD with pkgsrc <https://pkgsrc.se/mail/meli>
|
||||
- OpenBSD ports <https://openports.pl/path/mail/meli>
|
||||
- macOS with MacPorts <https://ports.macports.org/port/meli/>
|
||||
- Nix with Nixpkgs <https://search.nixos.org/packages?query=meli>
|
||||
- [Pre-built debian package, static binaries](https://github.com/meli/meli/releases/ "github releases for meli") for <code>amd64</code>, <code>arm64</code> architectures
|
||||
- [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 `make` or `cargo build --release --bin meli`.
|
||||
Run `cargo build --release --bin meli` or `make`.
|
||||
|
||||
For detailed building instructions, see [`BUILD.md`](./BUILD.md)
|
||||
|
||||
### Cargo Compile-time Features
|
||||
|
||||
`meli` supports opting in and out of features at compile time with cargo features.
|
||||
|
||||
The contents of the `default` feature are:
|
||||
|
||||
```toml
|
||||
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "static"]
|
||||
```
|
||||
|
||||
A list of all the features and a description for each follows:
|
||||
|
||||
| Feature flag | Dependencies | Notes |
|
||||
|---------------------------------------------------------------|----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| <a name="notmuch-feature">`notmuch`</a> | `maildir` feature | Provides the *notmuch* backend |
|
||||
| <a name="jmap-feature">`jmap`</a> | `http` feature, `url` crate with `serde` feature | Provides the *JMAP* backend |
|
||||
| <a name="smtp-feature">`smtp`</a> | `tls` feature | Integrated async *SMTP* client |
|
||||
| <a name="sqlite3-feature">`sqlite3`</a> | `rusqlite` crate with `bundled-full` feature | Used in caches |
|
||||
| <a name="sqlite3-static-feature">`sqlite3-static`</a> | `rusqlite` crate with `bundled-full` feature | Same as `sqlite3` feature but provided for consistency and in case `sqlite3` feature stops bundling libsqlite3 statically in the future. |
|
||||
| <a name="smtp-trace-feature">`smtp-trace`</a> | `smtp` feature | Connection trace logs on the `trace` logging level |
|
||||
| <a name="gpgme-feature">`gpgme`</a> | | *GPG* use by dynamically loading `libgpgme.so` |
|
||||
| <a name="tls-static-feature">`tls-static`</a> | `native-tls` crate with `vendored` feature | Links with `OpenSSL` statically where it's used |
|
||||
| <a name="http-static-feature">`http-static`</a> | `isahc` crate with `static-curl` feature | Links with `curl` statically |
|
||||
| <a name="dbus-notifications-feature">`dbus-notifications`</a> | `notify-rust` dependency | Uses DBus notifications |
|
||||
| <a name="dbus-static-feature">`dbus-static`</a> | `notify-rust` dependency and enableds its `d_vendored` feature | Includes the dbus library statically. |
|
||||
| <a name="cli-docs-feature">`cli-docs`</a> | `flate2` dependency | 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]` |
|
||||
| <a name="libz-static-feature">`libz-static`</a> | `libz-sys` dependency and enables its `static` feature | Allows for the transitive dependency libz (from `curl`) to be linked statically. |
|
||||
| <a name="static-feature">`static`</a> | enables `tls-static`, `http-static`, `sqlite3-static`, `dbus-static`, `libz-static` features | |
|
||||
|
||||
## Quick start
|
||||
|
||||
<table>
|
||||
<tr><td>
|
||||
|
||||
```sh
|
||||
# Create configuration file in ${XDG_CONFIG_HOME}/meli/config.toml:
|
||||
$ meli create-config
|
||||
|
@ -85,19 +49,16 @@ $ meli edit-config
|
|||
$ meli install-man
|
||||
# Ready to go.
|
||||
$ meli
|
||||
# You can read any manual page with the CLI subcommand `man`:
|
||||
$ meli man meli.7
|
||||
# See help output for all options and subcommands.
|
||||
$ meli --help
|
||||
```
|
||||
|
||||
</td><td>
|
||||
|
||||
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.
|
||||
Examples for configuration file settings can be found in `meli.conf.examples(5)`
|
||||
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`.
|
||||
|
||||
|
@ -107,22 +68,26 @@ You can run meli with arbitrary configuration files by setting the `${MELI_CONFI
|
|||
MELI_CONFIG=./test_config cargo run
|
||||
```
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
See [`meli(7)`](./meli/docs/meli.7) for an extensive tutorial and [`meli.conf(5)`](./meli/docs/meli.conf.5) for all configuration values.
|
||||
|
||||
| Main view | Compact main view | Compose with embed terminal editor |
|
||||
|-----------|-------------------|------------------------------------|
|
||||
|  |  |  |
|
||||
| | | |
|
||||
:---:|:---:|:---:
|
||||
 |  | 
|
||||
Main view | Compact main view | Compose with embed terminal editor
|
||||
|
||||
### Supported E-mail backends
|
||||
|
||||
| Protocol | Support |
|
||||
|---------------|------------|
|
||||
| IMAP | full |
|
||||
| Maildir | full |
|
||||
| notmuch | full[^0] |
|
||||
| mbox | read-only |
|
||||
| JMAP | functional |
|
||||
| NNTP / Usenet | functional |
|
||||
| Protocol | Support |
|
||||
|:------------:|:----------------|
|
||||
| IMAP | full |
|
||||
| Maildir | full |
|
||||
| notmuch | full[^0] |
|
||||
| mbox | read-only |
|
||||
| JMAP | functional |
|
||||
| NNTP / Usenet| functional |
|
||||
|
||||
[^0]: there's no support for searching through all email directly, you'd have to
|
||||
create a mailbox with a notmuch query that returns everything and search
|
||||
|
|
141
cliff.toml
141
cliff.toml
|
@ -1,9 +1,5 @@
|
|||
# configuration for https://github.com/orhun/git-cliff
|
||||
|
||||
[remote.gitea]
|
||||
owner = "meli"
|
||||
repo = "meli"
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
|
@ -24,17 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
body = """
|
||||
|
||||
{% if not version %}
|
||||
## Unreleased
|
||||
## [Unreleased]
|
||||
{% else %}
|
||||
## [{{ version }}]({{ "https://git.meli-email.org/meli/meli/releases/tag/" ~ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% endif %}{% if get_env(name = "FRIENDS", default = "") != "" %}
|
||||
Contributors in alphabetical order:
|
||||
|
||||
{{ get_env(name = "FRIENDS") }}
|
||||
{%- endif -%}{% if gitea and gitea.contributors %}{% for contributor in gitea.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* [@{{ contributor.username }}](https://git.meli-email.org/{{ contributor.username }}) made their first contribution in [#{{ contributor.pr_number }}]({{ "https://git.meli-email.org/meli/meli/pulls/" ~ contributor.pr_number }})
|
||||
{%- endfor -%}{%- endif -%}{% macro commit(commit) -%}
|
||||
- [**`{{ 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 }}`{% if commit.remote and commit.remote.pr_number and commit.remote.pr_title %} in PR [`#{{ commit.remote.pr_number }}` "{{ commit.remote.pr_title }}"]({{ "https://git.meli-email.org/meli/meli/pulls/" ~ commit.remote.pr_number }}){%- endif -%}{% endmacro -%}
|
||||
## [{{ 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.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 }}
|
||||
|
@ -59,95 +50,61 @@ footer = """
|
|||
"""
|
||||
|
||||
[git]
|
||||
# don't parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = false
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = false
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://git.meli-email.org/meli/meli/issues/${2}))" }, # Replace the issue number with the link
|
||||
{ pattern = " +", replace = " "}, # Replace multiple spaces with a single space
|
||||
{ pattern = "`[^`]+`", replace_command = "pandoc -f commonmark -t plain" },
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://git.meli-email.org/meli/meli/issues/${2}))" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^chore\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^CHANGELOG", skip = true },
|
||||
{ message = "(?i)^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
{ message = "^.github", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^.gitea", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "(?i)^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^scripts", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = '(?i)^chore', group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^debian", group = "<!-- 06 -->Packaging" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance" },
|
||||
{ message = "(?i)readme", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "(?i)anpage", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "(?i)anual", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "meli.[17]", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "meli.conf.5", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "meli-themes.5", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "README.md", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "BUILD.md", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "DEVELOPMENT.md", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^[^.]*.rs:", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)^refactor\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)lints?\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)move\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)replace\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)remove\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)refactor\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)rename\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)formatting\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)cleanups?\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)fix\b', group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = '(?i)fixups?\b', group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = "(?i)implement", group = "<!-- 00 -->Added" },
|
||||
{ message = '(?i)add\b', group = "<!-- 00 -->Added" },
|
||||
{ message = '(?i)^update\b', group = "<!-- 02 -->Changes" },
|
||||
{ message = '(?i)\bdependency\b', group = "<!-- 02 -->Changes" },
|
||||
{ message = "^feat", group = "<!-- 00 -->Added" },
|
||||
{ message = '(?i)retry\b', group = "<!-- 02 -->Changes" },
|
||||
{ message = "^conf", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^contacts?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^compos[ie]?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^types", group = "<!-- 02 -->Changes" },
|
||||
{ message = '(?i)^use', group = "<!-- 02 -->Changes" },
|
||||
{ message = "^terminal", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^listing", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^mail", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^utilities", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^view", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^[aA]dd", group = "<!-- 00 -->Added" },
|
||||
{ message = "[fF]ix", group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = "[rR]efactor", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "[mM]ove", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "[rR]emove", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "^refactor", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "^[^.]*.rs:", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "^meli", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^melib", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^imap", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^jmap", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^notmuch", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^mbox", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^smtp", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^mbox", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "[mM]anual", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "[mM]anpage", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "[rR]eadme", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance" },
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^debian", group = "<!-- 06 -->Packaging" },
|
||||
{ message = "^mail/view", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^backends?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^commands?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^actions?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^log", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^pgp", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^gpgme", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^manage", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^smtp", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^mbox", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^jmap", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^imap", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^nntp", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^notmuch", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^melib", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^meli", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^accounts", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^embedded", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^jobs", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^view", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^utilities", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^mail", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^listing", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^terminal", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^types", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^conf", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^scripts", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
{ message = ".*", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
],
|
||||
"codeRepository": "https://git.meli-email.org/meli/meli.git",
|
||||
"dateCreated": "2016-04-25",
|
||||
"dateModified": "2024-11-27",
|
||||
"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.10.tar.gz",
|
||||
"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": [
|
||||
|
@ -49,21 +49,14 @@
|
|||
],
|
||||
"programmingLanguage": "Rust",
|
||||
"relatedLink": [
|
||||
"https://lists.meli-email.org/",
|
||||
"https://codeberg.org/meli/meli",
|
||||
"https://github.com/meli/meli",
|
||||
"https://gitlab.com/meli-project/meli",
|
||||
"https://crates.io/crates/meli",
|
||||
"https://packages.debian.org/trixie/meli",
|
||||
"https://pkgsrc.se/mail/meli",
|
||||
"https://openports.pl/path/mail/meli",
|
||||
"https://ports.macports.org/port/meli/",
|
||||
"https://search.nixos.org/packages?query=meli"
|
||||
"https://lists.meli-email.org/"
|
||||
],
|
||||
"version": "0.8.10",
|
||||
"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/tag/v0.8.10/README.md",
|
||||
"buildInstructions": "https://git.meli-email.org/meli/meli/raw/tag/v0.8.10/BUILD.md"
|
||||
"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,50 +0,0 @@
|
|||
<!-- SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later -->
|
||||
# Useful scripts and files for use with `meli`
|
||||
|
||||
This directory includes various useful scripts and files that are contributed
|
||||
by the community and not actively maintained or supported.
|
||||
|
||||
If you believe something in this directory needs updates to work with the
|
||||
current version of `meli` or there are bugs that need fixing, please file an
|
||||
issue on our issue tracker!
|
||||
|
||||
## Connecting to a Gmail account with OAUTH2
|
||||
|
||||
The script [`./oauth2.py`](./oauth2.py) is a helper script to authenticate to a Gmail account using IMAP OAUTH2 tokens.
|
||||
|
||||
See [`meli.conf(5)`](../meli/docs/meli.conf.5) for documentation.
|
||||
|
||||
If the script does not work and you're certain it's because it needs changes to
|
||||
work with Google's servers and not a user error on your part, please file a bug
|
||||
on our issue tracker!
|
||||
|
||||
## Using `meli` for `mailto:` links
|
||||
|
||||
To use `meli` to open `mailto:` links from your browser place the [`mailto-meli`](./mailto-meli) and [`mailto-meli-expect`](./mailto-meli-expect) scripts into `/usr/bin`
|
||||
(or `.local/bin`, and adjust the path in the script accordingly).
|
||||
|
||||
Ensure all scripts are executable by your user account, if not set the permissions accordingly:
|
||||
|
||||
```sh
|
||||
chmod u+x /path/to/mailto-meli
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```sh
|
||||
chmod u+x /path/to/mailto-meli-expect
|
||||
```
|
||||
|
||||
Then set `mailto-meli` as program to open `mailto` links
|
||||
in your browser.
|
||||
|
||||
E.g. in Firefox this can be done under "Settings" (`about:preferences`) which you can access from the menu button or `Edit -> Settings`.
|
||||
|
||||
```text
|
||||
General -> Applications -> Content-Type: mailto.
|
||||
```
|
||||
|
||||
You can test that it works by clicking the system menu entry `File -> Email link...`.
|
||||
|
||||
_NOTE_: that you need to have the [`expect`](https://en.wikipedia.org/wiki/Expect) binary installed for this to work.
|
||||
`expect` is a scripting language used for interactive with interactive terminal applications like `meli`.
|
|
@ -1,21 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
#
|
||||
# mailto-meli -- wrapper to use meli as mailto handler
|
||||
# To use meli as mailto: handler point your browser to use this as application for opening
|
||||
# mailto: links.
|
||||
# Note: This assumes that x-terminal-emulator supports the "-e" flag for passing along arguments.
|
||||
|
||||
# Copyright: 2024 Matthias Geiger <werdahias@debian.org>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Check if mailto-meli and expect are present
|
||||
if ! command -v mailto-meli > /dev/null 2>&1
|
||||
then echo "mailto-meli not found" && exit 1
|
||||
else
|
||||
if ! command -v expect > /dev/null 2>&1
|
||||
then echo "expect not found" && exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
exec x-terminal-emulator -e mailto-meli-expect "$@"
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env -S expect -f
|
||||
# Copyright 2024 Manos Pitsidianakis
|
||||
#
|
||||
# SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
# Trap window resize signal
|
||||
trap {
|
||||
set rows [stty rows]
|
||||
set cols [stty columns]
|
||||
stty rows $rows columns $cols < $spawn_out(slave,name)
|
||||
} WINCH
|
||||
# send the input with human-like delay:
|
||||
set send_human {.001 .003 0.01 .005 .005}
|
||||
spawn meli
|
||||
send -h ":mailto "
|
||||
send -h [lindex $argv 0]
|
||||
send -h "\n"
|
||||
interact
|
640
debian/changelog
vendored
640
debian/changelog
vendored
|
@ -1,643 +1,3 @@
|
|||
meli (0.8.10-1) bookworm; urgency=low
|
||||
|
||||
Highlights:
|
||||
===========
|
||||
|
||||
- added pipe-attachment command
|
||||
- added sample scripts for using meli as a mailto scheme handler in
|
||||
contrib/
|
||||
- fixed GPG encryption with libgpgme
|
||||
|
||||
Contributors in alphabetical order:
|
||||
===================================
|
||||
|
||||
- Manos Pitsidianakis
|
||||
- Matthias Geiger
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- 5e77821f mail/view: add pipe-attachment command in PR #540
|
||||
"mail/view: add pipe-attachment command"
|
||||
- fa896f6b contrib: add mailto: scheme handler scripts
|
||||
- 00ce9660
|
||||
melib/backends: add as_any/as_any_mut methods to BackendMailbox
|
||||
- fd243fa5 maildir: add mailbox creation tests
|
||||
- de65eec3 meli/accounts: add mailbox_by_path() tests in PR #535
|
||||
"Rework maildir mailbox path logic, add tests"
|
||||
- 6b363601 melib/gpgme: impl Display for gpgme::Key
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- 60c90d75 melib/attachments: ensure MIME boundary prefixed with CRLF
|
||||
- 3433c5c3 compose/pgp: rewrite key selection logic in PR #541 "More
|
||||
gpgme/PGP fixes again"
|
||||
- 12de82e7 melib/conf: fix mutt_alias_file not being validated in PR
|
||||
#550 "Remove sealed_test dependency"
|
||||
- c8e055a7 Fix version migrations being triggered backwards in PR #557
|
||||
"Fix version migrations being triggered backwards"
|
||||
- efab99fd
|
||||
terminal: check for NO_COLOR env var without unicode validation
|
||||
- 36a63e88 melib/maildir: rewrite create_mailbox()
|
||||
- fcab855f view: ensure envelope headers are always populated in PR
|
||||
#538 "view: ensure envelope headers are always populated"
|
||||
- 84564f44 mailcap: don't drop File before opening it in PR #552
|
||||
"mailcap: don't drop File before opening it"
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- ed85da51 Remove sealed_test dependency
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 03df2ac1 meli/utilities: add print utilities for tests
|
||||
- 18e9d5c1 conf.rs: impl From<melib::AccountSettings> for AccountConf
|
||||
- 1f2fec19 Fix 1.83.0 lints in PR #536 "CI: Add action to check for
|
||||
DCO signoffs in PRs"
|
||||
- 192ecea2 compose/gpg.rs: Fix msrv regression
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- 4a61a4b8 melib: include README.md as preamble of crate rustdocs
|
||||
- 80e53471 BUILD.md: move melib specific stuff to melib/README.md
|
||||
- 91a17ece melib/README.md: mention sqlite3-static feature
|
||||
- b77a691b meli/README.md: Add cargo features section in PR #549
|
||||
"Document cargo features in READMEs"
|
||||
- 91dc271d contrib: add a README.md file
|
||||
- 2e900be6 contrib/README.md: add section about oauth2.py
|
||||
- 07812d2c contrib/README.md: elaborate a bit about mailto in PR #545
|
||||
"Add external mailto: handler support via scripts in contrib"
|
||||
- e784e8d2 scripts: add markdown_doc_lints.py
|
||||
|
||||
Continuous Integration
|
||||
======================
|
||||
|
||||
- 77629851 CI: Add action to check for DCO signoffs in PRs
|
||||
- f944ebed CI: Add error msg when cargo-derivefmt check fails
|
||||
- d49344f9 CI: Move MSRV checks from manifest to lints in PR #553
|
||||
"ci-workflow-fixes"
|
||||
- ece6bfc2 CI: non-zero exit if cargo-derivefmt-* targets fail
|
||||
- 2257b91b CI: add actions/cache steps in PR #554 "CI: add
|
||||
actions/cache steps"
|
||||
- a1c9524f CI: fix check_dco.sh not working with other repos in PR
|
||||
#555 "CI: fix check_dco.sh not working with other repos"
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Fri, 06 Dec 2024 07:03:58 +0200
|
||||
|
||||
meli (0.8.9-1) bookworm; urgency=low
|
||||
|
||||
This is mostly a fixups release.
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- cf16bf65 meli/sqlite3: add tests for reindexing
|
||||
- a389772d accounts: suggest tips on mailbox_by_path error
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- 25f0a3f8 conf/terminal: fix serde of ProgressSpinnerSequence
|
||||
- c375b48e terminal: fix Synchronized Output response parsed as input
|
||||
in PR #523 "terminal: fix Synchronized Output response parsed as
|
||||
input"
|
||||
- b7e215f9
|
||||
melib/utils: fix test_fd_locks() on platforms without OFD support in
|
||||
PR #524 "melib/utils: fix test_fd_locks() on platforms without OFD
|
||||
support"
|
||||
- 25c32a6b meli/docs/meli.conf.examples.5: fix .Dt macro arguments
|
||||
- 18ae5848 meli: fix reindex of previously indexed account with sqlite3
|
||||
backend
|
||||
- 13e917d9 Fix some compilation errors with cfg feature attrs in PR #531
|
||||
"accounts: suggest tips on mailbox_by_path error"
|
||||
- 8c176d38 contacts/editor: fix crash on saving contact in PR #532
|
||||
"contacts/editor: fix crash on saving contact"
|
||||
- fb5a88c2
|
||||
melib/collection: ensure mailbox exists when inserting new envelopes
|
||||
in PR #529 "Small account stuff fixes"
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- 7f8f1cf6 melib/gpgme bindings renewal in PR #533 "melib/gpgme
|
||||
bindings renewal"
|
||||
- 9b7825bc Update futures-util dep, remove stderrlog dep
|
||||
- 4be69360 Remove obsolete "encoding" dependency in PR #530
|
||||
"Remove/update obsolete dependencies"
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 5af6e059 meli/accounts: use Arc<str> for account name
|
||||
- 567270e1 melib: use Vec instead of SmallVec for search results
|
||||
- 2bd8d7ba
|
||||
conf/tests.rs: Rename test functions to follow path convention
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- 97242482 meli/docs: add meli.conf.examples to CLI and tests
|
||||
- 0f096338 README.md: Update ways to install, add gitlab mirror link
|
||||
in PR #528 "Integrate meli.conf.examples.5 into CLI and build, also
|
||||
update README with installation instructions"
|
||||
|
||||
Continuous Integration
|
||||
======================
|
||||
|
||||
- 630df308 CI: Add arm64 runners in job matrices in PR #527 "CI: Add
|
||||
arm64 runners in job matrices"
|
||||
- 49ecbb56 CI: .gitea/Makefile.lint: check if nightly exists
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Wed, 27 Nov 2024 16:16:06 +0200
|
||||
|
||||
meli (0.8.8-1) bookworm; urgency=low
|
||||
|
||||
WARNING: This release contains a breaking change in the configuration
|
||||
file: a global composing option is not required anymore. Now, composing
|
||||
options are per account.
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- f3d59ebf accounts: add force: bool arg to load()
|
||||
- 33836a32 melib/error: add WrapResultIntoError helper trait
|
||||
- 3216324c melib/mbox: impl FromStr for MboxFormat
|
||||
- 94f345d7 Implement mailbox renaming command
|
||||
- 8d45ecc1 melib/error: add related_path field
|
||||
- bf3a4c5d error: add ErrorChainDisplay struct for better output
|
||||
- 6be5fd26
|
||||
themes: add inheritance, and use themes when initializing grids
|
||||
- 0ee7fc4d Print clickable path links with subcommands
|
||||
- aed7a60f samples: add ibm-modern theme in PR #469 "conf-refactor"
|
||||
- 4bbf446b utils: add unix file locks module
|
||||
- 6fbf569f search: add Message-ID, and other header search support
|
||||
- 26d33ce5 address: add separator argument to display_slice()
|
||||
- 32e3be8b
|
||||
sqlite3: add optional directory field in DatabaseDescription
|
||||
- dbbb1529 Add missing ComponentUnrealize handlers
|
||||
- 87d2cec9 Add sealed_test dependency
|
||||
- 604ae111 Impl From<&[u8]> for u64-based hash newtypes
|
||||
- 8205c7f5 melib: add JsContact module in PR #479 "view-filters"
|
||||
- 2af5c8b6 terminal: add QuerySynchronizedOutputSupport WIP
|
||||
- 5c4faea5 Add transpose shortcut and tests for text field
|
||||
- e9b87b2e melib/maildr: add rename_regex config option
|
||||
- 8f0e1d66 Add human-readable identifiers in temp draft files
|
||||
- 601e3711 Add vCard exports
|
||||
- 719e2eb2 listing: add customizable view divider like sidebar's in PR
|
||||
#485 "listing: add customizable view divider like sidebar's"
|
||||
- ba3ad8ed listing: always show mail_view_divider in PR #486 "listing:
|
||||
always show mail_view_divider"
|
||||
- 46b2c3b1 Add listing.thread_layout config flag in PR #487 "Add
|
||||
listing.thread_layout config flag"
|
||||
- aaea3a5a nntp: add timeout conf flag
|
||||
- d4636bcc nntp: interpret IMPLEMENTATION cap as metadata
|
||||
- 5f120309 nntp: add select_group_by_name() method
|
||||
- 9a9cd03d nntp: add NntpType::article_message_id() method
|
||||
- 7cfcbb7a Add patch_retrieve module in PR #489 "Add patch_retrieve
|
||||
module"
|
||||
- c82341f3 File: try trimming filename if ENAMETOOLONG
|
||||
- 23395491 compose/pgp: add encrypt_for_self flag
|
||||
- 0b6988b7 gpgme: add always trust flag to encrypt op
|
||||
- be3b3ef8 melib/utils: add fnmatch(3) interface
|
||||
- 32f7e50f Add version migration support
|
||||
- a6c7621c jscontact: add {created,updated} fields
|
||||
- 39592ad0 jmap: implement changing mailbox subscription
|
||||
- ca7eb792 jmap: Implement deleting email
|
||||
- b8e841bb jmap: implement mailbox deletion
|
||||
- 77e7c3df Add support for signatures in PR #500 "Add support for
|
||||
signatures"
|
||||
- dba5b68b components: add prelude module
|
||||
- f656aff0 composer: add discard-draft command
|
||||
- 789a88b2 shortcuts: add select_motion equivalent to select_entry
|
||||
- cb2dd5de listing/threaded: impl missing select functionality in PR
|
||||
#514 "listing/threaded: impl missing filter functionality"
|
||||
- c1901c96
|
||||
melib/email/compose: add Content-Type header for utf8 text plain attachments
|
||||
- 0e77bd5b
|
||||
melib/email/compose/tests: add multipart mixed attachment test in PR
|
||||
#515 "Fix incorrect multipart/mixed rendering when sending text with
|
||||
attachments under certain circumstances"
|
||||
- 7b1be139 melib: make mbox backend build by default
|
||||
- 7ff1db14 manage-mailboxes: add delete option in PR #520
|
||||
"manage-mailboxes: add delete option"
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- 6b05279a Update time dep to fix 1.80.0 breakage
|
||||
- 2084ce93 Fix invalid cfg feature combinations for macos in PR #471
|
||||
"Fix invalid cfg feature combinations for macos"
|
||||
- 4707ec9f text/line_break: fix ReflowState::{No,All} break
|
||||
- 86e25bc0 sqlite: fix database reset sequence
|
||||
- 4d4e189c imap: code style fixups
|
||||
- 335cca88 listing: fix highlight_self flag off by one error in PR
|
||||
#477 "listing: fix highlight_self flag off by one error"
|
||||
- 80915832 mailto: rewrite parsing in PR #480 "mailto-rewrite"
|
||||
- 65b32e77 subcommands: Fix wrong help info in imap-shell prompt
|
||||
- d0c81749 conf::data_types: minor style and error msg fixups
|
||||
- 7dbee81d view: fix nested filter jobs never being completed
|
||||
- f78884ce melib/nntp: fix an ancient FIXME
|
||||
- e0cfe8e4 Fix compilation for 32-bit architectures in PR #492 "Fix
|
||||
compilation for 32-bit architectures"
|
||||
- 1b708a99 melib: attempt FromSql from Blob for u64 hash in PR #506
|
||||
"melib: attempt FromSql from Blob for u64 hash"
|
||||
- 6c315580 compose: fix add-attachment-file-picker
|
||||
- c6e9e424 listing/threaded: impl missing filter functionality
|
||||
- e7a164de Configure some gpgme stuff under gpgme feature
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- 8e300c46 melib/jmap: call req text(). asap
|
||||
- 374ea8ba accounts: extract tests to tests.rs file
|
||||
- 7020cd66 meli: derive PartialEq/Eq for some types
|
||||
- 69065859 accounts: split mailbox to enum out of JobRequest
|
||||
- 14f2d911 melib/backends: change RefreshEvent field decl order
|
||||
- 56b1bf28 meli/accounts: batch process refresh events
|
||||
- 6513c188 melib/imap: on sync only update exists/unseen if loaded
|
||||
- a8dad317 melib/imap: renamed cache module to sync
|
||||
- 9e9c04a3 Update indexmap dep to 2.3.0
|
||||
- 2b3828d8 Update futures dependency to 0.3.30
|
||||
- 84812941
|
||||
melib/jmap: do not serialize server-set fields in Set create
|
||||
- eda6620c jmap: detect supported Auth schemes on connect in PR #467
|
||||
"jmap: detect supported Auth schemes on connect"
|
||||
- 35f12b15 embedded: prevent double-close of pty fd in PR #468
|
||||
"embedded: prevent double-close of pty fd"
|
||||
- 0bed37b5 melib: use IndexMap in conf fields
|
||||
- f3ad824d meli: use itoa to format offset indices in listings
|
||||
- 1cfb0b15 Update nix dependency to 0.29.0
|
||||
- 9c1b4424 jobs: make cancel flag an AtomicBool
|
||||
- f06a9072 jmap: fetch mailbox with receivedAt descending sort
|
||||
- 53b0d035 accounts: cancel any previous mailbox fetches
|
||||
- 60833ee5 accounts: make mailbox available as soon as possible
|
||||
- 28f45805 mail/view: try cancel env fetch on Drop
|
||||
- 2bb9b20d mail/view: do not highlight reply subjects in thread
|
||||
- a4f344b3 Use create_new to avoid overwriting files
|
||||
- d6197e8b listing: clear count modifier on Home/End
|
||||
- b798ca4a imap: return cached response in {select,examine}_mailbox()
|
||||
- 151fcebe imap: use BTreeMap for message sequence number store
|
||||
- e48fcc33 imap/protocol_parser: also populate other_headers
|
||||
- 1e11c29c imap: resync cache first when fetching a mailbox
|
||||
- 1779ad5d imap: interpret empty server response as BYE
|
||||
- 2d320688 mail/listing: pre-lookup conf values
|
||||
- 4e967280 nntp: don't needlessly select group before ARTICLE in PR
|
||||
#473 "Various"
|
||||
- 67b88d24 Update polling dependency from "2.8" to "3"
|
||||
- 14d74f36 Update smol dependency from "1" to "2"
|
||||
- b950fcea melib: Use IndexMap in VCard
|
||||
- 32acc347 view: show signature verification properly
|
||||
- ac1349b8 command: alias pwd to cwd
|
||||
- 7c056e4b Retry loading mailbox on recoverable error in PR #481
|
||||
"Retry loading mailbox on recoverable error"
|
||||
- cbafdcf7 terminal: color report WIP
|
||||
- 4a26cfa1 logging: disable tracing from output
|
||||
- 90974e7c imap: cache miss if row env hash != row hash
|
||||
- 4c44c440 melib: #[ignore] shellexpand tests
|
||||
- dc9e91df contacts/editor: Use FormButtonAction in form
|
||||
- c0511901 Update debian/meli.{docs,examples} and Cargo exclude
|
||||
- 592ce159 mbox: use Uuid::nil() as default envelope from
|
||||
- 6eeb4571 nntp: make all fields public
|
||||
- b27bac7f nntp: use DEFLATE when available by default
|
||||
- 128b959f
|
||||
nntp: prepend Newsgroups header if missing on NntpType::submit()
|
||||
- a69122f8
|
||||
pgp: use default sign/encrypt keys when no keys are selected
|
||||
- e6fa7093 view/envelope: trim headers values to 3 lines maximum
|
||||
- 7f0157a9 compose: make dialogs bigger in height in PR #490 "pgp: use
|
||||
default sign/encrypt keys when no keys are selected"
|
||||
- e032acfa view: pass filtered body to Composer as reply text in PR
|
||||
#493 "view: pass filtered body to Composer as reply text"
|
||||
- 49dcbc5e terminal: Extend Ask default actions, prompts
|
||||
- cd2e4bf3 melib/utils: vendor urn crate
|
||||
- 5915f125 backends: use IsSubscribedFn in method signatures
|
||||
- 4f927bbe nntp: properly return all nntp mailboxes
|
||||
- b930cb49 maildir: do not use rename_regex when only updating flags
|
||||
- 27486f29 Accept newer versions of base64 dependency
|
||||
- c3cac77d Update imap-codec dependency to 2.0.0-alpha.4
|
||||
- 05f404ba jobs: do not use AtomicU64 in PR #505 "jobs: do not use
|
||||
AtomicU64"
|
||||
- 46916895 melib/gpgme: s/NULL/NUL when referring to NUL byte
|
||||
- 81ace71b terminal/embedded: lift error checking earlier
|
||||
- 24114811 manage: parse scroll_{left,right} actions
|
||||
- d2559e42 imap: return all mailboxes, not just subscribed ones in PR
|
||||
#509 "compose: fix add-attachment-file-picker"
|
||||
- 320fddad melib/gpgme: disable layout tests on non-x86_64 hosts in PR
|
||||
#511 "melib/gpgme: disable layout tests on non-x86_64 hosts"
|
||||
- bcbcb012
|
||||
melib/email/compose: ensure boundary always prefixed with CRLF
|
||||
- d21c686d melib/attachments: Make AttachmentBuilder::set_raw generic
|
||||
- d5d34579 melib/email/compose/tests: normalise test fn names
|
||||
- e9ec6761 melib: make base64 dep mandatory
|
||||
- 30405216 melib: make notmuch feature depend on maildir feature
|
||||
- 35fa8e94 melib/imap: gracefully retry without DEFLATE on BYE in PR
|
||||
#517 "Fix some unrelated bugs I found while debugging build failure
|
||||
on armhf"
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 20d73292 melib: replace async-stream dep with async-fn-stream
|
||||
- 201081b6 meli/command: move tests to tests.rs
|
||||
- 84cfa358 conf: remove need for global send_mail setting
|
||||
- 7be8912c Cargo.tomls: make formatting more consistent
|
||||
- e6877e89 melib/jmap: refactor some parser imports
|
||||
- f7ec6d6b melib/jmap: implement mailbox rename
|
||||
- 15d24ab0 meli/jobs: refactor spawn_{blocking,specialized} to spawn()
|
||||
- 6ee148c0 Fix 1.80.0 clippy lints
|
||||
- de72bc6a melib/error.rs: move network stuff to submodule
|
||||
- a214a35c conf: refactor into submodules
|
||||
- 978cefbb Replace Escape ascii char with hex literal
|
||||
- 4b959f5c Remove pcre feature/dependency
|
||||
- 036586a2 Update serde dependency to 1.0.205
|
||||
- 191725b5
|
||||
Fix some borrow checker error/warnings from upcoming 2024 edition
|
||||
- 11798be8 Replace Envelope::message_id_display() with Display impls
|
||||
- 394236ba email/address: Refactor References struct
|
||||
- a7c73fc8 gpgme: refactor Rust interface, add tests
|
||||
- 41e1fdd5 Fix cargo-derivefmt lints
|
||||
- a44486d9 imap: fix minor clippy lint
|
||||
- 0c0f8210 Add a "move to Trash" shortcut
|
||||
- d20a9d0a Fix new clippy lints
|
||||
- e9a72072 Remove unused/obsolete plugins code and mentions
|
||||
- 2ddd28ee main.rs: always send a JobFinished event to all components
|
||||
- 571ae390 pager.rs: don't set self dirty after filter selector in PR
|
||||
#488 "view: fix nested filter jobs never being completed"
|
||||
- 6bc0caf4 melib: remove redundant get_path_hash macro
|
||||
- fc3308e4 melib: Add Mail::as_mbox() method
|
||||
- b1f24cbe view/filters: forward events on child filters
|
||||
- 1b201bf6 Remove GlobMatch trait, replace usage with Fnmatch
|
||||
- 8af003ab Rename addressbook stuff to "contacts"
|
||||
- 2069b4da errors: impl From<xdg::BaseDirectoriesError>
|
||||
- 7dee32ae contacts: refactor Card to its own module
|
||||
- 6d0d9680 jmap: move EmailObject state to Store
|
||||
- 0c590bbc contact-editor: remove empty space in PR #495 "Add version
|
||||
migration support"
|
||||
- b2200ec3 Remove unused smtp tests in PR #501 "Apply patches from
|
||||
upstream debian package"
|
||||
- ae294945 remove unused module file
|
||||
- 3558db51 Move jobs and mailbox management Components together
|
||||
- 3a931035 command: move Composer actions under TabActions
|
||||
- 441fda56 terminal: move TextPresentation trait to melib
|
||||
- ee897942 lints: deny clippy::or_fun_call
|
||||
- 0d088962 lints: Address clippy::too_long_first_doc_paragraph
|
||||
- ecc9b482 Small repo cleanups
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- a83b4176 meli.1: small fixes
|
||||
- 72dea6f3 Manpage fixes
|
||||
- a55f65e1
|
||||
meli.conf.5: Fix wrong default value type in default_header_values
|
||||
- 57b45a9c docs/historical-manpages: add DEP5 copyright file
|
||||
- 00236b86 docs: add meli.conf.examples(5) WIP
|
||||
- b88dc441 Comment out svgfeature; no need to ship it in PR #482
|
||||
"milestone/0.8.8"
|
||||
- b048c95a BUILD.md: add instructions for Android build
|
||||
- 593ed22b pgp: perform gpgme's sign+encrypt manually in PR #494 "pgp:
|
||||
perform gpgme's sign+encrypt manually"
|
||||
- 50922d97 melib/README.md: update and fix feature table
|
||||
- b912aabc docs: add examples of file picker usage in PR #516 "docs:
|
||||
add examples of file picker usage"
|
||||
|
||||
Packaging
|
||||
=========
|
||||
|
||||
- b55edd47 debian: update meli.docs and add meli.manpages
|
||||
|
||||
Miscellaneous Tasks
|
||||
===================
|
||||
|
||||
- 1232e16a scripts/make_html_manual_page.py: don't prettify
|
||||
- 6d520605 Vendor vobject crate
|
||||
- b33433e4
|
||||
Don't create backends as Box<dyn MailBackend>, but as Box<Self>
|
||||
- 2001b4dd Make subscribed_mailboxes conf val optional
|
||||
- 6cfe4da0 Enable rusqlite feature "modern_sqlite" always
|
||||
- 707a129e Coalesce repeating TUI notification messages
|
||||
- f036f95e scripts: add generate_release_changelog_entry.sh
|
||||
|
||||
Continuous Integration
|
||||
======================
|
||||
|
||||
- 4684b601 CI: remove env vars from action names in PR #458 "Minor QoL
|
||||
fixes"
|
||||
- 7419b465 CI: unpin rust version after updating time dependency in PR
|
||||
#460 "Update time dep to fix 1.80.0 breakage"
|
||||
- 77da86eb CI: Update cargo-derivefmt version
|
||||
- 1b3f2732 CI: Move build.yaml actions to Makefile.build
|
||||
- 598a70f9 CI: move lints.yaml actions to Makefile.lint
|
||||
- 7e800a8f
|
||||
CI: move manifest_lints.yaml actions to Makefile.manifest-lints
|
||||
- 98652110 CI: prepend printf commands with @
|
||||
- ad79bf84 .gitea/Makefile.lint: attempt cargo-fmt with +nightly
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Tue, 19 Nov 2024 14:09:13 +0200
|
||||
meli (0.8.7-1) bookworm; urgency=low
|
||||
|
||||
Contributors in alphabetical order:
|
||||
|
||||
- Andrei Zisu
|
||||
- Damian Poddebniak
|
||||
- Herby Gillot
|
||||
- Manos Pitsidianakis
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- 9fcb0a04 Add cargo-deny configuration file deny.toml
|
||||
- 7e8d19af Add Envelope::sender_any
|
||||
- 9ab404c5 Add pgp signed attachment support
|
||||
- b4579075 Allow XOAUTH2 string passed as string
|
||||
- 0ffe7fa5 Add text/plain or text/html arg for text decoding
|
||||
- e107d613 Add prelude module for import cleanup
|
||||
- 7200589a Add ErrorKind::NotFound
|
||||
- 8c880dc7 Add {Error,ErrorKind}::is_recoverable()
|
||||
- eb27773b Add pager.named_filters setting
|
||||
- 84d93d65 Add support for ID extension (opt-in)
|
||||
- af6838c2 Add metadata field to MailBackendCapabilities
|
||||
- d1499242 Add From<Infallible> impl
|
||||
- 814af0e9 Add --gzipped flag to man subcommand
|
||||
- 475860c9 Accept - for stdio in `{create,test}_config`
|
||||
- 86f9b213 Add timeout conf field in validate()
|
||||
- dd525bd9 Use Error::is_recoverable
|
||||
- 6e1fea80 Show suggestions on Unauthorized error
|
||||
- 38620866 Detect DNS lookup std::io::Error
|
||||
- a330ff96 Retry on DNS failure
|
||||
- 2429f17b On invalid conf value, print what value is expected
|
||||
- 6379fbe8 Add support for Undercurl attribute
|
||||
- a13bf13f Add stub Undercurl support
|
||||
- f5f1e068 Add UIDPLUS support
|
||||
- afccebf3 Add AUTH=PLAIN support
|
||||
- 9fb5bc41 Impl AUTH=ANONYMOUS (RFC4505)
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- ff3fe077 Fix new 1.79.0 clippy lints
|
||||
- 430cbdfd Fix python errors
|
||||
- e3c1656e Fix LOGINDISABLED support
|
||||
- a82d1e1e Fix RowsState::rename_env stale data
|
||||
- 8dc4465c Fix toml value ser after update of toml dependency
|
||||
- 39e903b1 Fix issues with ShellExpandTrait
|
||||
- 608301dc Expand save-to paths asap
|
||||
- 100fa8b3 Fix edge case in ShellExpandTrait
|
||||
- a85b3a08 Allow default_mailbox to be any mailbox
|
||||
- 0dc24623 Fix one by off error on menu unread count
|
||||
- 073aef86 Fix lints/errors when compiling specific feature combos
|
||||
- 12695a00 Fix MSRV breakage
|
||||
- 27ac3061 Fix tag support not being printed
|
||||
- 97af00cd Respect timeout value from user configuration
|
||||
- 824de287 Fix make_address! use
|
||||
- f2e9cac3 Use suggested minimum for maxObjectsInGet
|
||||
- 41d07fbc NewState in EmailImportResponse cannot be null
|
||||
- 197132cc Support fetching with BODY[] for buggy servers
|
||||
- 91fdef98 Return NotFound on cache miss
|
||||
- 96cc02a0 Do not use ErrorKind::Configuration
|
||||
- e96e9789 Don't discard pre-auth capabilities
|
||||
- 122a2a4d Drain event_queue when mailbox made available
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- 27c4876f Prevent log flooding when drawing listing entries
|
||||
- 7bdc8f52 Highlight_self also when self is sender
|
||||
- c4f7b77a Rework attachment rendering logic with filters
|
||||
- 1cce8c11 Accept invalid "+" CRLF cont req
|
||||
- c04b593b Use BODY instead of RFC822
|
||||
- 084a222a Remove subscribed mailboxes list
|
||||
- 5b6c1aa8 Don't show all background jobs
|
||||
- f9a3b333 Return NotFound on empty FETCH
|
||||
- 15f3a3fb Retry fetch envelope only if err.is_recoverable()
|
||||
- 15eeac51 Enable dns_cache, tcp_keepalive & tcp_nodelay
|
||||
- 06437e60 Set not_yet_seen to 0 when inserting existing
|
||||
- 0b113cdb Use MELI_FEATURES in all cargo invocations
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 7856ea33 Transition more to imap-codec
|
||||
- 6f61176a Remove unecessary mut modifier
|
||||
- 3251e7bd Scrub skip_serializing_if from attributes
|
||||
- ebc1fa3b Move module to self dir
|
||||
- 5110813e Refactor MaildirOp and watch()
|
||||
- a9122c6e Draw with x range argument
|
||||
- 3ebf5510 Pass entire screen area when drawing overlay
|
||||
- 2dc1721a Move signal handling stuff to submodule
|
||||
- 738f7c46 Execute Opt subcommand in Opt::execute()
|
||||
- 46df4b57 Remove unused function stub
|
||||
- 52c75e92 Use HeaderName constants
|
||||
- 6da4e2ec Replace stringify! in Debug impls with type checked macro
|
||||
- 85a55ed6 Add some missing ErrorKinds to errors
|
||||
- 8b568f6e Add if_in_state argument in Set::new()
|
||||
- 1e2e3da0 Treat color input `; ;` as `; 0 ;`
|
||||
- 7c47f702 Extract test and parser modules to files
|
||||
- d40ee692 Extract tests mod from protocol_parser
|
||||
- 1e50911c Add utils module to protocol_parser
|
||||
- d3a45b34 Make default shared lib name a const
|
||||
- a9e9d952 Change termination_string arg to Option
|
||||
- fd76df78 Use MELI_CONFIG env var in mock tests
|
||||
- 8552e499 Replace std::mem::{replace,take}
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- dfc2bb43 Add link to MacPorts page for `meli`
|
||||
- 97aa6a8e Replace obsolete .Tn macro with .Em
|
||||
- a8e82a30 Add missing entries from JMAP
|
||||
|
||||
Miscellaneous Tasks
|
||||
===================
|
||||
|
||||
- bbe2cffa Add rust-bindgen's friends.sh to scripts/
|
||||
- a8956baf Update to `imap-codec` v2.0.0-alpha.1
|
||||
- c99633e1 Update futures dependency 0.3.28 -> 0.3.30
|
||||
- fe604bf0 Update "openssl" dependency to 0.10.64
|
||||
- 9daf9437 Add test_cli_subcommands.rs
|
||||
- 9f783d9a Pin assert_cmd ver to 2.0.13
|
||||
- b7da1d0f Check all targets in cargo-msrv verify test
|
||||
- 8a74920d Pin rust version to 1.79.0
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Tue, 30 Jul 2024 14:21:31 +0300
|
||||
|
||||
meli (0.8.6-1) bookworm; urgency=low
|
||||
|
||||
Contributors in alphabetical order:
|
||||
|
||||
- euxane
|
||||
- Manos Pitsidianakis
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- 735b44f2 Add 'highlight_self' theme attribute
|
||||
- e187bb3f Add tools subcommand with smtp shell for debugging
|
||||
- 571bd984 Add proper imap-shell in tools subcommand for debugging
|
||||
- 0e1e5b9e Add support for Alternate Scroll Mode (xterm)
|
||||
- fe08d52a Add force_text_emoji_presentation option
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- 3de4908d man.7 Fix typo for toggle_expand_headers
|
||||
- a8c7582f Fix ENVELOPE parsing in untagged responses
|
||||
- c65635ef Fix compilation for macos
|
||||
- 06ec2790 Fix str slice index panic
|
||||
- f2b59a76 Add RequestUrlTemplate type
|
||||
- 7eed944a Fix screwed up rfc8620 module split
|
||||
- 74a3539f Fix degenerate OOB cell access
|
||||
- e8e76970 Fix edge case with strings/linebreaking
|
||||
- 81955187 Fix decryption error not shown
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- a9c3b151 Impl highlight_self in all index styles
|
||||
- 57e3e643 Remove excessive right padding in flags
|
||||
- a4ebe3b7 Add ErrorKind::Platform
|
||||
- 4bdfb3a3 Disable Nagle's algorithm by default
|
||||
- 4148aee5 Refactor smtp,draft errors and email tests
|
||||
- ed5a6b04 Add a symbols range to is_emoji check
|
||||
- fc1122a2 Rename to backend_mailbox.rs
|
||||
- 50ecade7 Merge rfc8620/tests.rs to tests.rs
|
||||
- a78f3f26 Move submodules to jmap/
|
||||
- f7838b1d Split to methods.rs and objects.rs
|
||||
- 74f0d12a Remove obsolete imapshell.rs and smtp_conn.rs
|
||||
- dce3852f Add capabilities module
|
||||
- 7ba7dc70 Imports cleanup in all modules
|
||||
- 45bfcf87 Minor refactors
|
||||
- 77867aee Unwrap object module
|
||||
- 33999fc6 Re-add Submission to USING
|
||||
- 6be25ac3 Don't use client field for get/posts
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- 4722d7cc Also mention server_password_command for jmap
|
||||
|
||||
Miscellaneous Tasks
|
||||
===================
|
||||
|
||||
- 2bfe6086 Hide self from "add contacts" options
|
||||
- 9ca34a68 Update MSRV to 1.70.0
|
||||
- 50ff16c4 Add LIGHT, DARK constant theme keys
|
||||
- 1abce964 Add Envelope::recipient_any method
|
||||
- 671d35e2 Update mailin-embedded dependency to 0.8.2
|
||||
- 39fbb164 Change info_message_{next,prev} shortcuts to '<, >'
|
||||
- 58d73271 Change new mail text content
|
||||
- f0d1b9cf Add ayllu mirror link
|
||||
- 3bab5324 Improve Debug impl for ContentType etc
|
||||
- e9dd6bec Comment out content
|
||||
- 8dd87c1a Add ContentType::is_text_plain()
|
||||
- 01bc62e0 Add new_plaintext method
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Sat, 08 Jun 2024 11:47:40 +0300
|
||||
|
||||
meli (0.8.5-1) bookworm; urgency=low
|
||||
|
||||
Contributors in alphabetical order:
|
||||
|
|
8
debian/meli.docs
vendored
8
debian/meli.docs
vendored
|
@ -1,4 +1,4 @@
|
|||
meli/docs/*.1
|
||||
meli/docs/*.5
|
||||
meli/docs/*.7
|
||||
meli/docs/external-tools.md
|
||||
meli/docs/meli.1
|
||||
meli/docs/meli.7
|
||||
meli/docs/meli.conf.5
|
||||
meli/docs/meli-themes.5
|
||||
|
|
5
debian/meli.examples
vendored
5
debian/meli.examples
vendored
|
@ -1,3 +1,2 @@
|
|||
contrib/*
|
||||
meli/docs/mail.vim
|
||||
meli/docs/samples/*
|
||||
meli/docs/samples/sample-config.toml
|
||||
meli/docs/samples/themes
|
||||
|
|
3
debian/meli.manpages
vendored
3
debian/meli.manpages
vendored
|
@ -1,3 +0,0 @@
|
|||
meli/docs/*.1
|
||||
meli/docs/*.5
|
||||
meli/docs/*.7
|
4
debian/patches/fix-prefix-for-debian.patch
vendored
4
debian/patches/fix-prefix-for-debian.patch
vendored
|
@ -1,6 +1,6 @@
|
|||
Description: Fix PREFIX env var in Makefile for use in Debian
|
||||
Author: Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
Last-Update: 2024-11-19
|
||||
Author: Manos Pitsidianakis <epilys@nessuent.xyz>
|
||||
Last-Update: 2023-03-06
|
||||
Index: meli/Makefile
|
||||
===================================================================
|
||||
--- meli.orig/Makefile
|
||||
|
|
23
debian/patches/usr_bin_editor.patch
vendored
23
debian/patches/usr_bin_editor.patch
vendored
|
@ -1,11 +1,16 @@
|
|||
Description: If EDITOR or VISUAL is not set, fall back to /usr/bin/editor, which is set by update-alternatives.
|
||||
Author: Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
Last-Update: 2024-11-19
|
||||
Index: meli/meli/src/subcommands.rs
|
||||
===================================================================
|
||||
--- meli.orig/meli/src/subcommands.rs
|
||||
+++ meli/meli/src/subcommands.rs
|
||||
@@ -56,9 +56,7 @@
|
||||
From: Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
Date: Thu, 27 Feb 2014 16:06:15 +0100
|
||||
Subject: usr_bin_editor
|
||||
|
||||
If EDITOR or VISUAL is not set, fall back to /usr/bin/editor,
|
||||
which is set by update-alternatives.
|
||||
---
|
||||
meli/src/subcommands.rs | 1 +---
|
||||
1 file changed, 1 insertion(+), 3 deletions(-)
|
||||
|
||||
--- a/meli/src/subcommands.rs
|
||||
+++ b/meli/src/subcommands.rs
|
||||
@@ -52,9 +52,7 @@
|
||||
pub fn edit_config() -> Result<()> {
|
||||
let editor = std::env::var("EDITOR")
|
||||
.or_else(|_| std::env::var("VISUAL"))
|
||||
|
@ -13,6 +18,6 @@ Index: meli/meli/src/subcommands.rs
|
|||
- format!("Could not find any value in environment variables EDITOR and VISUAL. {err}")
|
||||
- })?;
|
||||
+ .unwrap_or_else(|_| "/usr/bin/editor".into());
|
||||
let config_path = conf::get_config_file()?;
|
||||
let config_path = crate::conf::get_config_file()?;
|
||||
|
||||
let mut cmd = Command::new(editor);
|
||||
|
|
1
debian/rules
vendored
1
debian/rules
vendored
|
@ -2,7 +2,6 @@
|
|||
# You must remove unused comment lines for the released package.
|
||||
export RUSTUP_HOME=${HOME}/.rustup
|
||||
export DH_VERBOSE = 1
|
||||
export NO_MAN
|
||||
#export DEB_BUILD_MAINT_OPTIONS = hardening=+all
|
||||
#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic
|
||||
#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
|
||||
|
|
191
deny.toml
191
deny.toml
|
@ -1,191 +0,0 @@
|
|||
# cargo-deny configuration
|
||||
|
||||
[graph]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
[output]
|
||||
feature-depth = 1
|
||||
|
||||
[advisories]
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2021-0145", reason = "Affects Windows, which we do not support officially." },
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
#"MIT",
|
||||
#"Apache-2.0",
|
||||
#"Apache-2.0 WITH LLVM-exception",
|
||||
]
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.8
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
#crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
github = [""]
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
gitlab = [""]
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
bitbucket = [""]
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "meli"
|
||||
version = "0.8.10"
|
||||
version = "0.8.5"
|
||||
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.70.0"
|
||||
license = "EUPL-1.2 OR GPL-3.0-or-later"
|
||||
rust-version = "1.68.2"
|
||||
license = "GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
description = "terminal e-mail client"
|
||||
homepage = "https://meli-email.org"
|
||||
|
@ -12,7 +12,6 @@ repository = "https://git.meli-email.org/meli/meli.git"
|
|||
keywords = ["mail", "mua", "maildir", "terminal", "imap"]
|
||||
categories = ["command-line-utilities", "email"]
|
||||
default-run = "meli"
|
||||
exclude = ["/docs/historical-manpages"]
|
||||
|
||||
[[bin]]
|
||||
name = "meli"
|
||||
|
@ -23,31 +22,34 @@ name = "meli"
|
|||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
async-task = { version = "^4.2.0" }
|
||||
async-task = "^4.2.0"
|
||||
bitflags = { version = "2.4", features = ["serde"] }
|
||||
crossbeam = { version = "^0.8" }
|
||||
flate2 = { version = "1", optional = true }
|
||||
futures = { version = "0.3.30", default-features = false, features = ["async-await", "executor", "std"] }
|
||||
indexmap = { version = "^2.3", default-features = false, features = ["serde", "std"] }
|
||||
itoa = { version = "1.0.11", default-features = false }
|
||||
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.10", default-features = false }
|
||||
melib = { path = "../melib", version = "0.8.10", features = [] }
|
||||
nix = { version = "0.29", default-features = false, features = ["signal", "poll", "term", "ioctl", "process"] }
|
||||
regex = { version = "1" }
|
||||
serde = { version = "1.0.71" }
|
||||
serde_derive = { version = "1.0.71" }
|
||||
serde_json = { version = "1.0" }
|
||||
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"
|
||||
signal-hook = { version = "^0.3", default-features = false, features = ["iterator"] }
|
||||
signal-hook-registry = { version = "1.2.0", default-features = false }
|
||||
smallvec = { version = "^1.5.0", features = ["serde"] }
|
||||
structopt = { version = "0.3.26", default-features = false }
|
||||
# svg_crate = { version = "^0.13", optional = true, package = "svg" }
|
||||
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.8", default-features = false, features = ["display","preserve_order","parse"] }
|
||||
xdg = { version = "2.1.0" }
|
||||
xdg = "2.1.0"
|
||||
|
||||
[dependencies.pcre2]
|
||||
# An [env] entry in .cargo/config.toml should force a static build instead of
|
||||
# looking for a system library.
|
||||
version = "0.2.3"
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "static"]
|
||||
|
@ -56,9 +58,10 @@ jmap = ["melib/jmap"]
|
|||
sqlite3 = ["melib/sqlite3"]
|
||||
smtp = ["melib/smtp"]
|
||||
smtp-trace = ["smtp", "melib/smtp-trace"]
|
||||
regexp = ["dep:pcre2"]
|
||||
dbus-notifications = ["dep:notify-rust"]
|
||||
cli-docs = ["dep:flate2"]
|
||||
# svgscreenshot = ["dep:svg_crate"]
|
||||
svgscreenshot = ["dep:svg_crate"]
|
||||
gpgme = ["melib/gpgme"]
|
||||
# Static / vendoring features.
|
||||
tls-static = ["melib/tls-static"]
|
||||
|
@ -74,18 +77,15 @@ debug-tracing = ["melib/debug-tracing"]
|
|||
|
||||
[build-dependencies]
|
||||
flate2 = { version = "1", optional = true }
|
||||
proc-macro2 = { version = "1.0.37" }
|
||||
quote = { version = "^1.0" }
|
||||
regex = { version = "1" }
|
||||
proc-macro2 = "1.0.37"
|
||||
quote = "^1.0"
|
||||
regex = "1"
|
||||
syn = { version = "1", features = [] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = { version = "=2.0.13" }
|
||||
flate2 = { version = "1" }
|
||||
predicates = { version = "3" }
|
||||
regex = { version = "1" }
|
||||
rusty-fork = { version = "0.3.0" }
|
||||
tempfile = { version = "3.3" }
|
||||
regex = "1"
|
||||
tempfile = "3.3"
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
notify-rust = { version = "^4", default-features = false, features = ["dbus"], optional = true }
|
||||
|
|
|
@ -75,20 +75,10 @@ fn main() {
|
|||
|
||||
cl("docs/meli.1", "meli.txt.gz", false);
|
||||
cl("docs/meli.conf.5", "meli.conf.txt.gz", false);
|
||||
cl(
|
||||
"docs/meli.conf.examples.5",
|
||||
"meli.conf.examples.txt.gz",
|
||||
false,
|
||||
);
|
||||
cl("docs/meli-themes.5", "meli-themes.txt.gz", false);
|
||||
cl("docs/meli.7", "meli.7.txt.gz", false);
|
||||
cl("docs/meli.1", "meli.mdoc.gz", true);
|
||||
cl("docs/meli.conf.5", "meli.conf.mdoc.gz", true);
|
||||
cl(
|
||||
"docs/meli.conf.examples.5",
|
||||
"meli.conf.examples.mdoc.gz",
|
||||
true,
|
||||
);
|
||||
cl("docs/meli-themes.5", "meli-themes.mdoc.gz", true);
|
||||
cl("docs/meli.7", "meli.7.mdoc.gz", true);
|
||||
}
|
||||
|
|
|
@ -58,19 +58,14 @@ pub(crate) fn override_derive(filenames: &[(&str, &str)]) {
|
|||
|
||||
//! This module is automatically generated by `config_macros.rs`.
|
||||
|
||||
use super::*;
|
||||
use melib::HeaderName;
|
||||
|
||||
use indexmap::IndexSet;
|
||||
|
||||
use crate::conf::{*, data_types::*};
|
||||
|
||||
"##
|
||||
.to_string();
|
||||
|
||||
let cfg_attr_default_attr_regex = Regex::new(r"\s*default\s*[,]").unwrap();
|
||||
let cfg_attr_default_val_attr_regex = Regex::new(r#"\s*default\s*=\s*"[^"]*"\s*,\s*"#).unwrap();
|
||||
let cfg_attr_skip_ser_attr_regex =
|
||||
Regex::new(r#"\s*,?\s*skip_serializing_if\s*=\s*"[^"]*"\s*,?\s*"#).unwrap();
|
||||
let cfg_attr_feature_regex = Regex::new(r"[(](?:not[(]\s*)?feature").unwrap();
|
||||
|
||||
'file_loop: for (filename, ident) in filenames {
|
||||
|
@ -125,14 +120,6 @@ use crate::conf::{*, data_types::*};
|
|||
f.tokens.clone().into_iter().next().unwrap()
|
||||
{
|
||||
let mut attr_inner_value = f.tokens.to_string();
|
||||
if attr_inner_value.contains("skip_serializing_if") {
|
||||
attr_inner_value = cfg_attr_skip_ser_attr_regex
|
||||
.replace_all(&attr_inner_value, "")
|
||||
.to_string();
|
||||
let new_toks: proc_macro2::TokenStream =
|
||||
attr_inner_value.parse().unwrap();
|
||||
new_attr.tokens = quote! { #new_toks };
|
||||
}
|
||||
if cfg_attr_feature_regex.is_match(&attr_inner_value) {
|
||||
attr_inner_value = cfg_attr_default_val_attr_regex
|
||||
.replace_all(&attr_inner_value, "")
|
||||
|
@ -167,11 +154,6 @@ use crate::conf::{*, data_types::*};
|
|||
} else if attr_inner_value.starts_with("( default")
|
||||
|| attr_inner_value.starts_with("(default")
|
||||
{
|
||||
if attr_inner_value.ends_with("default)")
|
||||
|| attr_inner_value.ends_with("default )")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let rest = g.stream().into_iter().skip(2);
|
||||
new_attr.tokens = quote! { ( #(#rest)*) };
|
||||
match new_attr.tokens.to_string().as_str() {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Sending mail with a command line tool
|
||||
|
||||
`send_mail` can use either settings for an SMTP server or a shell
|
||||
`composing.send_mail` can use either settings for an SMTP server or a shell
|
||||
command to which it pipes new mail to.
|
||||
|
||||
### `msmtp` and `send_mail`
|
||||
|
@ -12,6 +12,7 @@ with many SMTP servers. It supports queuing and other small useful features.
|
|||
See [the documentation](https://marlam.de/msmtp/msmtp.html).
|
||||
|
||||
```toml
|
||||
[composing]
|
||||
send_mail = 'msmtp --logfile=/home/user/.mail/msmtp.log --read-recipients
|
||||
--read-envelope-from'
|
||||
```
|
||||
|
@ -119,32 +120,3 @@ The HTML of the e-mail is piped into `html_filter`'s standard input.
|
|||
If your account's syncing is handled by an external tool, you can use the
|
||||
refresh shortcuts within `meli` to call this tool with
|
||||
`accounts.refresh_command`.
|
||||
|
||||
## Viewing binary attachments such as images inside your terminal
|
||||
|
||||
If you have a specific terminal tool that lets you pipe binary data to it and
|
||||
it outputs command suitable for the terminal, you can use the `pipe-attachment`
|
||||
command to view/preview attachments without leaving `meli` or opening a GUI app.
|
||||
|
||||
This requires the output to be interactive otherwise `meli` will run the tool
|
||||
and immediately return, probably too quickly for you to notice the output in
|
||||
your terminal. A general solution is to pipe the output to an interactive pager
|
||||
like `less` which requires the user to exit it interactively.
|
||||
|
||||
The [`chafa`] tool can be used for images in this example:
|
||||
|
||||
Write a wrapper script that outputs the tool's output into a pager, for example
|
||||
`less`. If the output contains ANSI escape codes (i.e. colors, or bold/italic
|
||||
text) make sure to use `less -r` to preserve those codes.
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
/bin/chafa "$@" | less -r
|
||||
```
|
||||
|
||||
Save it somewhere as a file with executable permissions and you can use
|
||||
`pipe-attachment 1 /path/to/your/chafa/wrapper` to view the first attachment as
|
||||
an image with [`chafa`].
|
||||
|
||||
[`chafa`]: https://hpjansson.org/chafa/
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Comment: This directory contains various manual pages that may be of use to meli users.
|
||||
|
||||
Files: mailaddr.7.gz
|
||||
Comment: Sourced from debian manpages package.
|
||||
Copyright: Copyright (c) 1983, 1987 The Regents of the University of California.
|
||||
License: 6.5 (Berkeley) 2/14/89
|
||||
Redistribution and use in source and binary forms are permitted
|
||||
provided that the above copyright notice and this paragraph are
|
||||
duplicated in all such forms and that any documentation,
|
||||
advertising materials, and other materials related to such
|
||||
distribution and use acknowledge that the software was developed
|
||||
by the University of California, Berkeley. The name of the
|
||||
University may not be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
Files: maildir.5.en.gz
|
||||
Comment: Sourced from debian maildrop package.
|
||||
Copyright: Copyright 1998 - 2007 Double Precision, Inc.
|
||||
License: GPLv3 with OpenSSL linking extension
|
||||
This software is released under the GPL, version 3 (see COPYING.GPL).
|
||||
Additionally, compiling, linking, and/or using the OpenSSL toolkit in
|
||||
conjunction with this software is allowed.
|
||||
|
||||
Files: mbox.5.en.gz
|
||||
Comment: Sourced from debian mutt package.
|
||||
Copyright: Copyright (C) 2000 Thomas Roessler <roessler@does-not-exist.org>
|
||||
License: public-domain
|
||||
This document is in the public domain and may be distributed and
|
||||
changed arbitrarily.
|
||||
|
||||
Files: mbox.5qmail.en.gz
|
||||
Comment: Sourced from (now obsolete) debian qmail package.
|
||||
Copyright: D. J. Bernstein
|
||||
License: From http://cr.yp.to/qmail/dist.html
|
||||
I hereby place the qmail package (in particular, qmail-1.03.tar.gz,
|
||||
with MD5 checksum 622f65f982e380dbe86e6574f3abcb7c) into the public
|
||||
domain. You are free to modify the package, distribute modified
|
||||
versions, etc.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -128,8 +128,6 @@ Case-sensitive.
|
|||
.It
|
||||
"Underline"
|
||||
.It
|
||||
"Undercurl"
|
||||
.It
|
||||
"Blink"
|
||||
.It
|
||||
"Reverse"
|
||||
|
@ -154,7 +152,7 @@ name but with some modifications (for a full table see COLOR NAMES addendum) (Ca
|
|||
.El
|
||||
.Sh NO COLOR
|
||||
To completely disable
|
||||
.Em ANSI
|
||||
.Tn ANSI
|
||||
colors, there are two options:
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
|
@ -167,7 +165,7 @@ option (section
|
|||
The
|
||||
.Ev NO_COLOR
|
||||
environmental variable, when present (regardless of its value), prevents the addition of
|
||||
.Em ANSI
|
||||
.Tn ANSI
|
||||
color.
|
||||
When the configuration value
|
||||
.Ic use_color
|
||||
|
@ -178,7 +176,7 @@ is ignored.
|
|||
.Pp
|
||||
In this mode, cursor locations (i.e., currently selected entries/items) will use the
|
||||
.Ql reverse video
|
||||
.Em ANSI
|
||||
.Tn ANSI
|
||||
attribute to invert the terminal's default foreground/background colors.
|
||||
.Sh VALID KEYS
|
||||
.Bl -dash -compact
|
||||
|
|
131
meli/docs/meli.1
131
meli/docs/meli.1
|
@ -69,25 +69,9 @@ Start meli with given configuration file.
|
|||
Create configuration file in
|
||||
.Pa path
|
||||
if given, or at
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml Ns
|
||||
\&.
|
||||
If
|
||||
.Ar path
|
||||
is
|
||||
.Ar \-
|
||||
the result is printed to the standard output stream.
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.It Cm test-config Op Ar path
|
||||
Test a configuration for syntax issues or missing options.
|
||||
The configuration is read from
|
||||
.Pa path
|
||||
if given, or from
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml Ns
|
||||
\&.
|
||||
If
|
||||
.Ar path
|
||||
is
|
||||
.Ar \-
|
||||
the configuration is read from the standard input stream.
|
||||
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
|
||||
|
@ -214,30 +198,14 @@ See
|
|||
for the location of the mailcap files and
|
||||
.Xr mailcap 5
|
||||
for their syntax.
|
||||
You can save individual attachments with the following command:
|
||||
.Command save\-attachment Ar INDEX Ar path\-to\-file
|
||||
You can save individual attachments with the
|
||||
.Command save-attachment Ar INDEX Ar path-to-file
|
||||
command.
|
||||
.Ar INDEX
|
||||
is the attachment's index in the listing.
|
||||
.Bl -tag -compact -width 8n
|
||||
.It If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
|
||||
.It If the 0th index is provided, the entire message is saved.
|
||||
.It If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message\-id.
|
||||
.El
|
||||
.Pp
|
||||
You can pipe individual attachments to binaries with the following command:
|
||||
.Command pipe\-attachment Ar INDEX Ar binary Ar ARGS
|
||||
Example usage with the
|
||||
.Xr less 1
|
||||
pager:
|
||||
.D1 pipe\-attachment 0 less
|
||||
If the binary does not wait for your input before exiting, you will probably
|
||||
not see its output since you will return back to the user interface
|
||||
immediately.
|
||||
You can write a wrapper script that pipes your binary's output to
|
||||
.Dl less
|
||||
or
|
||||
.Dl less \-r
|
||||
if you want to preserve the ANSI escape codes in the pager's output.
|
||||
If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
|
||||
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.
|
||||
.Em IMAP
|
||||
|
@ -266,7 +234,7 @@ To enable sqlite3 indexing for an account set
|
|||
to
|
||||
.Em sqlite3
|
||||
in the configuration file and to create the sqlite3 index issue command:
|
||||
.Command reindex Ar ACCOUNT_NAME Ns
|
||||
.Command index Ar ACCOUNT_NAME Ns
|
||||
To search in the message body type your keywords without any special formatting.
|
||||
To search in specific fields, prepend your search keyword with "field:" like so:
|
||||
.Pp
|
||||
|
@ -303,12 +271,10 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable
|
|||
.HorizontalRule
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | message_id | in_reply_to | references | header | all_addresses | subject | flags | has_attachment | query \&"or\&" query | query \&"and\&" query | not query
|
||||
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | alladdresses | subject | flags | has_attachments | query \&"or\&" query | query \&"and\&" query | not query
|
||||
.It
|
||||
.Li not = \&"not\&" | \&"!\&"
|
||||
.It
|
||||
.Li has_attachment = \&"has:attachment\&" | \&"has:attachments\&"
|
||||
.It
|
||||
.Li quoted = ALPHA / SP *(ALPHA / DIGIT / SP)
|
||||
.It
|
||||
.Li term = ALPHA *(ALPHA / DIGIT) | DQUOTE quoted DQUOTE
|
||||
|
@ -319,8 +285,6 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable
|
|||
.It
|
||||
.Li flagterm = flagval | flagval \&",\&" flagterm
|
||||
.It
|
||||
.Li flags = \&"flag:\&" flag | \&"flags:\&" flag | \&"tag:\&" flag | \&"tags:\&" flag | \&"is:\&" flag
|
||||
.It
|
||||
.Li from = \&"from:\&" term
|
||||
.It
|
||||
.Li to = \&"to:\&" term
|
||||
|
@ -329,21 +293,11 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable
|
|||
.It
|
||||
.Li bcc = \&"bcc:\&" term
|
||||
.It
|
||||
.Li message_id = \&"message-id:\&" term | \&"msg-id:\&" term
|
||||
.It
|
||||
.Li in_reply_to = \&"in-reply-to:\&" term
|
||||
.It
|
||||
.Li references = \&"references:\&" term
|
||||
.It
|
||||
.Li header = \&"header:\&" field_name \&",\&" field_value
|
||||
.It
|
||||
.Li field_name = term
|
||||
.It
|
||||
.Li field_value = term
|
||||
.It
|
||||
.Li all_addresses = \&"all-addresses:\&" term
|
||||
.Li alladdresses = \&"alladdresses:\&" term
|
||||
.It
|
||||
.Li subject = \&"subject:\&" term
|
||||
.It
|
||||
.Li flags = \&"flags:\&" flag | \&"tags:\&" flag | \&"is:\&" flag
|
||||
.El
|
||||
.Sh FLAGS
|
||||
.Nm
|
||||
|
@ -466,7 +420,7 @@ supports three kinds of contact backends:
|
|||
.Bl -enum -compact
|
||||
.It
|
||||
an internal format that gets saved under
|
||||
.Pa $XDG_DATA_HOME/meli/account_name/contacts Ns
|
||||
.Pa $XDG_DATA_HOME/meli/account_name/addressbook Ns
|
||||
\&.
|
||||
.It
|
||||
vCard files (v3, v4) through the
|
||||
|
@ -521,9 +475,9 @@ threaded:shows threads as a tree structure
|
|||
plain:shows one row per mail, regardless of threading
|
||||
.TE
|
||||
.Bl -tag -width 36n
|
||||
.It Cm sort Oo Ar subject | date Oc Ar asc | desc
|
||||
.It Cm sort Ar subject | date \ Ar asc | desc
|
||||
sort mail listing
|
||||
.It Cm subsort Oo Ar subject | date Oc Ar asc | desc
|
||||
.It Cm subsort Ar subject | date \ Ar asc | desc
|
||||
sorts only the first level of replies.
|
||||
.It Cm go Ar n
|
||||
where
|
||||
|
@ -544,7 +498,7 @@ select threads matching
|
|||
query.
|
||||
.It Cm clear-selection
|
||||
Clear current selection.
|
||||
.It Cm set Ar seen | unseen
|
||||
.It Cm set seen, set unseen
|
||||
Set seen status of message.
|
||||
.It Cm import Ar FILEPATH Ar MAILBOX_PATH
|
||||
Import mail from file into given mailbox.
|
||||
|
@ -553,7 +507,7 @@ Copy or move to other mailbox.
|
|||
.It Cm copyto, moveto Ar ACCOUNT Ar MAILBOX_PATH
|
||||
Copy or move to another account's mailbox.
|
||||
.It Cm delete
|
||||
Delete selected entries.
|
||||
Delete selected threads.
|
||||
.It Cm export-mbox Ar FILEPATH
|
||||
Export selected threads to mboxcl2 file.
|
||||
.It Cm create\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
|
@ -572,19 +526,10 @@ This action is irreversible.
|
|||
.Ss Mail view commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm pipe Ar EXECUTABLE Oo Ar ARGS Oc
|
||||
.It Cm pipe Ar EXECUTABLE Ar ARGS
|
||||
pipe pager contents to binary
|
||||
.It Cm filter Ar EXECUTABLE Oo Ar ARGS Oc
|
||||
.It Cm filter Ar EXECUTABLE Ar ARGS
|
||||
filter and display pager contents through command
|
||||
.It Cm filter
|
||||
select a filter from
|
||||
.Ic pager.named_filters
|
||||
configuration value
|
||||
.Po
|
||||
See
|
||||
.Xr meli.conf 5 PAGER
|
||||
for its syntax
|
||||
.Pc
|
||||
.It Cm list-post
|
||||
post in list of viewed envelope
|
||||
.It Cm list-unsubscribe
|
||||
|
@ -598,7 +543,7 @@ open list archive with
|
|||
.Bl -tag -width 36n
|
||||
.It Cm mailto Ar MAILTO_ADDRESS
|
||||
Opens a composer tab with initial values parsed from the
|
||||
.Li mailto :
|
||||
.Li mailto:
|
||||
address.
|
||||
.It Cm add-attachment Ar PATH
|
||||
in composer, add
|
||||
|
@ -617,11 +562,7 @@ in
|
|||
Launch command
|
||||
.Ar CMD Ar ARGS Ns
|
||||
\&.
|
||||
The command should print file paths in stdout, separated by NUL bytes.
|
||||
Example usage with
|
||||
.Xr fzf 1 Ns
|
||||
:
|
||||
.D1 add-attachment-file-picker < fzf --print0
|
||||
The command should print file paths in stderr, separated by NULL bytes.
|
||||
.It Cm remove-attachment Ar INDEX
|
||||
remove attachment with given index
|
||||
.It Cm toggle sign
|
||||
|
@ -669,14 +610,14 @@ catchall for general errors
|
|||
process panic
|
||||
.El
|
||||
.Sh ENVIRONMENT
|
||||
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
|
||||
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
|
||||
.It Ev EDITOR
|
||||
Specifies the editor to use
|
||||
.It Ev MELI_CONFIG
|
||||
Override the configuration file
|
||||
.It Ev NO_COLOR
|
||||
When defined (regardless of its value), prevents the addition of
|
||||
.Em ANSI
|
||||
.Tn ANSI
|
||||
color.
|
||||
The configuration value
|
||||
.Ic use_color
|
||||
|
@ -685,7 +626,7 @@ overrides this.
|
|||
.Sh FILES
|
||||
.Nm
|
||||
uses the following parts of the XDG standard:
|
||||
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
|
||||
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
|
||||
.It Ev XDG_CONFIG_HOME
|
||||
defaults to
|
||||
.Pa ~/.config/
|
||||
|
@ -695,13 +636,17 @@ defaults to
|
|||
.El
|
||||
.Pp
|
||||
and appropriates the following locations:
|
||||
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
|
||||
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
|
||||
.It Pa $XDG_CONFIG_HOME/meli/
|
||||
User configuration directory
|
||||
.It Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
User configuration file, see
|
||||
.Xr meli.conf 5
|
||||
for its syntax and values.
|
||||
.It Pa $XDG_CONFIG_HOME/meli/hooks/*
|
||||
Reserved for event hooks.
|
||||
.It Pa $XDG_CONFIG_HOME/meli/plugins/*
|
||||
Reserved for plugin files.
|
||||
.It Pa $XDG_CACHE_HOME/meli/*
|
||||
Internal cached data used by meli.
|
||||
.It Pa $XDG_DATA_HOME/meli/*
|
||||
|
@ -798,14 +743,6 @@ Mailcap entries are searched for in the following files, in this order:
|
|||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2971 IMAP4 ID extension
|
||||
.%I IETF
|
||||
.%D October 01, 2000
|
||||
.%A Tim Showalter
|
||||
.%U https://datatracker.ietf.org/doc/rfc2971/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3156 MIME Security with OpenPGP
|
||||
.%I IETF
|
||||
.%D August 01, 2001
|
||||
|
@ -857,14 +794,6 @@ Mailcap entries are searched for in the following files, in this order:
|
|||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC4505 Anonymous Simple Authentication and Security Layer (SASL) Mechanism
|
||||
.%I IETF
|
||||
.%D June 12, 2006
|
||||
.%A Kurt Zeilenga
|
||||
.%U https://datatracker.ietf.org/doc/rfc4505/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC4549 Synchronization Operations for Disconnected IMAP4 Clients
|
||||
.%I IETF
|
||||
.%D June 16, 2006
|
||||
|
|
|
@ -412,9 +412,9 @@ which shows more than one line per thread which can include multiple e\-mails wi
|
|||
.Sy Performing actions on entries and/or selections\&.
|
||||
.Pp
|
||||
Press
|
||||
.Shortcut V listing select_entry
|
||||
.Shortcut v listing select_entry
|
||||
to toggle the selection of a single entry.
|
||||
.Shortcut v listing select_motion
|
||||
.Qq select_entry
|
||||
can be prefixed by a number modifier and affixed by a scrolling motion (up or down) to select multiple entries.
|
||||
.Tg number-modifier
|
||||
Simple set operations can be performed on a selection with these shortcut modifiers:
|
||||
|
@ -586,7 +586,7 @@ Forward e\-mail:
|
|||
.Shortcut Ctrl\-f envelope_view forward
|
||||
.It
|
||||
Expand extra headers: (References and others)
|
||||
.Shortcut h envelope_view toggle_expand_headers
|
||||
.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
|
||||
|
@ -713,11 +713,7 @@ in
|
|||
Launch command
|
||||
.Ar CMD Ar ARGS Ns
|
||||
\&.
|
||||
The command should print file paths in stdout, separated by NUL bytes.
|
||||
Example usage with
|
||||
.Xr fzf Ns
|
||||
:
|
||||
.D1 add-attachment-file-picker < fzf --print0
|
||||
The command should print file paths in stderr, separated by NULL bytes.
|
||||
.It Cm remove\-attachment Ar INDEX
|
||||
remove attachment with given index
|
||||
.It Cm toggle sign
|
||||
|
|
|
@ -43,7 +43,7 @@ Rendered as:
|
|||
.Sm
|
||||
..
|
||||
.\".Dd November 11, 2022
|
||||
.Dd May 20, 2024
|
||||
.Dd March 10, 2024
|
||||
.Dt MELI.CONF 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
@ -67,18 +67,18 @@ terminal e-mail client
|
|||
Configuration for
|
||||
.Sy meli
|
||||
is written in
|
||||
.Em TOML
|
||||
.Tn TOML
|
||||
which has a few things to consider (quoting the specification):
|
||||
.sp
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Em TOML
|
||||
.Tn TOML
|
||||
is case sensitive.
|
||||
.It
|
||||
A
|
||||
.Em TOML
|
||||
.Tn TOML
|
||||
file must be a valid
|
||||
.Em UTF-8
|
||||
.Tn UTF-8
|
||||
encoded Unicode document.
|
||||
.It
|
||||
White-space means
|
||||
|
@ -99,13 +99,13 @@ or
|
|||
.El
|
||||
.sp
|
||||
Refer to
|
||||
.Em TOML
|
||||
.Tn TOML
|
||||
documentation for valid
|
||||
.Em TOML
|
||||
.Tn TOML
|
||||
syntax.
|
||||
.sp
|
||||
Though not part of
|
||||
.Em TOML
|
||||
.Tn TOML
|
||||
syntax,
|
||||
.Nm
|
||||
can have nested configuration files by using the following
|
||||
|
@ -293,11 +293,6 @@ Path of
|
|||
.Xr mutt 1
|
||||
compatible alias file in the option
|
||||
They are parsed and imported read-only.
|
||||
.It Ic notmuch_address_book_query Ar String
|
||||
.Pq Em optional
|
||||
Query passed to
|
||||
.Qq Li notmuch address
|
||||
to import contacts into meli. Contacts are parsed and imported read-only.
|
||||
.It Ic mailboxes Ar mailbox
|
||||
.Pq Em optional
|
||||
Configuration for each mailbox.
|
||||
|
@ -376,7 +371,7 @@ format = "notmuch"
|
|||
.\"
|
||||
.Ss IMAP only
|
||||
.HorizontalRule
|
||||
.Em IMAP
|
||||
.Tn IMAP
|
||||
specific options are:
|
||||
.Bl -tag -width 36n
|
||||
.It Ic server_hostname Ar String
|
||||
|
@ -405,80 +400,61 @@ is unspecified, it becomes false by default.
|
|||
.It Ic use_tls Ar boolean
|
||||
.Pq Em optional
|
||||
Connect with
|
||||
.Em TLS
|
||||
.Tn TLS
|
||||
.Po
|
||||
or upgrade from plain connection to
|
||||
.Em TLS
|
||||
.Tn TLS
|
||||
if
|
||||
.Em STARTTLS
|
||||
.Tn STARTTLS
|
||||
is set.
|
||||
.Pc
|
||||
.Pq Em true \" default value
|
||||
.It Ic danger_accept_invalid_certs Ar boolean
|
||||
.Pq Em optional
|
||||
Do not validate
|
||||
.Em TLS
|
||||
.Tn TLS
|
||||
certificates.
|
||||
.Pq Em false \" default value
|
||||
.It Ic offline_cache Ar boolean
|
||||
.Pq Em optional
|
||||
Keep mail headers in an
|
||||
.Sy sqlite3
|
||||
database.
|
||||
.Pq Em true \" default value
|
||||
.It Ic use_idle Ar boolean
|
||||
.Pq Em optional
|
||||
Use
|
||||
.Em IDLE
|
||||
.Tn IDLE
|
||||
extension.
|
||||
.Pq Em true \" default value
|
||||
.It Ic use_condstore Ar boolean
|
||||
.Pq Em optional
|
||||
Use
|
||||
.Em CONDSTORE
|
||||
.Tn CONDSTORE
|
||||
extension.
|
||||
.Pq Em true \" default value
|
||||
.It Ic use_deflate Ar boolean
|
||||
.Pq Em optional
|
||||
Use
|
||||
.Em COMPRESS=DEFLATE
|
||||
.Tn COMPRESS=DEFLATE
|
||||
extension
|
||||
.Po if built with
|
||||
.Em DEFLATE
|
||||
.Tn DEFLATE
|
||||
support
|
||||
.Pc
|
||||
.Pq Em true \" default value
|
||||
.It Ic use_oauth2 Ar boolean
|
||||
.Pq Em optional
|
||||
Use
|
||||
.Em OAUTH2
|
||||
.Tn OAUTH2
|
||||
authentication.
|
||||
Can only be used with
|
||||
.Ic server_password_command
|
||||
which should return a base64-encoded
|
||||
.Em OAUTH2
|
||||
.Tn OAUTH2
|
||||
token ready to be passed to
|
||||
.Em IMAP Ns
|
||||
.Tn IMAP Ns
|
||||
\&.
|
||||
For help on setup with
|
||||
.Em Gmail Ns
|
||||
.Tn Gmail Ns
|
||||
, see
|
||||
.Em Gmail
|
||||
.Tn Gmail
|
||||
section below.
|
||||
.Pq Em false \" default value
|
||||
.It Ic use_auth_anonymous Ar boolean
|
||||
.Pq Em optional
|
||||
Use
|
||||
.Em AUTH=ANONYMOUS
|
||||
extension for authentication.
|
||||
.Pq Em false \" default value
|
||||
.It Ic use_id Ar boolean
|
||||
.Pq Em optional
|
||||
Use
|
||||
.Em ID
|
||||
extension to retrieve server metadata, viewable at the account status page.
|
||||
Enabling this does not send any information to the server.
|
||||
.Pq Em false \" default value
|
||||
.It Ic timeout Ar integer
|
||||
.Pq Em optional
|
||||
Timeout to use for server connections in seconds.
|
||||
|
@ -489,27 +465,27 @@ seconds means there is no timeout.
|
|||
.El
|
||||
.Ss Gmail
|
||||
.HorizontalRule
|
||||
.Em Gmail
|
||||
.Tn Gmail
|
||||
has non-standard
|
||||
.Em IMAP
|
||||
.Tn IMAP
|
||||
behaviors that need to be worked around.
|
||||
.Ss Gmail - sending mail
|
||||
.HorizontalRule
|
||||
Option
|
||||
.Ic store_sent_mail
|
||||
should be disabled since
|
||||
.Em Gmail
|
||||
.Tn Gmail
|
||||
auto-saves sent mail by its own.
|
||||
.Ss Gmail OAUTH2
|
||||
.HorizontalRule
|
||||
To use
|
||||
.Em OAUTH2 Ns
|
||||
.Tn OAUTH2 Ns
|
||||
, you must go through a process to register your own private
|
||||
.Qq application
|
||||
with
|
||||
.Em Google
|
||||
.Tn Google
|
||||
that can use
|
||||
.Em OAUTH2
|
||||
.Tn OAUTH2
|
||||
tokens,
|
||||
and set the option
|
||||
.Ic use_oauth2
|
||||
|
@ -525,14 +501,14 @@ directory you can find a
|
|||
file named
|
||||
.Li oauth2.py
|
||||
to generate and request the appropriate data to perform
|
||||
.Em OAUTH2
|
||||
.Tn OAUTH2
|
||||
authentication.
|
||||
.sp
|
||||
Steps:
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
In
|
||||
.Em Google API Ns
|
||||
.Tn Google API Ns
|
||||
s, create a custom OAuth client ID and note down the Client ID and Client
|
||||
Secret.
|
||||
You may need to create a consent screen; follow the steps described in the
|
||||
|
@ -559,9 +535,9 @@ enter a command like this
|
|||
On startup,
|
||||
.Sy meli
|
||||
should evaluate this command which if successful must only return a
|
||||
.Em base64 Ns
|
||||
.Tn base64 Ns
|
||||
-encoded token ready to be passed to
|
||||
.Em IMAP.
|
||||
.Tn IMAP.
|
||||
.Pp
|
||||
Your account section should look like this:
|
||||
.Bd -literal
|
||||
|
@ -583,7 +559,7 @@ composing.send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type =
|
|||
.El
|
||||
.Ss JMAP only
|
||||
.HorizontalRule
|
||||
.Em JMAP
|
||||
.Tn JMAP
|
||||
specific options
|
||||
.Bl -tag -width 36n
|
||||
.It Ic server_url Ar String
|
||||
|
@ -595,37 +571,22 @@ example:
|
|||
Server username
|
||||
.It Ic server_password Ar String
|
||||
Server password
|
||||
.It Ic server_password_command Ar String
|
||||
.Pq Em optional
|
||||
Use instead of
|
||||
.Ic server_password
|
||||
.It Ic use_token Ar boolean
|
||||
Authenticate using the API Bearer token method.
|
||||
If enabled, the token value must be provided as the configured password.
|
||||
.Pq Em false \" default value
|
||||
.It Ic danger_accept_invalid_certs Ar boolean
|
||||
.Pq Em optional
|
||||
Do not validate
|
||||
.Em TLS
|
||||
.Tn TLS
|
||||
certificates.
|
||||
.Pq Em false \" default value
|
||||
.It Ic timeout Ar integer
|
||||
.Pq Em optional
|
||||
Timeout to use for server connections in seconds.
|
||||
A timeout of
|
||||
.Li 0
|
||||
seconds means there is no timeout.
|
||||
.Pq Em 16 \" default value
|
||||
.El
|
||||
.Ss mbox only
|
||||
.HorizontalRule
|
||||
.Em mbox
|
||||
.Tn mbox
|
||||
specific options:
|
||||
.Bl -tag -width 36n
|
||||
.It Ic prefer_mbox_type Ar String
|
||||
.Pq Em optional
|
||||
Prefer specific
|
||||
.Em mbox
|
||||
.Tn mbox
|
||||
format reader for each message.
|
||||
Default is
|
||||
.Qq Li mboxcl2
|
||||
|
@ -668,7 +629,7 @@ mailboxes."Python mailing list" = { path = "~/.mail/python.mbox", subscribe = tr
|
|||
.\"
|
||||
.Ss NNTP
|
||||
.HorizontalRule
|
||||
.Em NNTP
|
||||
.Tn NNTP
|
||||
specific options
|
||||
.Bl -tag -width 36n
|
||||
.It Ic server_hostname Ar String
|
||||
|
@ -685,7 +646,7 @@ require authentication in every case
|
|||
.It Ic use_tls Ar boolean
|
||||
.Pq Em optional
|
||||
Connect with
|
||||
.Em TLS Ns
|
||||
.Tn TLS Ns
|
||||
\&.
|
||||
.Pq Em false \" default value
|
||||
.It Ic server_port Ar number
|
||||
|
@ -695,7 +656,7 @@ The port to connect to
|
|||
.It Ic danger_accept_invalid_certs Ar boolean
|
||||
.Pq Em optional
|
||||
Do not validate
|
||||
.Em TLS
|
||||
.Tn TLS
|
||||
certificates.
|
||||
.Pq Em false \" default value
|
||||
.It Ic store_flags_locally Ar boolean
|
||||
|
@ -704,13 +665,6 @@ Store seen status locally in an
|
|||
.Sy sqlite3
|
||||
database.
|
||||
.Pq Em true \" default value
|
||||
.It Ic timeout Ar integer
|
||||
.Pq Em optional
|
||||
Timeout to use for server connections in seconds.
|
||||
A timeout of
|
||||
.Li 0
|
||||
seconds means there is no timeout.
|
||||
.Pq Em 16 \" default value
|
||||
.El
|
||||
.Pp
|
||||
You have to explicitly state the groups you want to see in the
|
||||
|
@ -729,7 +683,7 @@ Example:
|
|||
.\"
|
||||
.Pp
|
||||
To submit articles directly to the
|
||||
.Em NNTP
|
||||
.Tn NNTP
|
||||
server, you must set the special value
|
||||
.Em server_submission
|
||||
in the
|
||||
|
@ -751,7 +705,7 @@ composing.send_mail = "server_submission"
|
|||
.It Ic alias Ar String
|
||||
.Pq Em optional
|
||||
Show a different name for this mailbox in the
|
||||
.Em UI Ns
|
||||
.Tn UI Ns
|
||||
\&.
|
||||
.It Ic autoload Ar boolean
|
||||
.Pq Em optional
|
||||
|
@ -836,10 +790,10 @@ Example:
|
|||
.It Ic encoding Ar String
|
||||
.Pq Em optional
|
||||
Override the default
|
||||
.Em UTF-8
|
||||
.Tn UTF-8
|
||||
charset for the mailbox name.
|
||||
Useful only for
|
||||
.Em UTF-7
|
||||
.Tn UTF-7
|
||||
mailboxes.
|
||||
.Pq Em "utf7", "utf-7", "utf8", "utf-8" \" default value
|
||||
.El
|
||||
|
@ -860,7 +814,7 @@ exit code must be
|
|||
for success
|
||||
.Pc
|
||||
or settings for an
|
||||
.Em SMTP
|
||||
.Tn SMTP
|
||||
server connection.
|
||||
See section
|
||||
.Sx SMTP Connections
|
||||
|
@ -900,7 +854,7 @@ header in new drafts.
|
|||
.It Ic default_header_values Ar hash table String[String]
|
||||
.Pq Em optional
|
||||
Default header values used when creating a new draft.
|
||||
.Pq Em {} \" default value
|
||||
.Pq Em [] \" default value
|
||||
.It Ic wrap_header_preamble Ar Option<(String, String)>
|
||||
.Pq Em optional
|
||||
Wrap header pre-ample when editing a draft in an editor.
|
||||
|
@ -912,14 +866,14 @@ This can be useful when for example you're writing Markdown; you can set the
|
|||
value to
|
||||
.Em ["<!--",\ "-->"]
|
||||
which wraps the headers in an
|
||||
.Em HTML
|
||||
.Tn HTML
|
||||
comment.
|
||||
.Pq Em None \" default value
|
||||
.It Ic store_sent_mail Ar boolean
|
||||
.Pq Em optional
|
||||
Store sent mail after successful submission.
|
||||
This setting is meant to be disabled for non-standard behaviour in
|
||||
.Em Gmail Ns
|
||||
.Tn Gmail Ns
|
||||
, which auto-saves sent mail on its own.
|
||||
.Pq Em true \" default value
|
||||
.It Ic attribution_format_string Ar String
|
||||
|
@ -946,7 +900,7 @@ with the replied envelope's date.
|
|||
Whether the
|
||||
.Xr strftime 3
|
||||
call for the attribution string uses the
|
||||
.Em POSIX
|
||||
.Tn POSIX
|
||||
locale instead of the user's active locale.
|
||||
.Pq Em true \" default value
|
||||
.It Ic forward_as_attachment Ar boolean or "ask"
|
||||
|
@ -992,7 +946,7 @@ Example:
|
|||
.Bd -literal
|
||||
[composing]
|
||||
editor_cmd = '~/.local/bin/vim +/^$'
|
||||
embedded_pty = true
|
||||
embed = true
|
||||
custom_compose_hooks = [ { name ="spellcheck", command="aspell --mode email --dont-suggest --ignore-case list" }]
|
||||
.Ed
|
||||
.\"
|
||||
|
@ -1035,36 +989,6 @@ or draft body mention attachments but they are missing.
|
|||
.Ic empty-draft-warn
|
||||
— Warn if draft has no subject and no body.
|
||||
.El
|
||||
.It Ic signature_file Ar Path
|
||||
.Pq Em optional
|
||||
Plain text file with signature that will pre-populate an email draft.
|
||||
Signatures must be explicitly enabled to be used, otherwise this setting will be ignored.
|
||||
.Pq Em None \" default value
|
||||
.It Ic use_signature Ar bool
|
||||
Pre-populate email drafts with signature, if any.
|
||||
.Sy meli
|
||||
will lookup the signature value in this order:
|
||||
.Bl -enum -compact
|
||||
.It
|
||||
The
|
||||
.Ic signature_file
|
||||
setting.
|
||||
.It
|
||||
.Pa ${XDG_CONFIG_DIR}/meli/<account>/signature
|
||||
.It
|
||||
.Pa ${XDG_CONFIG_DIR}/meli/signature
|
||||
.It
|
||||
.Pa ${XDG_CONFIG_DIR}/signature
|
||||
.It
|
||||
.Pa ${HOME}/.signature
|
||||
.It
|
||||
No signature otherwise.
|
||||
.El
|
||||
.Pq Em false \" default value
|
||||
.It Ic signature_delimiter Ar String
|
||||
.Pq Em optional
|
||||
Signature delimiter, that is, text that will be prefixed to your signature to separate it from the email body.
|
||||
.Pq Ql \en\en\-\- \en
|
||||
.El
|
||||
.\"
|
||||
.\"
|
||||
|
@ -1226,10 +1150,10 @@ Open list entry. (catch-all setting)
|
|||
.Pq Em Enter \" default value
|
||||
.It Ic info_message_next
|
||||
Show next info message, if any.
|
||||
.Pq Em > \" default value
|
||||
.Pq Em M-> \" default value
|
||||
.It Ic info_message_previous
|
||||
Show previous info message, if any.
|
||||
.Pq Em < \" default value
|
||||
.Pq Em M-< \" default value
|
||||
.It Ic focus_in_text_field
|
||||
Focus on a text field.
|
||||
.Pq Em Enter \" default value
|
||||
|
@ -1285,9 +1209,6 @@ Manually request a mailbox refresh.
|
|||
.It Ic set_seen
|
||||
Set thread as seen.
|
||||
.Pq Em n \" default value
|
||||
.It Ic send_to_trash
|
||||
Send entry to trash folder.
|
||||
.Pq Em D \" default value
|
||||
.It Ic union_modifier
|
||||
Union modifier.
|
||||
.Pq Em C-u \" default value
|
||||
|
@ -1298,10 +1219,7 @@ Difference modifier.
|
|||
Intersection modifier.
|
||||
.Pq Em i \" default value
|
||||
.It Ic select_entry
|
||||
Select (or toggle) thread entry.
|
||||
.Pq Em V \" default value
|
||||
.It Ic select_motion
|
||||
Perform select motion with a movement.
|
||||
Select thread entry.
|
||||
.Pq Em v \" default value
|
||||
.It Ic increase_sidebar
|
||||
Increase sidebar width.
|
||||
|
@ -1336,25 +1254,18 @@ Open e-mail entry.
|
|||
.sp
|
||||
.Em pager
|
||||
.Bl -tag -width 36n
|
||||
.It Ic page_down
|
||||
Go to next page.
|
||||
.Pq Em PageDown \" default value
|
||||
.It Ic page_up
|
||||
Go to previous page.
|
||||
.Pq Em PageUp \" default value
|
||||
.It Ic scroll_down
|
||||
Scroll down pager.
|
||||
.Pq Em j \" default value
|
||||
.It Ic scroll_up
|
||||
Scroll up pager.
|
||||
.Pq Em k \" default value
|
||||
.It Ic select_filter
|
||||
Select content filter.
|
||||
.Pq Em f \" default value
|
||||
.Pp
|
||||
See also
|
||||
.Ic named_filters
|
||||
setting.
|
||||
.It Ic scroll_down
|
||||
Scroll down pager.
|
||||
.Pq Em j \" default value
|
||||
.It Ic page_up
|
||||
Go to previous pager page
|
||||
.Pq Em PageUp \" default value
|
||||
.It Ic page_down
|
||||
Go to next pager pag
|
||||
.Pq Em PageDown \" default value
|
||||
.El
|
||||
.sp
|
||||
.Em contact-list
|
||||
|
@ -1371,9 +1282,6 @@ Create new contact.
|
|||
.It Ic edit_contact
|
||||
Edit contact under cursor.
|
||||
.Pq Em e \" default value
|
||||
.It Ic export_contact
|
||||
Export contact under cursor to .vcf.
|
||||
.Pq Em E \" default value
|
||||
.It Ic delete_contact
|
||||
Delete contact under cursor.
|
||||
.Pq Em d \" default value
|
||||
|
@ -1562,19 +1470,7 @@ A command to open html files.
|
|||
.It Ic filter Ar String
|
||||
.Pq Em optional
|
||||
A command to pipe mail output through for viewing in pager.
|
||||
.It Ic named_filters Ar String[String]
|
||||
.Pq Em optional
|
||||
Named filter commands to use at will.
|
||||
.Pq Em empty \" default value
|
||||
.Pp
|
||||
Show menu with an empty
|
||||
.Cm filter
|
||||
command.
|
||||
Example:
|
||||
.Bd -literal
|
||||
[pager]
|
||||
named_filters = { diff = "pygmentize -l diff -f 256", par = "par 72" }
|
||||
.Ed
|
||||
.Pq Em none \" default value
|
||||
.It Ic format_flowed Ar boolean
|
||||
.Pq Em optional
|
||||
Respect format=flowed
|
||||
|
@ -1612,8 +1508,6 @@ The URL will be given as the first argument of the command.
|
|||
.Pq Em optional
|
||||
Extra headers to display, if present, in the default header preamble of the
|
||||
pager.
|
||||
.Pq Em empty \" default value
|
||||
.Pp
|
||||
This setting is useful especially when used per-folder or per-account.
|
||||
For example, if you use
|
||||
.Sy rss2email
|
||||
|
@ -1635,6 +1529,7 @@ INBOX = {}
|
|||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Pq Em empty \" default value
|
||||
.El
|
||||
.\"
|
||||
.\"
|
||||
|
@ -1754,15 +1649,6 @@ Show relative indices in listings to quickly help with jumping to them.
|
|||
.It Ic hide_sidebar_on_launch Ar boolean
|
||||
Start app with sidebar hidden.
|
||||
.Pq Em false \" default value
|
||||
.It Ic mail_view_divider Ar char
|
||||
Character to show in the divider space between mail view and listing.
|
||||
.Pq Em ' ' \" default value
|
||||
.It Ic thread_layout Ar "auto" | "vertical" | "horizontal"
|
||||
Force specific layout in thread view.
|
||||
The layout can also be toggled at runtime with the
|
||||
.Li listing.toggle_layout
|
||||
shortcut.
|
||||
.Pq Em "auto"
|
||||
.El
|
||||
.Ss Examples of sidebar mailbox tree customization
|
||||
.HorizontalRule
|
||||
|
@ -2023,14 +1909,9 @@ If true, box drawing will be done with ASCII characters.
|
|||
.It Ic use_color Ar boolean
|
||||
.Pq Em optional
|
||||
If false, no
|
||||
.Em ANSI
|
||||
.Tn ANSI
|
||||
colors are used.
|
||||
.Pq Em true \" default value
|
||||
.It Ic force_text_presentation Ar boolean
|
||||
.Pq Em optional
|
||||
If true, text presentations of color symbols and emoji will be enforced as much as possible.
|
||||
Might not work on all non-text symbols and is experimental.
|
||||
.Pq Em false \" default value
|
||||
.It Ic window_title Ar String
|
||||
.Pq Em optional
|
||||
Set window title in xterm compatible terminals An empty string means no window
|
||||
|
@ -2038,14 +1919,14 @@ title is set.
|
|||
.Pq Em "meli" \" default value
|
||||
.It Ic file_picker_command Ar String
|
||||
.Pq Em optional
|
||||
Set command that prints file paths in stdout, separated by NUL bytes.
|
||||
Set command that prints file paths in stderr, separated by NULL bytes.
|
||||
Used with
|
||||
.Ic add-attachment-file-picker
|
||||
when composing new mail.
|
||||
.Pq Em None \" default value
|
||||
.It Ic themes Ar hash table String[String[Attribute]]
|
||||
Define
|
||||
.Em UI
|
||||
.Tn UI
|
||||
themes.
|
||||
See
|
||||
.Xr meli-themes 5
|
||||
|
@ -2214,7 +2095,7 @@ server port
|
|||
address to set as sender in SMTP transactions
|
||||
.Pq Em none \" default value
|
||||
.It Ic auth Ar SmtpAuth
|
||||
.Em SMTP
|
||||
.Tn SMTP
|
||||
server authentication.
|
||||
See
|
||||
.Sx SmtpAuth
|
||||
|
@ -2230,7 +2111,7 @@ subsection
|
|||
.It Ic extensions Ar SmtpExtensions
|
||||
.Pq Em optional
|
||||
set support for
|
||||
.Em SMTP
|
||||
.Tn SMTP
|
||||
extensions if they are advertised by the server.
|
||||
.Po see \" default value
|
||||
.Sx SmtpExtensions
|
||||
|
@ -2260,7 +2141,7 @@ For type
|
|||
.Bl -tag -width 36n
|
||||
.It Ic token_command Ar String
|
||||
Command to evaluate that returns an
|
||||
.Em XOAUTH2
|
||||
.Tn XOAUTH2
|
||||
token.
|
||||
.It Ic require_auth Ar boolean
|
||||
.Pq Em optional
|
||||
|
@ -2289,7 +2170,7 @@ auth = { type = "none" }
|
|||
.Ed
|
||||
.sp
|
||||
For
|
||||
.Em Gmail
|
||||
.Tn Gmail
|
||||
(see
|
||||
.Sx Gmail OAUTH2
|
||||
for details on the authentication token command):
|
||||
|
@ -2333,9 +2214,9 @@ Default security type is
|
|||
.It Ic type Ar "none" | "auto" | "starttls" | "tls"
|
||||
.It Ic danger_accept_invalid_certs Ar boolean
|
||||
Accept invalid
|
||||
.Em SSL
|
||||
.Tn SSL
|
||||
/
|
||||
.Em TLS
|
||||
.Tn TLS
|
||||
certificates
|
||||
.Pq Em false \" default value
|
||||
.El
|
||||
|
@ -2365,7 +2246,7 @@ RFC3461
|
|||
.Sh STANDARDS
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Em TOML
|
||||
.Tn TOML
|
||||
Standard
|
||||
.Li v.0.5.0
|
||||
.Lk https://toml.io/en/v0.5.0
|
||||
|
|
|
@ -1,227 +0,0 @@
|
|||
.\" meli - meli.conf.examples.5
|
||||
.\"
|
||||
.\" Copyright 2024 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/>.
|
||||
.\"
|
||||
.\" SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
.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 November 22, 2024
|
||||
.Dt MELI.CONF.EXAMPLES 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli.conf examples
|
||||
.Nd Example configurations for various mail backends supported by the
|
||||
.Xr meli 1
|
||||
terminal e-mail client
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.\".Sh SYNOPSIS
|
||||
.\".Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.\".\"
|
||||
.\".\"
|
||||
.\".\"
|
||||
.\".\"
|
||||
.\".\"
|
||||
.\".Sh DESCRIPTION
|
||||
.Sh MAILDIR ACCOUNT
|
||||
An example configuration:
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Bd -literal
|
||||
[accounts.account-name]
|
||||
root_mailbox = "/path/to/root/folder"
|
||||
format = "Maildir"
|
||||
listing.index_style = "Compact"
|
||||
identity="email@example.com"
|
||||
display_name = "Name"
|
||||
send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
#send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
|
||||
# Set mailbox-specific settings
|
||||
[accounts.account-name.mailboxes]
|
||||
"INBOX" = { alias="Inbox" } #inline table
|
||||
"drafts" = { alias="Drafts" } #inline table
|
||||
[accounts.account-name.mailboxes."foobar-devel"] # or a regular table
|
||||
ignore = true # don't show notifications for this mailbox
|
||||
.Ed
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Sh MBOX ACCOUNT
|
||||
An example configuration:
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Bd -literal
|
||||
[accounts.account-name]
|
||||
root_mailbox = "/var/mail/username"
|
||||
format = "mbox"
|
||||
listing.index_style = "Compact"
|
||||
identity="username@hostname.local"
|
||||
composing.send_mail = '/bin/false'
|
||||
.Ed
|
||||
.Sh IMAP ACCOUNT
|
||||
.Bd -literal
|
||||
[accounts."account-name"]
|
||||
root_mailbox = "INBOX"
|
||||
format = "imap"
|
||||
server_hostname="mail.example.com"
|
||||
server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
|
||||
server_username="username@example.com"
|
||||
#server_port="993" # imaps
|
||||
server_port="143" # STARTTLS
|
||||
use_starttls=true #optional
|
||||
send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
display_name = "Name Name"
|
||||
identity = "username@example.com"
|
||||
## show only specific mailboxes:
|
||||
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
.Ed
|
||||
.Ss Gmail account example
|
||||
.Bd -literal
|
||||
[accounts."account-name"]
|
||||
root_mailbox = '[Gmail]'
|
||||
format = "imap"
|
||||
send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
server_hostname='imap.gmail.com'
|
||||
server_password="password"
|
||||
server_username="username@gmail.com"
|
||||
server_port="993"
|
||||
listing.index_style = "Conversations"
|
||||
identity = "username@gmail.com"
|
||||
display_name = "Name Name"
|
||||
# Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
|
||||
composing.store_sent_mail = false
|
||||
.Ed
|
||||
|
||||
.Sh JMAP ACCOUNT
|
||||
The
|
||||
.Ic server_url
|
||||
option must hold the address of the server's session endpoint.
|
||||
.Bd -literal
|
||||
[accounts."account-name"]
|
||||
root_mailbox = "INBOX"
|
||||
format = "jmap"
|
||||
send_mail = 'server_submission'
|
||||
server_url="http://localhost:8080"
|
||||
server_username="user@hostname.local"
|
||||
server_password="changeme"
|
||||
identity = "user@hostname.local"
|
||||
.Ed
|
||||
.Ss fastmail.com account example
|
||||
.Lk https://fastmail.com/ Fastmail
|
||||
uses the
|
||||
.Em Bearer token
|
||||
authentication mechanism, so the option
|
||||
.Ic use_token
|
||||
must be enabled:
|
||||
.Bd -literal
|
||||
[accounts."fastmail-jmap"]
|
||||
root_mailbox = "INBOX"
|
||||
format = "jmap"
|
||||
server_url="https://api.fastmail.com/jmap/session"
|
||||
server_username="user@fastmail.com"
|
||||
server_password="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
use_token=true
|
||||
identity = "My Name <user@fastmail.com>"
|
||||
send_mail = "server_submission"
|
||||
.Ed
|
||||
.Sh NOTMUCH ACCOUNT
|
||||
TODO
|
||||
.Sh NNTP / USENET ACCOUNT
|
||||
TODO
|
||||
.Sh SEE ALSO
|
||||
.Xr meli.conf 5 ,
|
||||
.Xr meli 1 ,
|
||||
.Xr meli-themes 5
|
||||
.Sh AUTHORS
|
||||
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.
|
||||
.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
|
||||
.\" [pager]
|
||||
.\" filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
|
||||
.\" html_filter = "w3m -I utf-8 -T text/html"
|
||||
|
||||
.\" [notifications]
|
||||
.\" script = "notify-send"
|
||||
|
||||
.\" [composing]
|
||||
.\" # required for sending e-mail
|
||||
.\" send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
.\" #send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
.\" editor_command = 'vim +/^$'
|
||||
|
||||
.\" [shortcuts]
|
||||
.\" [shortcuts.composing]
|
||||
.\" edit = 'e'
|
||||
|
||||
.\" [shortcuts.listing]
|
||||
.\" new_mail = 'm'
|
||||
.\" set_seen = 'n'
|
||||
|
||||
.\" [terminal]
|
||||
.\" theme = "light"
|
|
@ -11,12 +11,9 @@
|
|||
#[accounts.account-name]
|
||||
#root_mailbox = "/path/to/root/mailbox"
|
||||
#format = "Maildir"
|
||||
#send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
#listing.index_style = "Conversations" # or [plain, threaded, compact]
|
||||
#identity="email@example.com"
|
||||
#display_name = "Name"
|
||||
## Need to explicitly list mailboxes of interest:
|
||||
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
#
|
||||
## Set mailbox-specific settings
|
||||
|
@ -29,7 +26,6 @@
|
|||
#[accounts.mbox]
|
||||
#root_mailbox = "/var/mail/username"
|
||||
#format = "mbox"
|
||||
#send_mail = 'false'
|
||||
#listing.index_style = "Compact"
|
||||
#identity="username@hostname.local"
|
||||
#
|
||||
|
@ -37,7 +33,6 @@
|
|||
#[accounts."imap"]
|
||||
#root_mailbox = "INBOX"
|
||||
#format = "imap"
|
||||
#send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
#server_hostname="mail.example.com"
|
||||
#server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
|
||||
#server_username="username@example.com"
|
||||
|
@ -47,14 +42,15 @@
|
|||
#listing.index_style = "Conversations"
|
||||
#identity = "username@example.com"
|
||||
#display_name = "Name Name"
|
||||
### show only specific mailboxes, overriding the server's subscribed status.
|
||||
### match every mailbox:
|
||||
#subscribed_mailboxes = ["*" ]
|
||||
### match specific mailboxes:
|
||||
##subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
#
|
||||
## Setting up an account for an already existing notmuch database
|
||||
##[accounts.notmuch]
|
||||
##root_mailbox = "/path/to/folder" # where .notmuch/ directory is located
|
||||
##format = "notmuch"
|
||||
##send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
##listing.index_style = "conversations"
|
||||
##identity="username@example.com"
|
||||
##display_name = "Name Name"
|
||||
|
@ -68,7 +64,6 @@
|
|||
#[accounts."gmail"]
|
||||
#root_mailbox = '[Gmail]'
|
||||
#format = "imap"
|
||||
#send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
#server_hostname='imap.gmail.com'
|
||||
#server_password="password"
|
||||
#server_username="username@gmail.com"
|
||||
|
@ -76,18 +71,22 @@
|
|||
#listing.index_style = "Conversations"
|
||||
#identity = "username@gmail.com"
|
||||
#display_name = "Name Name"
|
||||
## Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
|
||||
### match every mailbox:
|
||||
#subscribed_mailboxes = ["*" ]
|
||||
#composing.send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
### Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
|
||||
#composing.store_sent_mail = false
|
||||
#
|
||||
##[accounts."jmap account"]
|
||||
##root_mailbox = "INBOX"
|
||||
##format = "jmap"
|
||||
##send_mail = 'server_submission'
|
||||
##server_url="http://localhost:8080"
|
||||
##server_username="user@hostname.local"
|
||||
##server_password="changeme"
|
||||
##listing.index_style = "Conversations"
|
||||
##identity = "user@hostname.local"
|
||||
##subscribed_mailboxes = ["*", ]
|
||||
##composing.send_mail = 'server_submission'
|
||||
#
|
||||
#[pager]
|
||||
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
|
||||
|
@ -129,8 +128,12 @@
|
|||
#page_down = "PageDown"
|
||||
#
|
||||
#[composing]
|
||||
##required for sending e-mail
|
||||
#send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
#editor_command = 'vim +/^$' # optional, by default $EDITOR is used.
|
||||
#
|
||||
#
|
||||
#[pgp]
|
||||
#auto_sign = false # always sign sent messages
|
||||
#auto_verify_signatures = true # always verify signatures when reading signed e-mails
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
[terminal.themes.ibm-modern]
|
||||
"theme_default" = { fg = "$Black100", bg = "$White0", attrs = "Default" }
|
||||
"status.bar" = { fg = "$Black100", bg = "$Magenta40", attrs = "theme_default" }
|
||||
"status.notification" = { fg = "$Black100", bg = "$Magenta40", attrs = "theme_default" }
|
||||
"tab.focused" = { fg = "$White0", bg = "$Purple40", attrs = "theme_default" }
|
||||
"tab.unfocused" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"widgets.list.header" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
|
||||
"widgets.form.label" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
|
||||
"widgets.form.field" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"widgets.form.highlighted" = { fg = "theme_default", bg = "$Cyan30", attrs = "theme_default" }
|
||||
"widgets.options.highlighted" = { fg = "$Cyan10", bg = "$Teal30", attrs = "theme_default" }
|
||||
"mail.sidebar" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.sidebar_account_name" = { fg = "mail.sidebar", attrs = "Bold" }
|
||||
"mail.sidebar_unread_count" = { fg = "$Magenta40", bg = "$CoolGray10" }
|
||||
"mail.sidebar_index" = { fg = "theme_default", bg = "theme_default" }
|
||||
"mail.sidebar_highlighted" = { fg = "theme_default", bg = "$CoolGray10" }
|
||||
"mail.sidebar_highlighted_unread_count" = { from = "mail.sidebar_highlighted" }
|
||||
"mail.sidebar_highlighted_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account" = { fg = "mail.sidebar_highlighted", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_name" = { fg = "mail.sidebar_highlighted_account", bg = "mail.sidebar_highlighted_account", attrs = "Bold" }
|
||||
"mail.sidebar_highlighted_account_unread_count" = { fg = "mail.sidebar_unread_count", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
|
||||
"mail.listing.compact.even" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_unseen" = { fg = "$Black100", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_unseen" = { fg = "$Black100", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_selected" = { fg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_selected" = { fg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.conversations" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.from" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.date" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "Bold" }
|
||||
"mail.listing.conversations.selected" = { fg = "$CoolGray10", bg = "$CoolGray30", attrs = "theme_default" }
|
||||
"mail.listing.plain.even" = { fg = "mail.listing.compact.even", bg = "mail.listing.compact.even", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd" = { fg = "mail.listing.compact.odd", bg = "mail.listing.compact.odd", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_unseen" = { fg = "$Black100", bg = "$CoolGray30", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_unseen" = { fg = "$Black100", bg = "$CoolGray30", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.view.headers" = { fg = "$Black100", bg = "$Purple40", attrs = "theme_default" }
|
||||
"mail.view.headers_names" = { fg = "$Black100", bg = "$Magenta40", attrs = "mail.view.headers" }
|
||||
"mail.view.headers_area" = { fg = "theme_default", bg = "$Purple40", attrs = "theme_default" }
|
||||
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.a" = { fg = "theme_default", bg = "CornflowerBlue", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.b" = { fg = "theme_default", bg = "Red1", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.c" = { fg = "theme_default", bg = "Pink3", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.d" = { fg = "theme_default", bg = "Gold1", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.e" = { fg = "theme_default", bg = "Orange3", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.f" = { fg = "theme_default", bg = "CadetBlue", attrs = "theme_default" }
|
||||
"mail.listing.attachment_flag" = { fg = "$CoolGray30", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.thread_snooze_flag" = { fg = "$Magenta40", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.tag_default" = { fg = "$White0", bg = "$Black100", attrs = "Bold" }
|
||||
"pager.highlight_search" = { fg = "theme_default", bg = "$Teal30", attrs = "Bold" }
|
||||
"pager.highlight_search_current" = { fg = "$CoolGray10", bg = "$Teal30", attrs = "Bold" }
|
||||
|
||||
[terminal.themes.ibm-modern.color_aliases]
|
||||
"Blue60" = "#0f62fe"
|
||||
"Black100" = "#000000"
|
||||
"White0" = "#ffffff"
|
||||
"Cyan30" = "#82cfff"
|
||||
"Purple40" = "#be95ff"
|
||||
"Magenta40" = "#ff7eb6"
|
||||
"Teal30" = "#3ddbd9"
|
||||
"Cyan10" = "#e5f6ff"
|
||||
"CoolGray10" = "#f2f4f8"
|
||||
"CoolGray30" = "#c1c7cd"
|
1143
meli/src/accounts.rs
1143
meli/src/accounts.rs
File diff suppressed because it is too large
Load diff
|
@ -35,10 +35,10 @@ impl Account {
|
|||
mailbox_hash,
|
||||
flags.clone(),
|
||||
)?;
|
||||
let handle =
|
||||
self.main_loop_handler
|
||||
.job_executor
|
||||
.spawn("set-flags".into(), fut, self.is_async());
|
||||
let handle = self
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("set_flags".into(), fut);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
job_id,
|
||||
|
@ -53,14 +53,13 @@ impl Account {
|
|||
Ok(job_id)
|
||||
}
|
||||
|
||||
// #[cfg(not(feature = "sqlite3"))]
|
||||
// pub(super) fn update_cached_env(&mut self, _: Envelope, _:
|
||||
// Option<EnvelopeHash>) {}
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
pub(super) fn update_cached_env(&mut self, _: Envelope, _: Option<EnvelopeHash>) {}
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub(super) fn update_cached_env(&mut self, env: Envelope, old_hash: Option<EnvelopeHash>) {
|
||||
if self.settings.conf.search_backend == SearchBackend::Sqlite3 {
|
||||
let msg_id = env.message_id().to_string();
|
||||
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
|
||||
let msg_id = env.message_id_display().to_string();
|
||||
let name = self.name.clone();
|
||||
let backend = self.backend.clone();
|
||||
let fut = async move {
|
||||
|
@ -73,11 +72,10 @@ impl Account {
|
|||
crate::sqlite3::AccountCache::insert(env, backend, name).await?;
|
||||
Ok(())
|
||||
};
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"sqlite3::remove".into(),
|
||||
fut,
|
||||
crate::sqlite3::AccountCache::is_async(),
|
||||
);
|
||||
let handle = self
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("sqlite3::remove".into(), fut);
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
|
|
|
@ -26,88 +26,12 @@ use futures::stream::Stream;
|
|||
use melib::{backends::*, email::*, error::Result, LogLevel};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{is_variant, jobs::JoinHandle, StatusEvent};
|
||||
use crate::{is_variant, jobs::JoinHandle};
|
||||
|
||||
pub enum MailboxJobRequest {
|
||||
pub enum JobRequest {
|
||||
Mailboxes {
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
CreateMailbox {
|
||||
path: String,
|
||||
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
|
||||
},
|
||||
DeleteMailbox {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
RenameMailbox {
|
||||
mailbox_hash: MailboxHash,
|
||||
new_path: String,
|
||||
handle: JoinHandle<Result<Mailbox>>,
|
||||
},
|
||||
SetMailboxPermissions {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetMailboxSubscription {
|
||||
mailbox_hash: MailboxHash,
|
||||
new_value: bool,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MailboxJobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
|
||||
Self::DeleteMailbox { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
|
||||
}
|
||||
Self::RenameMailbox {
|
||||
mailbox_hash,
|
||||
new_path,
|
||||
..
|
||||
} => {
|
||||
write!(f, "JobRequest::RenameMailbox {mailbox_hash} to {new_path} ")
|
||||
}
|
||||
Self::SetMailboxPermissions { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxPermissions")
|
||||
}
|
||||
Self::SetMailboxSubscription { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxSubscription")
|
||||
}
|
||||
Self::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MailboxJobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Mailboxes { .. } => write!(f, "Get mailbox list"),
|
||||
Self::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
|
||||
Self::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
|
||||
Self::RenameMailbox { new_path, .. } => write!(f, "Rename mailbox to {new_path}"),
|
||||
Self::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
|
||||
Self::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxJobRequest {
|
||||
pub fn cancel(&self) -> Option<StatusEvent> {
|
||||
match self {
|
||||
Self::Mailboxes { handle } => handle.cancel(),
|
||||
Self::CreateMailbox { handle, .. } => handle.cancel(),
|
||||
Self::DeleteMailbox { handle, .. } => handle.cancel(),
|
||||
Self::RenameMailbox { handle, .. } => handle.cancel(),
|
||||
Self::SetMailboxPermissions { handle, .. } => handle.cancel(),
|
||||
Self::SetMailboxSubscription { handle, .. } => handle.cancel(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum JobRequest {
|
||||
Fetch {
|
||||
mailbox_hash: MailboxHash,
|
||||
#[allow(clippy::type_complexity)]
|
||||
|
@ -148,17 +72,69 @@ pub enum JobRequest {
|
|||
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<()>>,
|
||||
},
|
||||
Mailbox(MailboxJobRequest),
|
||||
}
|
||||
|
||||
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::Mailbox(inner) => std::fmt::Debug::fmt(inner, f),
|
||||
Self::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
|
||||
Self::Fetch { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::Fetch({})", mailbox_hash)
|
||||
}
|
||||
|
@ -177,6 +153,17 @@ impl std::fmt::Debug for JobRequest {
|
|||
.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 { .. } => {
|
||||
|
@ -190,7 +177,7 @@ 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::Mailbox(inner) => std::fmt::Display::fmt(inner, f),
|
||||
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"),
|
||||
|
@ -210,6 +197,11 @@ impl std::fmt::Display for JobRequest {
|
|||
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")
|
||||
|
@ -221,27 +213,10 @@ impl std::fmt::Display for JobRequest {
|
|||
impl JobRequest {
|
||||
is_variant! { is_watch, Watch { .. } }
|
||||
is_variant! { is_online, IsOnline { .. } }
|
||||
is_variant! { is_any_fetch, Fetch { .. } }
|
||||
|
||||
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
|
||||
matches!(self, Self::Fetch {
|
||||
mailbox_hash: h, ..
|
||||
} if *h == mailbox_hash)
|
||||
}
|
||||
|
||||
pub fn cancel(&self) -> Option<StatusEvent> {
|
||||
match self {
|
||||
Self::Generic { handle, .. } => handle.cancel(),
|
||||
Self::Mailbox(inner) => inner.cancel(),
|
||||
Self::Fetch { handle, .. } => handle.cancel(),
|
||||
Self::IsOnline { handle, .. } => handle.cancel(),
|
||||
Self::Refresh { handle, .. } => handle.cancel(),
|
||||
Self::SetFlags { handle, .. } => handle.cancel(),
|
||||
Self::SaveMessage { handle, .. } => handle.cancel(),
|
||||
Self::DeleteMessages { handle, .. } => handle.cancel(),
|
||||
Self::Watch { handle, .. } => handle.cancel(),
|
||||
Self::SendMessage => None,
|
||||
Self::SendMessageBackground { handle, .. } => handle.cancel(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -259,3 +259,89 @@ pub fn build_mailboxes_order(
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,439 +0,0 @@
|
|||
//
|
||||
// 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 melib::conf::ToggleFlag;
|
||||
|
||||
use super::*;
|
||||
use crate::command::actions::MailboxOperation;
|
||||
|
||||
impl Account {
|
||||
pub fn mailbox_operation(&mut self, op: MailboxOperation) -> Result<JobId> {
|
||||
if self.settings.account.read_only {
|
||||
return Err(Error::new("Account is read-only."));
|
||||
}
|
||||
match op {
|
||||
MailboxOperation::Create(path) => {
|
||||
let job = self
|
||||
.backend
|
||||
.write()
|
||||
.unwrap()
|
||||
.create_mailbox(path.to_string())?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"create_mailbox".into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::CreateMailbox { path, handle }),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::Delete(path) => {
|
||||
if self.mailbox_entries.len() == 1 {
|
||||
return Err(Error::new("Cannot delete only mailbox."));
|
||||
}
|
||||
|
||||
let mailbox_hash = self.mailbox_by_path(&path)?;
|
||||
let job = self.backend.write().unwrap().delete_mailbox(mailbox_hash)?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"delete-mailbox".into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::DeleteMailbox {
|
||||
mailbox_hash,
|
||||
handle,
|
||||
}),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::Subscribe(path) => {
|
||||
let mailbox_hash = self.mailbox_by_path(&path)?;
|
||||
let job = self
|
||||
.backend
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_mailbox_subscription(mailbox_hash, true)?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"subscribe-mailbox".into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
|
||||
mailbox_hash,
|
||||
new_value: true,
|
||||
handle,
|
||||
}),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::Unsubscribe(path) => {
|
||||
let mailbox_hash = self.mailbox_by_path(&path)?;
|
||||
let job = self
|
||||
.backend
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_mailbox_subscription(mailbox_hash, false)?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"unsubscribe-mailbox".into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
|
||||
mailbox_hash,
|
||||
new_value: false,
|
||||
handle,
|
||||
}),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::Rename(path, new_path) => {
|
||||
let mailbox_hash = self.mailbox_by_path(&path)?;
|
||||
let job = self
|
||||
.backend
|
||||
.write()
|
||||
.unwrap()
|
||||
.rename_mailbox(mailbox_hash, new_path.clone())?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
format!("rename-mailbox {path} to {new_path}").into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::RenameMailbox {
|
||||
handle,
|
||||
mailbox_hash,
|
||||
new_path,
|
||||
}),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::SetPermissions(_) => Err(Error::new("Not implemented.")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_mailbox_event(&mut self, job_id: JobId, mut job: MailboxJobRequest) {
|
||||
macro_rules! try_handle {
|
||||
($handle:ident, $binding:pat => $then:block) => {{
|
||||
try_handle! { $handle, Err(err) => {
|
||||
self.main_loop_handler
|
||||
.job_executor
|
||||
.set_job_success(job_id, false);
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification {
|
||||
title: None,
|
||||
body: format!("{}: {} failed", &self.name, job).into(),
|
||||
kind: Some(NotificationType::Error(err.kind)),
|
||||
source: Some(err),
|
||||
}));
|
||||
return;
|
||||
},
|
||||
$binding => $then
|
||||
}
|
||||
}};
|
||||
($handle:ident, Err($err:pat) => $then_err: block, $binding:pat => $then:block) => {{
|
||||
match $handle.chan.try_recv() {
|
||||
_err @ Ok(None) | _err @ Err(_) => {
|
||||
/* canceled */
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!(
|
||||
"handle.chan.try_recv() for job {} returned {:?}",
|
||||
job_id,
|
||||
_err
|
||||
);
|
||||
self.main_loop_handler
|
||||
.job_executor
|
||||
.set_job_success(job_id, false);
|
||||
}
|
||||
Ok(Some(Err($err))) => $then_err,
|
||||
Ok(Some(Ok($binding))) => $then,
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
match job {
|
||||
MailboxJobRequest::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.is_recoverable() {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::Notification {
|
||||
title: Some(self.name.to_string().into()),
|
||||
source: Some(err.clone()),
|
||||
body: err.to_string().into(),
|
||||
kind: Some(NotificationType::Error(err.kind)),
|
||||
},
|
||||
));
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
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);
|
||||
return;
|
||||
}
|
||||
let mailboxes_job = self.backend.read().unwrap().mailboxes();
|
||||
if let Ok(mailboxes_job) = mailboxes_job {
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"list-mailboxes".into(),
|
||||
mailboxes_job,
|
||||
self.is_async(),
|
||||
);
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::Mailboxes { handle }),
|
||||
);
|
||||
};
|
||||
} else {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::AccountStatusChange(
|
||||
self.hash,
|
||||
Some("Loaded mailboxes.".into()),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
MailboxJobRequest::CreateMailbox { ref mut handle, .. } => {
|
||||
try_handle! { handle, (mailbox_hash, mut mailboxes) => {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::MailboxCreate((self.hash, mailbox_hash)),
|
||||
));
|
||||
let mut new = FileMailboxConf::default();
|
||||
new.mailbox_conf.subscribe = ToggleFlag::InternalVal(true);
|
||||
new.mailbox_conf.usage = if mailboxes[&mailbox_hash].special_usage()
|
||||
!= SpecialUsageMailbox::Normal
|
||||
{
|
||||
Some(mailboxes[&mailbox_hash].special_usage())
|
||||
} else {
|
||||
let tmp = SpecialUsageMailbox::detect_usage(
|
||||
mailboxes[&mailbox_hash].name(),
|
||||
);
|
||||
if let Some(tmp) = tmp.filter(|&v| v != SpecialUsageMailbox::Normal)
|
||||
{
|
||||
mailboxes.entry(mailbox_hash).and_modify(|entry| {
|
||||
let _ = entry.set_special_usage(tmp);
|
||||
});
|
||||
}
|
||||
tmp
|
||||
};
|
||||
// if new mailbox has parent, we need to update its children field
|
||||
if let Some(parent_hash) = mailboxes[&mailbox_hash].parent() {
|
||||
self.mailbox_entries
|
||||
.entry(parent_hash)
|
||||
.and_modify(|parent| {
|
||||
parent.ref_mailbox =
|
||||
mailboxes.remove(&parent_hash).unwrap();
|
||||
});
|
||||
}
|
||||
let status = MailboxStatus::default();
|
||||
|
||||
self.mailbox_entries.insert(
|
||||
mailbox_hash,
|
||||
MailboxEntry::new(
|
||||
status,
|
||||
mailboxes[&mailbox_hash].path().to_string(),
|
||||
mailboxes.remove(&mailbox_hash).unwrap(),
|
||||
new,
|
||||
),
|
||||
);
|
||||
self.collection
|
||||
.threads
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(mailbox_hash, Threads::default());
|
||||
self.collection
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(mailbox_hash, Default::default());
|
||||
build_mailboxes_order(
|
||||
&mut self.tree,
|
||||
&self.mailbox_entries,
|
||||
&mut self.mailboxes_order,
|
||||
);
|
||||
}}
|
||||
}
|
||||
MailboxJobRequest::DeleteMailbox {
|
||||
mailbox_hash,
|
||||
ref mut handle,
|
||||
..
|
||||
} => {
|
||||
try_handle! { handle, mut mailboxes => {
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::MailboxDelete((
|
||||
self.hash,
|
||||
mailbox_hash,
|
||||
))));
|
||||
if let Some(pos) =
|
||||
self.mailboxes_order.iter().position(|&h| h == mailbox_hash)
|
||||
{
|
||||
self.mailboxes_order.remove(pos);
|
||||
}
|
||||
if let Some(pos) = self.tree.iter().position(|n| n.hash == mailbox_hash) {
|
||||
self.tree.remove(pos);
|
||||
}
|
||||
if self.settings.sent_mailbox == Some(mailbox_hash) {
|
||||
self.settings.sent_mailbox = None;
|
||||
}
|
||||
self.collection
|
||||
.threads
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&mailbox_hash);
|
||||
let deleted_mailbox =
|
||||
self.mailbox_entries.shift_remove(&mailbox_hash).unwrap();
|
||||
// if deleted mailbox had parent, we need to update its children field
|
||||
if let Some(parent_hash) = deleted_mailbox.ref_mailbox.parent() {
|
||||
self.mailbox_entries
|
||||
.entry(parent_hash)
|
||||
.and_modify(|parent| {
|
||||
parent.ref_mailbox = mailboxes.remove(&parent_hash).unwrap();
|
||||
});
|
||||
}
|
||||
self.collection
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&mailbox_hash);
|
||||
build_mailboxes_order(
|
||||
&mut self.tree,
|
||||
&self.mailbox_entries,
|
||||
&mut self.mailboxes_order,
|
||||
);
|
||||
// [ref:FIXME] remove from settings as well
|
||||
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("{}: mailbox deleted successfully", &self.name).into(),
|
||||
),
|
||||
source: None,
|
||||
body: "".into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
}));
|
||||
}}
|
||||
}
|
||||
MailboxJobRequest::RenameMailbox {
|
||||
ref mut handle,
|
||||
mailbox_hash,
|
||||
ref mut new_path,
|
||||
} => {
|
||||
use indexmap::map::MutableKeys;
|
||||
try_handle! { handle, mailbox => {
|
||||
let new_hash = mailbox.hash();
|
||||
if let Some((_, key, entry)) = self.mailbox_entries.get_full_mut2(&mailbox_hash) {
|
||||
*key = new_hash;
|
||||
*entry = MailboxEntry::new(entry.status.clone(), std::mem::take(new_path), mailbox, entry.conf.clone());
|
||||
}
|
||||
if let Some(key) = self.mailboxes_order.iter_mut().find(|k| **k == mailbox_hash) {
|
||||
*key = new_hash;
|
||||
}
|
||||
if let Some((_, key, _)) = self.event_queue.get_full_mut2(&mailbox_hash) {
|
||||
*key = new_hash;
|
||||
}
|
||||
{
|
||||
let mut threads = self.collection.threads.write().unwrap();
|
||||
if let Some(entry) = threads.remove(&mailbox_hash) {
|
||||
threads.insert(new_hash, entry);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut mailboxes = self.collection.mailboxes.write().unwrap();
|
||||
if let Some(entry) = mailboxes.remove(&mailbox_hash) {
|
||||
mailboxes.insert(new_hash, entry);
|
||||
}
|
||||
}
|
||||
build_mailboxes_order(
|
||||
&mut self.tree,
|
||||
&self.mailbox_entries,
|
||||
&mut self.mailboxes_order,
|
||||
);
|
||||
}}
|
||||
}
|
||||
MailboxJobRequest::SetMailboxPermissions { ref mut handle, .. } => {
|
||||
try_handle! { handle, _ => {
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("{}: mailbox permissions set successfully", &self.name)
|
||||
.into(),
|
||||
),
|
||||
source: None,
|
||||
body: "".into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
}));
|
||||
}}
|
||||
}
|
||||
MailboxJobRequest::SetMailboxSubscription {
|
||||
ref mut handle,
|
||||
ref mailbox_hash,
|
||||
ref new_value,
|
||||
} => {
|
||||
try_handle! { handle, () => {
|
||||
if self.mailbox_entries.contains_key(mailbox_hash) {
|
||||
self.mailbox_entries.entry(*mailbox_hash).and_modify(|m| {
|
||||
m.conf.mailbox_conf.subscribe = if *new_value {
|
||||
ToggleFlag::True
|
||||
} else {
|
||||
ToggleFlag::False
|
||||
};
|
||||
let _ = m.ref_mailbox.set_is_subscribed(*new_value);
|
||||
});
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!(
|
||||
"{}: `{}` has been {}subscribed.",
|
||||
&self.name,
|
||||
self.mailbox_entries[mailbox_hash].name(),
|
||||
if *new_value { "" } else { "un" }
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
source: None,
|
||||
body: "".into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
}));
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,565 +0,0 @@
|
|||
//
|
||||
// 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::path::PathBuf;
|
||||
|
||||
use melib::{
|
||||
backends::{prelude::*, Mailbox, MailboxHash},
|
||||
error::Result,
|
||||
maildir::MaildirType,
|
||||
smol, MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::{
|
||||
accounts::{AccountConf, FileMailboxConf, MailboxEntry, MailboxStatus},
|
||||
command::actions::MailboxOperation,
|
||||
utilities::tests::{eprint_step_fn, eprintln_ok_fn},
|
||||
};
|
||||
|
||||
#[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!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn new_maildir_backend(
|
||||
temp_dir: &TempDir,
|
||||
acc_name: &str,
|
||||
event_consumer: BackendEventConsumer,
|
||||
with_root_mailbox: bool,
|
||||
) -> Result<(PathBuf, AccountConf, Box<MaildirType>)> {
|
||||
let root_mailbox = temp_dir.path().join("inbox");
|
||||
{
|
||||
std::fs::create_dir(&root_mailbox).expect("Could not create root mailbox directory.");
|
||||
if with_root_mailbox {
|
||||
for d in &["cur", "new", "tmp"] {
|
||||
std::fs::create_dir(root_mailbox.join(d))
|
||||
.expect("Could not create root mailbox directory contents.");
|
||||
}
|
||||
}
|
||||
}
|
||||
let subscribed_mailboxes = if with_root_mailbox {
|
||||
vec!["inbox".into()]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let mailboxes = if with_root_mailbox {
|
||||
vec![(
|
||||
"inbox".into(),
|
||||
melib::conf::MailboxConf {
|
||||
extra: indexmap::indexmap! {
|
||||
"path".into() => root_mailbox.display().to_string(),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect()
|
||||
} else {
|
||||
indexmap::indexmap! {}
|
||||
};
|
||||
let extra = if with_root_mailbox {
|
||||
indexmap::indexmap! {
|
||||
"root_mailbox".into() => root_mailbox.display().to_string(),
|
||||
}
|
||||
} else {
|
||||
indexmap::indexmap! {}
|
||||
};
|
||||
|
||||
let account_conf = melib::AccountSettings {
|
||||
name: acc_name.to_string(),
|
||||
root_mailbox: root_mailbox.display().to_string(),
|
||||
format: "maildir".to_string(),
|
||||
identity: "user@localhost".to_string(),
|
||||
extra_identities: vec![],
|
||||
read_only: false,
|
||||
display_name: None,
|
||||
order: Default::default(),
|
||||
subscribed_mailboxes,
|
||||
mailboxes,
|
||||
manual_refresh: true,
|
||||
extra,
|
||||
};
|
||||
|
||||
let maildir = MaildirType::new(&account_conf, Default::default(), event_consumer)?;
|
||||
Ok((root_mailbox, account_conf.into(), maildir))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_accounts_mailbox_by_path_error_msg() {
|
||||
const ACCOUNT_NAME: &str = "test";
|
||||
|
||||
let eprintln_ok = eprintln_ok_fn();
|
||||
let mut eprint_step_closure = eprint_step_fn();
|
||||
macro_rules! eprint_step {
|
||||
($($arg:tt)+) => {{
|
||||
eprint_step_closure(format_args!($($arg)+));
|
||||
}};
|
||||
}
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
{
|
||||
eprint_step!(
|
||||
"Create maildir backend with a root mailbox, \"inbox\" which will be a valid maildir \
|
||||
folder because it will contain cur, new, tmp subdirectories..."
|
||||
);
|
||||
let mut ctx = crate::Context::new_mock(&temp_dir);
|
||||
let backend_event_queue = Arc::new(std::sync::Mutex::new(
|
||||
std::collections::VecDeque::with_capacity(16),
|
||||
));
|
||||
|
||||
let backend_event_consumer = {
|
||||
let backend_event_queue = Arc::clone(&backend_event_queue);
|
||||
|
||||
BackendEventConsumer::new(Arc::new(move |ah, be| {
|
||||
backend_event_queue.lock().unwrap().push_back((ah, be));
|
||||
}))
|
||||
};
|
||||
|
||||
let (root_mailbox, settings, maildir) =
|
||||
new_maildir_backend(&temp_dir, ACCOUNT_NAME, backend_event_consumer, true).unwrap();
|
||||
eprintln_ok();
|
||||
let name = maildir.account_name.to_string();
|
||||
let account_hash = maildir.account_hash;
|
||||
let backend = maildir as Box<dyn MailBackend>;
|
||||
let ref_mailboxes = smol::block_on(backend.mailboxes().unwrap()).unwrap();
|
||||
let contacts = melib::contacts::Contacts::new(name.to_string());
|
||||
|
||||
let mut account = super::Account {
|
||||
hash: account_hash,
|
||||
name: name.into(),
|
||||
is_online: super::IsOnline::True,
|
||||
mailbox_entries: Default::default(),
|
||||
mailboxes_order: Default::default(),
|
||||
tree: Default::default(),
|
||||
contacts,
|
||||
collection: backend.collection(),
|
||||
settings,
|
||||
main_loop_handler: ctx.main_loop_handler.clone(),
|
||||
active_jobs: HashMap::default(),
|
||||
active_job_instants: std::collections::BTreeMap::default(),
|
||||
event_queue: IndexMap::default(),
|
||||
backend_capabilities: backend.capabilities(),
|
||||
backend: Arc::new(std::sync::RwLock::new(backend)),
|
||||
};
|
||||
account.init(ref_mailboxes).unwrap();
|
||||
while let Ok(thread_event) = ctx.receiver.try_recv() {
|
||||
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
|
||||
if !account.process_event(&job_id) {
|
||||
assert!(
|
||||
ctx.accounts[0].process_event(&job_id),
|
||||
"unclaimed job id: {:?}",
|
||||
job_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprint_step!("Assert that mailbox_by_path(\"inbox\") returns the root mailbox...");
|
||||
account.mailbox_by_path("inbox").unwrap();
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"box\") returns an error mentioning the root mailbox..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("box").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"Some matching paths that were found: [\"inbox\"]. You can inspect the list \
|
||||
of mailbox paths of an account with the manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
|
||||
macro_rules! wait_for_job {
|
||||
($job_id:expr) => {{
|
||||
let wait_for = $job_id;
|
||||
while let Ok(thread_event) = ctx.receiver.recv() {
|
||||
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
|
||||
if !account.process_event(&job_id) {
|
||||
assert!(
|
||||
ctx.accounts[0].process_event(&job_id),
|
||||
"unclaimed job id: {:?}",
|
||||
job_id
|
||||
);
|
||||
} else if job_id == wait_for {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
eprint_step!(
|
||||
"Create new mailboxes: \"Sent\", \"Trash\", \"Drafts\", \"Archive\", \"Outbox\", \
|
||||
\"Archive/Archive (old)\"..."
|
||||
);
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Sent".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Trash".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Drafts".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Archive".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Outbox".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create(
|
||||
"inbox/Archive/Archive (old)".to_string(),
|
||||
))
|
||||
.unwrap());
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"rchive\") returns an error and mentions matching \
|
||||
archives with mailboxes with the least depth in the tree hierarchy of mailboxes \
|
||||
mentioned first..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("rchive").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"Some matching paths that were found: [\"inbox/Archive\", \
|
||||
\"inbox/Archive/Archive (old)\"]. You can inspect the list of mailbox paths \
|
||||
of an account with the manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
eprint_step!("Create \"inbox/Archive/Archive{{1,2,3,4,5,6,7,8,9,10}}\" mailboxes...");
|
||||
for i in 1..=10 {
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create(format!(
|
||||
"inbox/Archive/Archive{i}"
|
||||
)))
|
||||
.unwrap());
|
||||
}
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"inbox/Archive/Archive{{n}}\") works, i.e. we have to \
|
||||
specify the root prefix \"inbox\"..."
|
||||
);
|
||||
for i in 1..=10 {
|
||||
account
|
||||
.mailbox_by_path(&format!("inbox/Archive/Archive{i}"))
|
||||
.unwrap();
|
||||
account
|
||||
.mailbox_by_path(&format!("Archive/Archive{i}"))
|
||||
.unwrap_err();
|
||||
}
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"rchive\") returns and error and truncates the matching \
|
||||
mailbox paths to 5 maximum..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("rchive").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"Some matching paths that were found: [\"inbox/Archive\", \
|
||||
\"inbox/Archive/Archive1\", \"inbox/Archive/Archive2\", \
|
||||
\"inbox/Archive/Archive3\", \"inbox/Archive/Archive4\"] and 7 others. You \
|
||||
can inspect the list of mailbox paths of an account with the \
|
||||
manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"inbox/Archive\") returns a valid result (since the \
|
||||
root mailbox is a valid maildir folder)..."
|
||||
);
|
||||
account.mailbox_by_path("inbox/Archive").unwrap();
|
||||
eprintln_ok();
|
||||
|
||||
eprint_step!("Cleanup maildir account with valid root mailbox...");
|
||||
std::fs::remove_dir_all(root_mailbox).unwrap();
|
||||
eprintln_ok();
|
||||
}
|
||||
|
||||
{
|
||||
eprint_step!(
|
||||
"Create maildir backend with a root mailbox, \"inbox\" which will NOT be a valid \
|
||||
maildir folder because it will NOT contain cur, new, tmp subdirectories..."
|
||||
);
|
||||
let mut ctx = crate::Context::new_mock(&temp_dir);
|
||||
let backend_event_queue = Arc::new(std::sync::Mutex::new(
|
||||
std::collections::VecDeque::with_capacity(16),
|
||||
));
|
||||
|
||||
let backend_event_consumer = {
|
||||
let backend_event_queue = Arc::clone(&backend_event_queue);
|
||||
|
||||
BackendEventConsumer::new(Arc::new(move |ah, be| {
|
||||
backend_event_queue.lock().unwrap().push_back((ah, be));
|
||||
}))
|
||||
};
|
||||
|
||||
let (_root_mailbox, settings, maildir) =
|
||||
new_maildir_backend(&temp_dir, ACCOUNT_NAME, backend_event_consumer, false).unwrap();
|
||||
eprintln_ok();
|
||||
let name = maildir.account_name.to_string();
|
||||
let account_hash = maildir.account_hash;
|
||||
let backend = maildir as Box<dyn MailBackend>;
|
||||
let ref_mailboxes = smol::block_on(backend.mailboxes().unwrap()).unwrap();
|
||||
eprint_step!("Assert that created account has no mailboxes at all...");
|
||||
assert!(
|
||||
ref_mailboxes.is_empty(),
|
||||
"ref_mailboxes were not empty: {:?}",
|
||||
ref_mailboxes
|
||||
);
|
||||
eprintln_ok();
|
||||
let contacts = melib::contacts::Contacts::new(name.to_string());
|
||||
|
||||
let mut account = super::Account {
|
||||
hash: account_hash,
|
||||
name: name.into(),
|
||||
is_online: super::IsOnline::True,
|
||||
mailbox_entries: Default::default(),
|
||||
mailboxes_order: Default::default(),
|
||||
tree: Default::default(),
|
||||
contacts,
|
||||
collection: backend.collection(),
|
||||
settings,
|
||||
main_loop_handler: ctx.main_loop_handler.clone(),
|
||||
active_jobs: HashMap::default(),
|
||||
active_job_instants: std::collections::BTreeMap::default(),
|
||||
event_queue: IndexMap::default(),
|
||||
backend_capabilities: backend.capabilities(),
|
||||
backend: Arc::new(std::sync::RwLock::new(backend)),
|
||||
};
|
||||
account.init(ref_mailboxes).unwrap();
|
||||
while let Ok(thread_event) = ctx.receiver.try_recv() {
|
||||
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
|
||||
if !account.process_event(&job_id) {
|
||||
assert!(
|
||||
ctx.accounts[0].process_event(&job_id),
|
||||
"unclaimed job id: {:?}",
|
||||
job_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"inbox\") does not return a valid result (there are no \
|
||||
mailboxes)..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("inbox").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"You can inspect the list of mailbox paths of an account with the \
|
||||
manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Create multiple maildir folders \"inbox/Archive{{1,2,3,4,5,6,7,8,9,10}}\"..."
|
||||
);
|
||||
macro_rules! wait_for_job {
|
||||
($job_id:expr) => {{
|
||||
let wait_for = $job_id;
|
||||
while let Ok(thread_event) = ctx.receiver.recv() {
|
||||
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
|
||||
if !account.process_event(&job_id) {
|
||||
assert!(
|
||||
ctx.accounts[0].process_event(&job_id),
|
||||
"unclaimed job id: {:?}",
|
||||
job_id
|
||||
);
|
||||
} else if job_id == wait_for {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
for i in 1..=10 {
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create(format!("inbox/Archive{i}")))
|
||||
.unwrap());
|
||||
}
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"Archive{{n}}\") works, and that we don't have to \
|
||||
specify the root prefix \"inbox\"..."
|
||||
);
|
||||
for i in 1..=10 {
|
||||
account.mailbox_by_path(&format!("Archive{i}")).unwrap();
|
||||
}
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"rchive\") returns an error message with matches..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("rchive").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"Some matching paths that were found: [\"Archive1\", \"Archive2\", \
|
||||
\"Archive3\", \"Archive4\", \"Archive5\"] and 5 others. You can inspect the \
|
||||
list of mailbox paths of an account with the manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"inbox/Archive{{n}}\") does not return a valid result..."
|
||||
);
|
||||
assert_eq!(
|
||||
account
|
||||
.mailbox_by_path("inbox/Archive1")
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"You can inspect the list of mailbox paths of an account with the \
|
||||
manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
}
|
||||
}
|
198
meli/src/args.rs
198
meli/src/args.rs
|
@ -21,29 +21,10 @@
|
|||
|
||||
//! Command line arguments.
|
||||
|
||||
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
|
||||
|
||||
use super::*;
|
||||
#[cfg(feature = "cli-docs")]
|
||||
use crate::manpages;
|
||||
|
||||
fn try_path_or_stdio(input: &OsStr) -> PathOrStdio {
|
||||
if input.as_bytes() == b"-" {
|
||||
PathOrStdio::Stdio
|
||||
} else {
|
||||
PathOrStdio::Path(PathBuf::from(input))
|
||||
}
|
||||
}
|
||||
|
||||
/// `Pathbuf` or standard stream (`-` operand).
|
||||
#[derive(Debug)]
|
||||
pub enum PathOrStdio {
|
||||
/// Path
|
||||
Path(PathBuf),
|
||||
/// standard stream (`-` operand)
|
||||
Stdio,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "meli", about = "terminal mail client", version_short = "v")]
|
||||
pub struct Opt {
|
||||
|
@ -70,37 +51,31 @@ pub enum SubCommand {
|
|||
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`. Path `-` will output to standard
|
||||
/// output instead.
|
||||
/// `$XDG_CONFIG_HOME/meli/config.toml`
|
||||
#[structopt(display_order = 1)]
|
||||
CreateConfig {
|
||||
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str = try_path_or_stdio))]
|
||||
path: Option<PathOrStdio>,
|
||||
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
/// test a configuration file for syntax issues or missing options.
|
||||
/// If `PATH` is not specified, meli will try to read it from
|
||||
/// `$XDG_CONFIG_HOME/meli/config.toml`. Path `-` will read input from
|
||||
/// standard input instead.
|
||||
#[structopt(display_order = 2)]
|
||||
TestConfig {
|
||||
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str = try_path_or_stdio))]
|
||||
path: Option<PathOrStdio>,
|
||||
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str))]
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
#[structopt(display_order = 3)]
|
||||
/// Testing tools such as IMAP, SMTP shells for debugging.
|
||||
Tools(ToolOpt),
|
||||
#[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
|
||||
#[structopt(display_order = 4)]
|
||||
#[structopt(display_order = 3)]
|
||||
/// print documentation page and exit (Piping to a pager is recommended.).
|
||||
Man(ManOpt),
|
||||
#[structopt(display_order = 5)]
|
||||
|
||||
#[structopt(display_order = 4)]
|
||||
/// 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 = 6)]
|
||||
#[structopt(display_order = 5)]
|
||||
/// Print compile time feature flags of this binary
|
||||
CompiledWith,
|
||||
/// Print log file location.
|
||||
|
@ -114,156 +89,15 @@ pub enum SubCommand {
|
|||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct ManOpt {
|
||||
/// If set, output text in stdout instead of spawning `$PAGER`.
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[cfg_attr(feature = "cli-docs", structopt(long = "no-raw", alias = "no-raw"))]
|
||||
pub no_raw: bool,
|
||||
/// If set, output compressed gzip manpage in binary form in stdout.
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[cfg_attr(feature = "cli-docs", structopt(long = "gzipped"))]
|
||||
pub gzipped: bool,
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum ToolOpt {
|
||||
ImapShell {
|
||||
#[structopt(value_name = "CONFIG_TOML_ACCOUNT_NAME")]
|
||||
account: String,
|
||||
},
|
||||
#[cfg(feature = "smtp")]
|
||||
SmtpShell {
|
||||
#[structopt(value_name = "CONFIG_TOML_ACCOUNT_NAME")]
|
||||
account: String,
|
||||
},
|
||||
#[cfg(feature = "jmap")]
|
||||
JmapShell {
|
||||
#[structopt(value_name = "CONFIG_TOML_ACCOUNT_NAME")]
|
||||
account: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn print_path(path: &std::path::Path) {
|
||||
if let Some(hostname) = nix::unistd::gethostname()
|
||||
.ok()
|
||||
.and_then(|s| s.into_string().ok())
|
||||
{
|
||||
println!(
|
||||
"{}",
|
||||
Hyperlink::new(
|
||||
&path.display(),
|
||||
&format_args!("file://{hostname}{}", path.display())
|
||||
)
|
||||
);
|
||||
} else {
|
||||
println!("{}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
impl Opt {
|
||||
/// Execute `self.subcommand` if any, and return its result. Otherwise
|
||||
/// return `None`.
|
||||
pub fn execute(self) -> Option<Result<()>> {
|
||||
macro_rules! ret_err {
|
||||
($sth:expr) => {
|
||||
match $sth {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Some(Err(err.into())),
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(match self.subcommand? {
|
||||
SubCommand::View { .. } => { return None ; }
|
||||
SubCommand::TestConfig { path } => {
|
||||
subcommands::test_config(path)
|
||||
}
|
||||
SubCommand::Tools(toolopt) => {
|
||||
subcommands::tool(self.config, toolopt)
|
||||
}
|
||||
SubCommand::CreateConfig { path } => {
|
||||
subcommands::create_config(path)
|
||||
}
|
||||
SubCommand::EditConfig => {
|
||||
subcommands::edit_config()
|
||||
}
|
||||
SubCommand::PrintConfigPath => {
|
||||
let config_path = ret_err!(crate::conf::get_config_file());
|
||||
print_path(&config_path);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
SubCommand::Man(ManOpt {}) => {
|
||||
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")]
|
||||
SubCommand::Man(ManOpt {
|
||||
page,
|
||||
no_raw,
|
||||
gzipped: true,
|
||||
}) => {
|
||||
use std::io::Write;
|
||||
|
||||
ret_err!(std::io::stdout().write_all(if no_raw {
|
||||
page.text_gz()
|
||||
} else {
|
||||
page.mdoc_gz()
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "cli-docs")]
|
||||
SubCommand::Man(ManOpt {
|
||||
page,
|
||||
no_raw,
|
||||
gzipped: false,
|
||||
}) => {
|
||||
subcommands::man(page, false).and_then(|s| subcommands::pager(s, no_raw))
|
||||
}
|
||||
SubCommand::CompiledWith => {
|
||||
subcommands::compiled_with()
|
||||
}
|
||||
SubCommand::PrintLoadedThemes => {
|
||||
let s = ret_err!(conf::FileSettings::new());
|
||||
print!("{}", s.terminal.themes);
|
||||
Ok(())
|
||||
}
|
||||
SubCommand::PrintDefaultTheme => {
|
||||
print!("{}", conf::Themes::default().key_to_string("dark", false));
|
||||
Ok(())
|
||||
}
|
||||
SubCommand::PrintAppDirectories => {
|
||||
print_path(&xdg::BaseDirectories::with_prefix("meli")
|
||||
.expect(
|
||||
"Could not find your XDG directories. If this is unexpected, please \
|
||||
report it as a bug."
|
||||
)
|
||||
.get_data_file(""));
|
||||
let mut temp_dir = std::env::temp_dir();
|
||||
temp_dir.push("meli");
|
||||
print_path(&temp_dir);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
SubCommand::InstallMan {
|
||||
destination_path: _,
|
||||
} => {
|
||||
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")]
|
||||
SubCommand::InstallMan { destination_path } => {
|
||||
match crate::manpages::ManPages::install(destination_path) {
|
||||
Ok(p) => println!("Installed at {}.", p.display()),
|
||||
Err(err) => return Some(Err(err)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
SubCommand::PrintLogPath => {
|
||||
let settings = ret_err!(crate::conf::Settings::new());
|
||||
print_path(&settings._logger.log_dest());
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
/// 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>>,
|
||||
}
|
||||
|
|
|
@ -40,9 +40,6 @@ use melib::{
|
|||
SortField, SortOrder,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod actions;
|
||||
#[macro_use]
|
||||
pub mod error;
|
||||
|
@ -58,7 +55,7 @@ pub use crate::actions::{
|
|||
AccountAction::{self, *},
|
||||
Action::{self, *},
|
||||
ComposeAction::{self, *},
|
||||
ComposerTabAction, FlagAction,
|
||||
FlagAction,
|
||||
ListingAction::{self, *},
|
||||
MailingListAction::{self, *},
|
||||
TabAction::{self, *},
|
||||
|
@ -426,11 +423,6 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
tokens: &[One(Literal("save-draft"))],
|
||||
parser: parser::save_draft
|
||||
},
|
||||
{ tags: ["discard-draft"],
|
||||
desc: "discard draft",
|
||||
tokens: &[One(Literal("discard-draft"))],
|
||||
parser: parser::discard_draft
|
||||
},
|
||||
{ tags: ["toggle sign "],
|
||||
desc: "switch between sign/unsign for this draft",
|
||||
tokens: &[One(Literal("toggle")), One(Literal("sign"))],
|
||||
|
@ -556,10 +548,179 @@ pub fn command_completion_suggestions(input: &str) -> Vec<String> {
|
|||
}
|
||||
if let Some((s, Filepath)) = _m.last() {
|
||||
let p = std::path::Path::new(s);
|
||||
sugg.extend(p.complete(true, s.ends_with('/')).into_iter());
|
||||
sugg.extend(p.complete(true).into_iter());
|
||||
}
|
||||
}
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", input, s.as_str()))
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_command_parser() {
|
||||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
//print!("{}", $input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
// //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()
|
||||
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
|
||||
.collect::<HashSet<String>>()
|
||||
}};
|
||||
}
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()])
|
||||
.collect(),
|
||||
);
|
||||
input = "so".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort".to_string()]).collect(),
|
||||
);
|
||||
input = "so ".to_string();
|
||||
assert_eq!(&match_input!(input), &HashSet::default(),);
|
||||
input = "to".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
|
||||
);
|
||||
input = "toggle ".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter([
|
||||
"toggle mouse".to_string(),
|
||||
"toggle sign".to_string(),
|
||||
"toggle encrypt".to_string(),
|
||||
"toggle thread_snooze".to_string()
|
||||
])
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_parser_interactive() {
|
||||
use std::io;
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
input.clear();
|
||||
print!("> ");
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(_n) => {
|
||||
println!("Input is {:?}", input.as_str().trim());
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"suggestions = {:#?}",
|
||||
sugg.into_iter()
|
||||
.zip(vec.into_iter())
|
||||
.map(|(s, v)| format!(
|
||||
"{}{} {:?}",
|
||||
input.as_str().trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
},
|
||||
v
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
if input.trim() == "quit" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => println!("error: {}", error),
|
||||
}
|
||||
}
|
||||
println!("alright");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_parser_all() {
|
||||
use CommandError::*;
|
||||
|
||||
for cmd in [
|
||||
"set unseen",
|
||||
"set seen",
|
||||
"delete",
|
||||
"copyto somewhere",
|
||||
"moveto somewhere",
|
||||
"import fpath mpath",
|
||||
"close ",
|
||||
"go 5",
|
||||
] {
|
||||
parse_command(cmd.as_bytes()).unwrap_or_else(|err| panic!("{} failed {}", cmd, err));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
parse_command(b"setfafsfoo").unwrap_err().to_string(),
|
||||
Parsing {
|
||||
inner: "setfafsfoo".into(),
|
||||
kind: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"set foo").unwrap_err().to_string(),
|
||||
BadValue {
|
||||
inner: "foo".into(),
|
||||
suggestions: Some(&[
|
||||
"seen",
|
||||
"unseen",
|
||||
"plain",
|
||||
"threaded",
|
||||
"compact",
|
||||
"conversations"
|
||||
])
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"moveto ").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: false,
|
||||
takes: (1, Some(1)),
|
||||
given: 0,
|
||||
__func__: "moveto",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"reindex 1 2 3").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: true,
|
||||
takes: (1, Some(1)),
|
||||
given: 2,
|
||||
__func__: "reindex",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,25 +21,25 @@
|
|||
|
||||
//! User actions that need to be handled by the UI
|
||||
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use melib::{email::mailto::Mailto, Flag, SortField, SortOrder};
|
||||
|
||||
use crate::components::{Component, ComponentId};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum FlagAction {
|
||||
Set(Flag),
|
||||
Unset(Flag),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum TagAction {
|
||||
Add(String),
|
||||
Remove(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum ListingAction {
|
||||
SetPlain,
|
||||
SetThreaded,
|
||||
|
@ -49,7 +49,6 @@ pub enum ListingAction {
|
|||
Select(String),
|
||||
SetSeen,
|
||||
SetUnseen,
|
||||
SendToTrash,
|
||||
CopyTo(MailboxPath),
|
||||
CopyToOtherAccount(AccountName, MailboxPath),
|
||||
MoveTo(MailboxPath),
|
||||
|
@ -64,23 +63,8 @@ pub enum ListingAction {
|
|||
ToggleThreadSnooze,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ComposerTabAction {
|
||||
DiscardDraft,
|
||||
SaveDraft,
|
||||
#[cfg(feature = "gpgme")]
|
||||
ToggleSign,
|
||||
#[cfg(feature = "gpgme")]
|
||||
ToggleEncrypt,
|
||||
AddAttachment(String),
|
||||
AddAttachmentFilePicker(Option<String>),
|
||||
AddAttachmentPipe(String),
|
||||
RemoveAttachment(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum TabAction {
|
||||
ComposerAction(ComposerTabAction),
|
||||
Close,
|
||||
Kill(ComponentId),
|
||||
New(Option<Box<dyn Component>>),
|
||||
|
@ -90,35 +74,41 @@ pub enum TabAction {
|
|||
Man(crate::manpages::ManPages),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum MailingListAction {
|
||||
ListPost,
|
||||
ListArchive,
|
||||
ListUnsubscribe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum ViewAction {
|
||||
Pipe(String, Vec<String>),
|
||||
Filter(Option<String>),
|
||||
Filter(String),
|
||||
SaveAttachment(usize, String),
|
||||
PipeAttachment(usize, String, Vec<String>),
|
||||
ExportMail(String),
|
||||
AddAddressesToContacts,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum ComposeAction {
|
||||
AddAttachment(String),
|
||||
AddAttachmentFilePicker(Option<String>),
|
||||
AddAttachmentPipe(String),
|
||||
RemoveAttachment(usize),
|
||||
SaveDraft,
|
||||
ToggleSign,
|
||||
ToggleEncrypt,
|
||||
Mailto(Mailto),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum AccountAction {
|
||||
ReIndex,
|
||||
PrintAccountSetting(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum MailboxOperation {
|
||||
Create(NewMailboxPath),
|
||||
Delete(MailboxPath),
|
||||
|
@ -129,7 +119,7 @@ pub enum MailboxOperation {
|
|||
SetPermissions(MailboxPath),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum Action {
|
||||
Listing(ListingAction),
|
||||
ViewMailbox(usize),
|
||||
|
@ -169,7 +159,7 @@ type MailboxPath = String;
|
|||
type NewMailboxPath = String;
|
||||
|
||||
macro_rules! impl_into_action {
|
||||
($({$t:ty => $var:tt}),*$(,)?) => {
|
||||
($({$t:ty => $var:tt}),*) => {
|
||||
$(
|
||||
impl From<$t> for Action {
|
||||
fn from(v: $t) -> Self {
|
||||
|
@ -180,11 +170,11 @@ macro_rules! impl_into_action {
|
|||
};
|
||||
}
|
||||
macro_rules! impl_tuple_into_action {
|
||||
($({$a:ty,$b:ty => $var:tt}),*$(,)?) => {
|
||||
($({$a:ty,$b:ty => $var:tt}),*) => {
|
||||
$(
|
||||
impl From<($a,$b)> for Action {
|
||||
fn from((a, b): ($a,$b)) -> Self {
|
||||
Self::$var(a.to_string(), b)
|
||||
Self::$var(a, b)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
@ -200,7 +190,5 @@ impl_into_action!(
|
|||
);
|
||||
impl_tuple_into_action!(
|
||||
{ AccountName, MailboxOperation => Mailbox },
|
||||
{ AccountName, AccountAction => AccountAction },
|
||||
{ Arc<str>, MailboxOperation => Mailbox },
|
||||
{ Arc<str>, AccountAction => AccountAction },
|
||||
{ AccountName, AccountAction => AccountAction }
|
||||
);
|
||||
|
|
|
@ -140,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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,13 +121,7 @@ pub fn listing_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandErro
|
|||
}
|
||||
|
||||
pub fn compose_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
alt((
|
||||
add_attachment,
|
||||
mailto,
|
||||
remove_attachment,
|
||||
save_draft,
|
||||
discard_draft,
|
||||
))(input)
|
||||
alt((add_attachment, mailto, remove_attachment, save_draft))(input)
|
||||
}
|
||||
|
||||
pub fn account_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
|
@ -139,7 +133,6 @@ pub fn view(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
filter,
|
||||
pipe,
|
||||
save_attachment,
|
||||
pipe_attachment,
|
||||
export_mail,
|
||||
add_addresses_to_contacts,
|
||||
))(input)
|
||||
|
@ -569,15 +562,15 @@ pub fn printenv(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
Ok((input, Ok(PrintEnv(key.to_string()))))
|
||||
}
|
||||
pub fn currentdir(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, pwd};
|
||||
let (input, _) = alt((tag("cwd"), tag("pwd")))(input.ltrim())?;
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, currentdir};
|
||||
let (input, _) = tag("cwd")(input.ltrim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(CurrentDirectory)))
|
||||
}
|
||||
pub fn change_currentdir(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg: 1, max_arg: 1, cd};
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, change_currentdir};
|
||||
let (input, _) = tag("cd")(input.ltrim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
|
@ -641,18 +634,14 @@ pub fn pipe<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
|
|||
))(input)
|
||||
}
|
||||
pub fn filter(input: &'_ [u8]) -> IResult<&'_ [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg:255, filter};
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, filter};
|
||||
let (input, _) = tag("filter")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
if let Ok((input, _)) = eof(input) {
|
||||
arg_chk!(finish check, input);
|
||||
return Ok((input, Ok(View(Filter(None)))));
|
||||
}
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, cmd) = map_res(not_line_ending, std::str::from_utf8)(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
Ok((input, Ok(View(Filter(Some(cmd.to_string()))))))
|
||||
Ok((input, Ok(View(Filter(cmd.to_string())))))
|
||||
}
|
||||
pub fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
alt((
|
||||
|
@ -667,12 +656,7 @@ pub fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, C
|
|||
let (input, cmd) = quoted_argument(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(ComposerTabAction::AddAttachmentPipe(
|
||||
cmd.to_string(),
|
||||
)))),
|
||||
))
|
||||
Ok((input, Ok(Compose(AddAttachmentPipe(cmd.to_string())))))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, add_attachment};
|
||||
|
@ -683,46 +667,32 @@ pub fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, C
|
|||
let (input, path) = quoted_argument(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Compose(AddAttachment(path.to_string())))))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, add_attachment};
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Compose(AddAttachmentFilePicker(None)))))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, add_attachment_file_picker};
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, _) = tag("<")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, shell) = map_res(not_line_ending, std::str::from_utf8)(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(ComposerTabAction::AddAttachment(
|
||||
path.to_string(),
|
||||
)))),
|
||||
Ok(Compose(AddAttachmentFilePicker(Some(shell.to_string())))),
|
||||
))
|
||||
},
|
||||
alt((
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, add_attachment_file_picker};
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, _) = tag("<")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, shell) = map_res(not_line_ending, std::str::from_utf8)(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(
|
||||
ComposerTabAction::AddAttachmentFilePicker(Some(shell.to_string())),
|
||||
))),
|
||||
))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, add_attachment};
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(
|
||||
ComposerTabAction::AddAttachmentFilePicker(None),
|
||||
))),
|
||||
))
|
||||
},
|
||||
)),
|
||||
))(input)
|
||||
}
|
||||
pub fn remove_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
|
@ -734,12 +704,7 @@ pub fn remove_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandE
|
|||
let (input, idx) = map_res(quoted_argument, usize::from_str)(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(ComposerTabAction::RemoveAttachment(
|
||||
idx,
|
||||
)))),
|
||||
))
|
||||
Ok((input, Ok(Compose(RemoveAttachment(idx)))))
|
||||
}
|
||||
pub fn save_draft(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, save_draft };
|
||||
|
@ -747,18 +712,7 @@ pub fn save_draft(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>>
|
|||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Tab(ComposerAction(ComposerTabAction::SaveDraft)))))
|
||||
}
|
||||
pub fn discard_draft(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, discard_draft };
|
||||
let (input, _) = tag("discard-draft")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(ComposerTabAction::DiscardDraft))),
|
||||
))
|
||||
Ok((input, Ok(Compose(SaveDraft))))
|
||||
}
|
||||
pub fn create_mailbox(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, create_malbox};
|
||||
|
@ -896,35 +850,6 @@ pub fn save_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandErr
|
|||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(View(SaveAttachment(idx, path.to_string())))))
|
||||
}
|
||||
pub fn pipe_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg:{u8::MAX}, pipe_attachment};
|
||||
let (input, _) = tag("pipe-attachment")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, idx) = map_res(quoted_argument, usize::from_str)(input)?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, bin) = quoted_argument(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, args) = alt((
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Vec<String>> {
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, args) = separated_list1(is_a(" "), quoted_argument)(input)?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
args.into_iter().map(String::from).collect::<Vec<String>>(),
|
||||
))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Vec<String>> {
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Vec::with_capacity(0)))
|
||||
},
|
||||
))(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
Ok((input, Ok(View(PipeAttachment(idx, bin.to_string(), args)))))
|
||||
}
|
||||
pub fn export_mail(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, export_mail};
|
||||
let (input, _) = tag("export-mail")(input.trim())?;
|
||||
|
@ -1032,13 +957,8 @@ pub fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
for (tok, action) in [
|
||||
("thread_snooze", Listing(ToggleThreadSnooze)),
|
||||
("mouse", ToggleMouse),
|
||||
#[cfg(feature = "gpgme")]
|
||||
("sign", Tab(ComposerAction(ComposerTabAction::ToggleSign))),
|
||||
#[cfg(feature = "gpgme")]
|
||||
(
|
||||
"encrypt",
|
||||
Tab(ComposerAction(ComposerTabAction::ToggleEncrypt)),
|
||||
),
|
||||
("sign", Compose(ToggleSign)),
|
||||
("encrypt", Compose(ToggleEncrypt)),
|
||||
] {
|
||||
if let Ok((inner_input, _)) = tag!()(tok)(input.trim()) {
|
||||
input = inner_input;
|
||||
|
@ -1080,40 +1000,38 @@ pub fn manage_jobs(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>>
|
|||
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);
|
||||
#[allow(unused_variables)]
|
||||
let (input, manpage) = map_res(not_line_ending, std::str::from_utf8)(input.trim())?;
|
||||
let (input, _) = eof(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
#[cfg(feature = "cli-docs")]
|
||||
{
|
||||
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"))]
|
||||
{
|
||||
Ok((
|
||||
match crate::manpages::parse_manpage(manpage) {
|
||||
Ok(m) => Ok((input, Ok(Tab(Man(m))))),
|
||||
Err(err) => Ok((
|
||||
input,
|
||||
Err(CommandError::Other {
|
||||
inner: "this meli binary has not been compiled with the cli-docs feature".into(),
|
||||
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())?;
|
||||
|
|
|
@ -1,206 +0,0 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// 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 super::*;
|
||||
|
||||
#[test]
|
||||
fn test_command_parser() {
|
||||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
//print!("{}", $input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
// //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()
|
||||
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
|
||||
.collect::<HashSet<String>>()
|
||||
}};
|
||||
}
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()]).collect(),
|
||||
);
|
||||
input = "so".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort".to_string()]).collect(),
|
||||
);
|
||||
input = "so ".to_string();
|
||||
assert_eq!(&match_input!(input), &HashSet::default(),);
|
||||
input = "to".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
|
||||
);
|
||||
input = "toggle ".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter([
|
||||
"toggle mouse".to_string(),
|
||||
"toggle sign".to_string(),
|
||||
"toggle encrypt".to_string(),
|
||||
"toggle thread_snooze".to_string()
|
||||
])
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_command_parser_interactive() {
|
||||
use std::io;
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
input.clear();
|
||||
print!("> ");
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(_n) => {
|
||||
println!("Input is {:?}", input.as_str().trim());
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"suggestions = {:#?}",
|
||||
sugg.into_iter()
|
||||
.zip(vec.into_iter())
|
||||
.map(|(s, v)| format!(
|
||||
"{}{} {:?}",
|
||||
input.as_str().trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
},
|
||||
v
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
if input.trim() == "quit" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => println!("error: {}", error),
|
||||
}
|
||||
}
|
||||
println!("alright");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_parser_all() {
|
||||
use CommandError::*;
|
||||
|
||||
for cmd in [
|
||||
"set unseen",
|
||||
"set seen",
|
||||
"delete",
|
||||
"copyto somewhere",
|
||||
"moveto somewhere",
|
||||
"import fpath mpath",
|
||||
"close ",
|
||||
"go 5",
|
||||
] {
|
||||
parse_command(cmd.as_bytes()).unwrap_or_else(|err| panic!("{} failed {}", cmd, err));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
parse_command(b"setfafsfoo").unwrap_err().to_string(),
|
||||
Parsing {
|
||||
inner: "setfafsfoo".into(),
|
||||
kind: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"set foo").unwrap_err().to_string(),
|
||||
BadValue {
|
||||
inner: "foo".into(),
|
||||
suggestions: Some(&[
|
||||
"seen",
|
||||
"unseen",
|
||||
"plain",
|
||||
"threaded",
|
||||
"compact",
|
||||
"conversations"
|
||||
])
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"moveto ").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: false,
|
||||
takes: (1, Some(1)),
|
||||
given: 0,
|
||||
__func__: "moveto",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"reindex 1 2 3").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: true,
|
||||
takes: (1, Some(1)),
|
||||
given: 2,
|
||||
__func__: "reindex",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[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"
|
||||
);
|
||||
}
|
|
@ -126,8 +126,6 @@ pub enum ScrollUpdate {
|
|||
},
|
||||
}
|
||||
|
||||
/// A user interface component (also referred to as a UI widget).
|
||||
///
|
||||
/// Types implementing this Trait can draw on the terminal and receive events.
|
||||
/// If a type wants to skip drawing if it has not changed anything, it can hold
|
||||
/// some flag in its fields (eg `self.dirty = false`) and act upon that in their
|
||||
|
@ -250,12 +248,6 @@ impl Component for Box<dyn Component> {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Box<dyn Component> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self == other || self.id() == other.id()
|
||||
}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Attributes of a [`Component`] widget.
|
||||
///
|
||||
|
@ -305,7 +297,7 @@ impl ComponentPath {
|
|||
// log::trace!("continue;");
|
||||
continue;
|
||||
}
|
||||
cursor = cursor.children().shift_remove(id)?;
|
||||
cursor = cursor.children().remove(id)?;
|
||||
}
|
||||
Some(cursor)
|
||||
}
|
||||
|
@ -320,22 +312,3 @@ impl ComponentPath {
|
|||
self.tail.last()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use std::borrow::Cow;
|
||||
|
||||
pub use indexmap::IndexMap;
|
||||
|
||||
pub use crate::{
|
||||
accounts::MailboxEntry,
|
||||
command::*,
|
||||
components::{
|
||||
Component, ComponentAttr, ComponentId, ComponentPath, ExtendShortcutsMaps,
|
||||
ScrollContext, *,
|
||||
},
|
||||
jobs::{JobId, JobMetadata},
|
||||
melib::{text::TextProcessing, utils::datetime, SortOrder},
|
||||
shortcut, AccountHash, Action, Area, Attr, CellBuffer, Context, DataColumns, EnvelopeHash,
|
||||
Key, MailboxHash, Shortcuts, StatusEvent, ThemeAttribute, UIDialog, UIEvent, UIMode,
|
||||
};
|
||||
}
|
||||
|
|
1132
meli/src/conf.rs
1132
meli/src/conf.rs
File diff suppressed because it is too large
Load diff
|
@ -20,15 +20,13 @@
|
|||
*/
|
||||
|
||||
//! Configuration for composing email.
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{conf::ActionFlag, email::HeaderName};
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
|
||||
use crate::conf::{
|
||||
default_values::{ask, false_val, none, true_val},
|
||||
use super::{
|
||||
default_vals::{ask, false_val, none, true_val},
|
||||
deserializers::non_empty_string,
|
||||
};
|
||||
|
||||
|
@ -36,6 +34,9 @@ use crate::conf::{
|
|||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ComposingSettings {
|
||||
/// A command to pipe new emails to
|
||||
/// Required
|
||||
pub send_mail: SendMail,
|
||||
/// Command to launch editor. Can have arguments. Draft filename is given as
|
||||
/// the last argument. If it's missing, the environment variable $EDITOR is
|
||||
/// looked up.
|
||||
|
@ -61,7 +62,7 @@ pub struct ComposingSettings {
|
|||
/// Set default header values for new drafts
|
||||
/// Default: empty
|
||||
#[serde(default, alias = "default-header-values")]
|
||||
pub default_header_values: IndexMap<HeaderName, String>,
|
||||
pub default_header_values: HashMap<HeaderName, String>,
|
||||
/// 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
|
||||
|
@ -74,13 +75,11 @@ pub struct ComposingSettings {
|
|||
/// mail on its own. Default: true
|
||||
#[serde(default = "true_val")]
|
||||
pub store_sent_mail: bool,
|
||||
/// The attribution line that appears above the quoted reply text.
|
||||
///
|
||||
/// The attribution line appears above the quoted reply text.
|
||||
/// The format specifiers for the replied address are:
|
||||
/// - `%+f` — the sender's name and email address.
|
||||
/// - `%+n` — the sender's name (or email address, if no name is included).
|
||||
/// - `%+a` — the sender's email address.
|
||||
///
|
||||
/// The format string is passed to strftime(3) with the replied envelope's
|
||||
/// date. Default: "On %a, %0e %b %Y %H:%M, %+f wrote:%n"
|
||||
#[serde(default = "none")]
|
||||
|
@ -112,44 +111,17 @@ pub struct ComposingSettings {
|
|||
/// Disabled `compose-hooks`.
|
||||
#[serde(default, alias = "disabled-compose-hooks")]
|
||||
pub disabled_compose_hooks: Vec<String>,
|
||||
/// Plain text file with signature that will pre-populate an email draft.
|
||||
///
|
||||
/// Signatures must be explicitly enabled to be used, otherwise this setting
|
||||
/// will be ignored.
|
||||
///
|
||||
/// Default: `None`
|
||||
#[serde(default, alias = "signature-file")]
|
||||
pub signature_file: Option<PathBuf>,
|
||||
/// Pre-populate email drafts with signature, if any.
|
||||
///
|
||||
/// `meli` will lookup the signature value in this order:
|
||||
///
|
||||
/// 1. The `signature_file` setting.
|
||||
/// 2. `${XDG_CONFIG_DIR}/meli/<account>/signature`
|
||||
/// 3. `${XDG_CONFIG_DIR}/meli/signature`
|
||||
/// 4. `${XDG_CONFIG_DIR}/signature`
|
||||
/// 5. `${HOME}/.signature`
|
||||
/// 6. No signature otherwise.
|
||||
///
|
||||
/// Default: `false`
|
||||
#[serde(default = "false_val", alias = "use-signature")]
|
||||
pub use_signature: bool,
|
||||
/// Signature delimiter, that is, text that will be prefixed to your
|
||||
/// signature to separate it from the email body.
|
||||
///
|
||||
/// Default: `"\n\n-- \n"`
|
||||
#[serde(default, alias = "signature-delimiter")]
|
||||
pub signature_delimiter: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ComposingSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
send_mail: SendMail::ShellCommand("false".into()),
|
||||
editor_command: None,
|
||||
embedded_pty: false,
|
||||
format_flowed: true,
|
||||
insert_user_agent: true,
|
||||
default_header_values: IndexMap::default(),
|
||||
default_header_values: HashMap::default(),
|
||||
store_sent_mail: true,
|
||||
wrap_header_preamble: None,
|
||||
attribution_format_string: None,
|
||||
|
@ -159,9 +131,6 @@ impl Default for ComposingSettings {
|
|||
reply_prefix: res(),
|
||||
custom_compose_hooks: vec![],
|
||||
disabled_compose_hooks: vec![],
|
||||
signature_file: None,
|
||||
use_signature: false,
|
||||
signature_delimiter: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -218,14 +187,6 @@ pub enum SendMail {
|
|||
ShellCommand(String),
|
||||
}
|
||||
|
||||
impl Default for SendMail {
|
||||
/// Returns the `false` POSIX shell utility, in order to return an error
|
||||
/// when called.
|
||||
fn default() -> Self {
|
||||
Self::ShellCommand("false".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Shell command compose hooks (See
|
||||
/// [`crate::mail::compose::hooks::Hook`])
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
@ -259,10 +220,9 @@ Using a shell script
|
|||
Direct SMTP connection
|
||||
======================
|
||||
|
||||
[accounts.account-name]
|
||||
send_mail = { hostname = "mail.example.com", port = 587, auth = { type = "auto", password = { type = "raw", value = "hunter2" } }, security = { type = "STARTTLS" } }
|
||||
|
||||
[accounts.account-name.send_mail]
|
||||
[composing.send_mail]
|
||||
hostname = "mail.example.com"
|
||||
port = 587
|
||||
auth = { type = "auto", password = { type = "command_eval", value = "/path/to/password_script.sh" } }
|
||||
|
@ -324,8 +284,6 @@ impl<'de> Deserialize<'de> for SendMail {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum SendMailInner {
|
||||
|
@ -336,17 +294,11 @@ impl<'de> Deserialize<'de> for SendMail {
|
|||
ShellCommand(String),
|
||||
}
|
||||
|
||||
match melib::serde_path_to_error::deserialize(deserializer) {
|
||||
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)
|
||||
if err.inner().to_string() == D::Error::missing_field("send_mail").to_string() =>
|
||||
{
|
||||
// Surely there should be a better way to do this...
|
||||
Err(err.into_inner())
|
||||
}
|
||||
Err(_err) => Err(de::Error::custom(SENDMAIL_ERR_HELP)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,294 +0,0 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// 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 crate::conf::*;
|
||||
|
||||
pub trait DotAddressable: serde::Serialize {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
if !path.is_empty() {
|
||||
Err(Error::new(format!(
|
||||
"{} has no fields, it is of type {}",
|
||||
parent_field,
|
||||
std::any::type_name::<Self>()
|
||||
)))
|
||||
} else {
|
||||
Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for bool {}
|
||||
|
||||
impl DotAddressable for String {}
|
||||
impl DotAddressable for char {}
|
||||
impl DotAddressable for data_types::IndexStyle {}
|
||||
impl DotAddressable for data_types::SearchBackend {}
|
||||
impl DotAddressable for data_types::ThreadLayout {}
|
||||
impl DotAddressable for u64 {}
|
||||
impl DotAddressable for TagHash {}
|
||||
impl DotAddressable for crate::terminal::Color {}
|
||||
impl DotAddressable for crate::terminal::Attr {}
|
||||
impl DotAddressable for crate::terminal::Key {}
|
||||
impl DotAddressable for usize {}
|
||||
impl DotAddressable for Query {}
|
||||
impl DotAddressable for melib::LogLevel {}
|
||||
impl DotAddressable for PathBuf {}
|
||||
impl DotAddressable for ToggleFlag {}
|
||||
impl DotAddressable for ActionFlag {}
|
||||
impl DotAddressable for melib::SpecialUsageMailbox {}
|
||||
impl<T: DotAddressable> DotAddressable for Option<T> {}
|
||||
impl<T: DotAddressable> DotAddressable for Vec<T> {}
|
||||
// impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash, V: DotAddressable>
|
||||
// DotAddressable for HashMap<K, V>
|
||||
// {
|
||||
// }
|
||||
// impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash> DotAddressable for
|
||||
// HashSet<K> {}
|
||||
impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash, V: DotAddressable> DotAddressable
|
||||
for indexmap::IndexMap<K, V>
|
||||
{
|
||||
}
|
||||
impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash> DotAddressable for indexmap::IndexSet<K> {}
|
||||
impl DotAddressable for (SortField, SortOrder) {}
|
||||
|
||||
impl DotAddressable for LogSettings {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"log_file" => self.log_file.lookup(field, tail),
|
||||
"maximum_level" => self.maximum_level.lookup(field, tail),
|
||||
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for Settings {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"accounts" => self.accounts.lookup(field, tail),
|
||||
"pager" => self.pager.lookup(field, tail),
|
||||
"listing" => self.listing.lookup(field, tail),
|
||||
"notifications" => self.notifications.lookup(field, tail),
|
||||
"shortcuts" => self.shortcuts.lookup(field, tail),
|
||||
"tags" => Err(Error::new("unimplemented")),
|
||||
"composing" => Err(Error::new("unimplemented")),
|
||||
"pgp" => Err(Error::new("unimplemented")),
|
||||
"terminal" => self.terminal.lookup(field, tail),
|
||||
"log" => self.log.lookup(field, tail),
|
||||
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for AccountConf {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"account" => self.account.lookup(field, tail),
|
||||
"conf" => self.conf.lookup(field, tail),
|
||||
"conf_override" => self.conf_override.lookup(field, tail),
|
||||
"mailbox_confs" => self.mailbox_confs.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for MailUIConf {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let _tail = &path[1..];
|
||||
match *field {
|
||||
"pager" => Err(Error::new("unimplemented")), /* self.pager.lookup(field, */
|
||||
// tail),
|
||||
"listing" => Err(Error::new("unimplemented")), /* self.listing.lookup(field, */
|
||||
// tail),
|
||||
"notifications" => Err(Error::new("unimplemented")), /* self.notifications.lookup(field, tail), */
|
||||
"shortcuts" => Err(Error::new("unimplemented")), /* self.shortcuts. */
|
||||
// lookup(field,
|
||||
// tail),
|
||||
"composing" => Err(Error::new("unimplemented")), /* self.composing. */
|
||||
// lookup(field, tail),
|
||||
"identity" => Err(Error::new("unimplemented")), /* self.identity. */
|
||||
// lookup(field,
|
||||
// tail)<String>,
|
||||
"tags" => Err(Error::new("unimplemented")), /* self.tags.lookup(field, */
|
||||
// tail),
|
||||
"themes" => Err(Error::new("unimplemented")), /* self.themes. */
|
||||
// lookup(field,
|
||||
// tail)<Themes>,
|
||||
"pgp" => Err(Error::new("unimplemented")), //self.pgp.lookup(field, tail),
|
||||
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for FileMailboxConf {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"conf_override" => self.conf_override.lookup(field, tail),
|
||||
"mailbox_conf" => self.mailbox_conf.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for FileAccount {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"root_mailbox" => self.root_mailbox.lookup(field, tail),
|
||||
"format" => self.format.lookup(field, tail),
|
||||
"identity" => self.identity.lookup(field, tail),
|
||||
"display_name" => self.display_name.lookup(field, tail),
|
||||
"read_only" => self.read_only.lookup(field, tail),
|
||||
"subscribed_mailboxes" => self.subscribed_mailboxes.lookup(field, tail),
|
||||
"mailboxes" => self.mailboxes.lookup(field, tail),
|
||||
"search_backend" => self.search_backend.lookup(field, tail),
|
||||
"manual_refresh" => self.manual_refresh.lookup(field, tail),
|
||||
"refresh_command" => self.refresh_command.lookup(field, tail),
|
||||
"conf_override" => self.conf_override.lookup(field, tail),
|
||||
"extra" => self.extra.lookup(field, tail),
|
||||
"order" => self.order.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for melib::AccountSettings {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"name" => self.name.lookup(field, tail),
|
||||
"root_mailbox" => self.root_mailbox.lookup(field, tail),
|
||||
"format" => self.format.lookup(field, tail),
|
||||
"identity" => self.identity.lookup(field, tail),
|
||||
"read_only" => self.read_only.lookup(field, tail),
|
||||
"display_name" => self.display_name.lookup(field, tail),
|
||||
"subscribed_mailboxes" => self.subscribed_mailboxes.lookup(field, tail),
|
||||
"mailboxes" => self.mailboxes.lookup(field, tail),
|
||||
"manual_refresh" => self.manual_refresh.lookup(field, tail),
|
||||
"extra" => self.extra.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for melib::MailboxConf {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"alias" => self.alias.lookup(field, tail),
|
||||
"autoload" => self.autoload.lookup(field, tail),
|
||||
"subscribe" => self.subscribe.lookup(field, tail),
|
||||
"ignore" => self.ignore.lookup(field, tail),
|
||||
"usage" => self.usage.lookup(field, tail),
|
||||
"extra" => self.extra.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
pub mod dotaddressable;
|
||||
pub mod regex_pattern;
|
||||
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub enum IndexStyle {
|
||||
Plain,
|
||||
Threaded,
|
||||
#[default]
|
||||
Compact,
|
||||
Conversations,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for IndexStyle {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
plain if plain.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
|
||||
threaded if threaded.eq_ignore_ascii_case("threaded") => Ok(Self::Threaded),
|
||||
compact if compact.eq_ignore_ascii_case("compact") => Ok(Self::Compact),
|
||||
conversations if conversations.eq_ignore_ascii_case("conversations") => {
|
||||
Ok(Self::Conversations)
|
||||
}
|
||||
_ => Err(de::Error::custom(
|
||||
"invalid `index_style` value, expected one of: \"plain\", \"threaded\", \
|
||||
\"compact\" or \"conversations\".",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for IndexStyle {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Plain => serializer.serialize_str("plain"),
|
||||
Self::Threaded => serializer.serialize_str("threaded"),
|
||||
Self::Compact => serializer.serialize_str("compact"),
|
||||
Self::Conversations => serializer.serialize_str("conversations"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum SearchBackend {
|
||||
None,
|
||||
#[default]
|
||||
Auto,
|
||||
#[cfg(feature = "sqlite3")]
|
||||
Sqlite3,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SearchBackend {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
#[cfg(feature = "sqlite3")]
|
||||
sqlite3
|
||||
if sqlite3.eq_ignore_ascii_case("sqlite3")
|
||||
|| sqlite3.eq_ignore_ascii_case("sqlite") =>
|
||||
{
|
||||
Ok(Self::Sqlite3)
|
||||
}
|
||||
none if none.eq_ignore_ascii_case("none")
|
||||
|| none.eq_ignore_ascii_case("nothing")
|
||||
|| none.is_empty() =>
|
||||
{
|
||||
Ok(Self::None)
|
||||
}
|
||||
auto if auto.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
|
||||
_ => Err(de::Error::custom(if cfg!(feature = "sqlite3") {
|
||||
"invalid `search_backend` value, expected one of: \"sqlite3\", \"sqlite\", \
|
||||
\"none\" or \"auto\"."
|
||||
} else {
|
||||
"invalid `search_backend` value, expected one of: \"none\" or \"auto\"."
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SearchBackend {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
#[cfg(feature = "sqlite3")]
|
||||
Self::Sqlite3 => serializer.serialize_str("sqlite3"),
|
||||
Self::None => serializer.serialize_str("none"),
|
||||
Self::Auto => serializer.serialize_str("auto"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub enum ThreadLayout {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
#[default]
|
||||
Auto,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ThreadLayout {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
vertical if vertical.eq_ignore_ascii_case("vertical") => Ok(Self::Vertical),
|
||||
horizontal if horizontal.eq_ignore_ascii_case("horizontal") => Ok(Self::Horizontal),
|
||||
auto if auto.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
|
||||
_ => Err(de::Error::custom(
|
||||
"invalid `thread_layout` value, expected one of: \"vertical\", \"horizontal\" or \
|
||||
\"auto\".",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ThreadLayout {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Vertical => serializer.serialize_str("vertical"),
|
||||
Self::Horizontal => serializer.serialize_str("horizontal"),
|
||||
Self::Auto => serializer.serialize_str("auto"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
//
|
||||
// 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 melib::error::{Result, WrapResultIntoError};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
const fn lf_val() -> u8 {
|
||||
b'\n'
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RegexValue {
|
||||
Default {
|
||||
pattern: regex::Regex,
|
||||
},
|
||||
Builder {
|
||||
pattern: regex::Regex,
|
||||
options: RegexOptions,
|
||||
},
|
||||
}
|
||||
|
||||
impl RegexValue {
|
||||
pub fn new_with_options(pattern: &str, o: RegexOptions) -> Result<Self> {
|
||||
let mut b = regex::RegexBuilder::new(pattern);
|
||||
b.unicode(o.unicode)
|
||||
.case_insensitive(o.case_insensitive)
|
||||
.multi_line(o.multi_line)
|
||||
.dot_matches_new_line(o.dot_matches_new_line)
|
||||
.crlf(o.crlf)
|
||||
.line_terminator(o.line_terminator)
|
||||
.swap_greed(o.swap_greed)
|
||||
.ignore_whitespace(o.ignore_whitespace)
|
||||
.octal(o.octal);
|
||||
if let Some(v) = o.size_limit {
|
||||
b.size_limit(v);
|
||||
}
|
||||
let pattern = b
|
||||
.build()
|
||||
.wrap_err(|| format!("Could not compile regular expression `{}`", pattern))?;
|
||||
Ok(Self::Builder {
|
||||
pattern,
|
||||
options: o,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_iter<'w, 's>(&'w self, s: &'s str) -> FindIter<'w, 's> {
|
||||
let (Self::Default { pattern } | Self::Builder { pattern, .. }) = self;
|
||||
FindIter {
|
||||
iter: pattern.find_iter(s),
|
||||
char_indices: s.char_indices(),
|
||||
char_offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct RegexOptions {
|
||||
#[serde(default = "crate::conf::true_val")]
|
||||
unicode: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
case_insensitive: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
multi_line: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
dot_matches_new_line: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
crlf: bool,
|
||||
#[serde(default = "lf_val")]
|
||||
line_terminator: u8,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
swap_greed: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
ignore_whitespace: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
octal: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
size_limit: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for RegexOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
unicode: true,
|
||||
case_insensitive: false,
|
||||
multi_line: false,
|
||||
dot_matches_new_line: false,
|
||||
crlf: false,
|
||||
line_terminator: b'\n',
|
||||
swap_greed: false,
|
||||
ignore_whitespace: false,
|
||||
octal: false,
|
||||
size_limit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RegexValue {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// [ref:FIXME]: clippy false positive, remove when resolved.
|
||||
#![allow(clippy::collection_is_never_read)]
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Inner<'a> {
|
||||
Default {
|
||||
pattern: &'a str,
|
||||
},
|
||||
Builder {
|
||||
pattern: &'a str,
|
||||
#[serde(flatten)]
|
||||
o: RegexOptions,
|
||||
},
|
||||
}
|
||||
let s = <Inner>::deserialize(deserializer);
|
||||
Ok(
|
||||
match s.map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
r#"expected one of "true", "false", "ask", found `{}`"#,
|
||||
err
|
||||
))
|
||||
})? {
|
||||
Inner::Default { pattern } => Self::Default {
|
||||
pattern: regex::Regex::new(pattern).map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Could not compile regular expression `{}`: {}",
|
||||
pattern, err
|
||||
))
|
||||
})?,
|
||||
},
|
||||
Inner::Builder { pattern, o } => {
|
||||
let mut b = regex::RegexBuilder::new(pattern);
|
||||
b.unicode(o.unicode)
|
||||
.case_insensitive(o.case_insensitive)
|
||||
.multi_line(o.multi_line)
|
||||
.dot_matches_new_line(o.dot_matches_new_line)
|
||||
.crlf(o.crlf)
|
||||
.line_terminator(o.line_terminator)
|
||||
.swap_greed(o.swap_greed)
|
||||
.ignore_whitespace(o.ignore_whitespace)
|
||||
.octal(o.octal);
|
||||
if let Some(v) = o.size_limit {
|
||||
b.size_limit(v);
|
||||
}
|
||||
let pattern = b.build().map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Could not compile regular expression `{}`: {}",
|
||||
pattern, err
|
||||
))
|
||||
})?;
|
||||
Self::Builder {
|
||||
pattern,
|
||||
options: o,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FindIter<'r, 's> {
|
||||
iter: regex::Matches<'r, 's>,
|
||||
char_indices: std::str::CharIndices<'s>,
|
||||
char_offset: usize,
|
||||
}
|
||||
|
||||
impl Iterator for FindIter<'_, '_> {
|
||||
type Item = (usize, usize);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let next_byte_offset = self.iter.next()?;
|
||||
|
||||
let mut next_char_index = self.char_indices.next()?;
|
||||
|
||||
while next_byte_offset.start() < next_char_index.0 {
|
||||
self.char_offset += 1;
|
||||
next_char_index = self.char_indices.next()?;
|
||||
}
|
||||
let start = self.char_offset;
|
||||
|
||||
while next_byte_offset.end()
|
||||
> self
|
||||
.char_indices
|
||||
.next()
|
||||
.map(|(v, _)| v)
|
||||
.unwrap_or_else(|| next_byte_offset.end())
|
||||
{
|
||||
self.char_offset += 1;
|
||||
}
|
||||
let end = self.char_offset + 1;
|
||||
|
||||
Some((start, end))
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// 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
|
||||
|
||||
//! default value functions for deserializing
|
||||
|
||||
pub fn false_val<T: From<bool>>() -> T {
|
||||
false.into()
|
||||
}
|
||||
|
||||
pub fn true_val<T: From<bool>>() -> T {
|
||||
true.into()
|
||||
}
|
||||
|
||||
pub fn zero_val<T: From<usize>>() -> T {
|
||||
0.into()
|
||||
}
|
||||
|
||||
pub fn eighty_val<T: From<usize>>() -> T {
|
||||
80.into()
|
||||
}
|
||||
|
||||
pub fn none<T>() -> Option<T> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn internal_value_false<T: From<melib::conf::ToggleFlag>>() -> T {
|
||||
melib::conf::ToggleFlag::InternalVal(false).into()
|
||||
}
|
||||
|
||||
pub fn internal_value_true<T: From<melib::conf::ToggleFlag>>() -> T {
|
||||
melib::conf::ToggleFlag::InternalVal(true).into()
|
||||
}
|
||||
|
||||
pub fn action_internal_value_false<T: From<melib::ActionFlag>>() -> T {
|
||||
melib::conf::ActionFlag::InternalVal(false).into()
|
||||
}
|
||||
|
||||
//pub fn action_internal_value_true<
|
||||
// T: From<melib::conf::ActionFlag>,
|
||||
//>() -> T {
|
||||
// melib::conf::ActionFlag::InternalVal(true).into()
|
||||
//}
|
||||
|
||||
pub fn ask<T: From<melib::conf::ActionFlag>>() -> T {
|
||||
melib::conf::ActionFlag::Ask.into()
|
||||
}
|
|
@ -21,11 +21,7 @@
|
|||
|
||||
use melib::{search::Query, Error, Result, ToggleFlag};
|
||||
|
||||
use crate::conf::{
|
||||
data_types::{IndexStyle, ThreadLayout},
|
||||
default_values::*,
|
||||
DotAddressable,
|
||||
};
|
||||
use super::{default_vals::*, DotAddressable, IndexStyle};
|
||||
|
||||
/// Settings for mail listings
|
||||
///
|
||||
|
@ -171,14 +167,6 @@ pub struct ListingSettings {
|
|||
/// Hide sidebar on launch. Default: "false"
|
||||
#[serde(default = "false_val", alias = "hide-sidebar-on-launch")]
|
||||
pub hide_sidebar_on_launch: bool,
|
||||
|
||||
/// Default: ' '
|
||||
#[serde(default = "default_divider")]
|
||||
pub mail_view_divider: char,
|
||||
|
||||
/// Default: "auto"
|
||||
#[serde(default)]
|
||||
pub thread_layout: ThreadLayout,
|
||||
}
|
||||
|
||||
const fn default_divider() -> char {
|
||||
|
@ -215,8 +203,6 @@ impl Default for ListingSettings {
|
|||
relative_menu_indices: true,
|
||||
relative_list_indices: true,
|
||||
hide_sidebar_on_launch: false,
|
||||
mail_view_divider: default_divider(),
|
||||
thread_layout: ThreadLayout::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -260,17 +246,13 @@ impl DotAddressable for ListingSettings {
|
|||
"relative_menu_indices" => self.relative_menu_indices.lookup(field, tail),
|
||||
"relative_list_indices" => self.relative_list_indices.lookup(field, tail),
|
||||
"hide_sidebar_on_launch" => self.hide_sidebar_on_launch.lookup(field, tail),
|
||||
"mail_view_divider" => self.mail_view_divider.lookup(field, tail),
|
||||
"thread_layout" => self.thread_layout.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
|
||||
use melib::{Error, Result, ToggleFlag};
|
||||
|
||||
use crate::conf::{
|
||||
default_values::{internal_value_false, none, true_val},
|
||||
use super::{
|
||||
default_vals::{internal_value_false, none, true_val},
|
||||
DotAddressable,
|
||||
};
|
||||
|
||||
|
@ -88,9 +88,7 @@ impl DotAddressable for NotificationsSettings {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -21,10 +21,9 @@
|
|||
|
||||
//! Settings for the pager function.
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{Error, Result, ToggleFlag};
|
||||
|
||||
use crate::conf::{default_values::*, deserializers::*, DotAddressable};
|
||||
use super::{default_vals::*, deserializers::*, DotAddressable};
|
||||
|
||||
/// Settings for the pager function.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
@ -61,12 +60,6 @@ pub struct PagerSettings {
|
|||
#[serde(default = "none", deserialize_with = "non_empty_opt_string")]
|
||||
pub filter: Option<String>,
|
||||
|
||||
/// Named filter commands to use at will.
|
||||
///
|
||||
/// Default: empty
|
||||
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
||||
pub named_filters: IndexMap<String, String>,
|
||||
|
||||
/// A command to pipe html output before displaying it in a pager
|
||||
/// Default: None
|
||||
#[serde(
|
||||
|
@ -133,7 +126,6 @@ impl Default for PagerSettings {
|
|||
sticky_headers: false,
|
||||
pager_ratio: 80,
|
||||
filter: None,
|
||||
named_filters: IndexMap::default(),
|
||||
html_filter: None,
|
||||
html_open: None,
|
||||
format_flowed: true,
|
||||
|
@ -158,7 +150,6 @@ impl DotAddressable for PagerSettings {
|
|||
"sticky_headers" => self.sticky_headers.lookup(field, tail),
|
||||
"pager_ratio" => self.pager_ratio.lookup(field, tail),
|
||||
"filter" => self.filter.lookup(field, tail),
|
||||
"named_filters" => self.named_filters.lookup(field, tail),
|
||||
"html_filter" => self.html_filter.lookup(field, tail),
|
||||
"html_open" => self.html_open.lookup(field, tail),
|
||||
"format_flowed" => self.format_flowed.lookup(field, tail),
|
||||
|
@ -176,9 +167,7 @@ impl DotAddressable for PagerSettings {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
use melib::conf::ActionFlag;
|
||||
|
||||
use crate::conf::default_values::*;
|
||||
use super::default_vals::*;
|
||||
|
||||
/// Settings for digital signing and encryption
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
@ -60,10 +60,6 @@ pub struct PGPSettings {
|
|||
#[serde(default = "none", alias = "encrypt-key")]
|
||||
pub encrypt_key: Option<String>,
|
||||
|
||||
/// Default: true
|
||||
#[serde(default = "true_val", alias = "encrypt-for-self")]
|
||||
pub encrypt_for_self: bool,
|
||||
|
||||
/// Allow remote lookups
|
||||
/// Default: False
|
||||
#[serde(
|
||||
|
@ -103,7 +99,6 @@ impl Default for PGPSettings {
|
|||
auto_decrypt: true.into(),
|
||||
auto_sign: false.into(),
|
||||
auto_encrypt: false.into(),
|
||||
encrypt_for_self: true,
|
||||
sign_key: None,
|
||||
decrypt_key: None,
|
||||
encrypt_key: None,
|
||||
|
|
|
@ -1,281 +0,0 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// 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
|
||||
|
||||
//! Preprocess configuration files by unfolding `include` macros.
|
||||
|
||||
use std::{
|
||||
io::{self, BufRead, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use melib::{
|
||||
error::{Error, ErrorKind, Result, ResultIntoError, WrapResultIntoError},
|
||||
utils::parsec::*,
|
||||
ShellExpandTrait,
|
||||
};
|
||||
|
||||
/// Try to parse line into a path to be included.
|
||||
pub fn include_directive<'a>() -> impl Parser<'a, Option<&'a str>> {
|
||||
move |input: &'a str| {
|
||||
enum State {
|
||||
Start,
|
||||
Path,
|
||||
}
|
||||
use State::*;
|
||||
let mut state = State::Start;
|
||||
|
||||
let mut i = 0;
|
||||
while i < input.len() {
|
||||
match (&state, input.as_bytes()[i]) {
|
||||
(Start, b'#') => {
|
||||
return Ok(("", None));
|
||||
}
|
||||
(Start, b) if (b as char).is_whitespace() => { /* consume */ }
|
||||
(Start, _) if input.as_bytes()[i..].starts_with(b"include(") => {
|
||||
i += "include(".len();
|
||||
state = Path;
|
||||
continue;
|
||||
}
|
||||
(Start, _) => {
|
||||
return Ok(("", None));
|
||||
}
|
||||
(Path, b'"') | (Path, b'\'') | (Path, b'`') => {
|
||||
let mut end = i + 1;
|
||||
while end < input.len() && input.as_bytes()[end] != input.as_bytes()[i] {
|
||||
end += 1;
|
||||
}
|
||||
if end == input.len() {
|
||||
return Err(input);
|
||||
}
|
||||
let ret = &input[i + 1..end];
|
||||
end += 1;
|
||||
if end < input.len() && input.as_bytes()[end] != b')' {
|
||||
/* Nothing else allowed in line */
|
||||
return Err(input);
|
||||
}
|
||||
end += 1;
|
||||
while end < input.len() {
|
||||
if !(input.as_bytes()[end] as char).is_whitespace() {
|
||||
/* Nothing else allowed in line */
|
||||
return Err(input);
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
return Ok(("", Some(ret)));
|
||||
}
|
||||
(Path, _) => return Err(input),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Ok(("", None))
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands `include` macros in path.
|
||||
fn pp_helper(path: &Path, level: u8) -> Result<String> {
|
||||
if level > 7 {
|
||||
return Err(Error::new(format!(
|
||||
"Maximum recursion limit reached while unfolding include directives in {}. Have you \
|
||||
included a config file within itself?",
|
||||
path.display()
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError));
|
||||
}
|
||||
let mut contents = String::new();
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
file.read_to_string(&mut contents)?;
|
||||
let mut ret = String::with_capacity(contents.len());
|
||||
|
||||
for (i, l) in contents.lines().enumerate() {
|
||||
if let (_, Some(sub_path)) = include_directive().parse(l).map_err(|l| {
|
||||
Error::new(format!(
|
||||
"Malformed include directive in line {} of file {}: {}\nConfiguration uses the \
|
||||
standard m4 macro include(\"filename\").",
|
||||
i,
|
||||
path.display(),
|
||||
l
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError)
|
||||
})? {
|
||||
let mut p = Path::new(sub_path).expand();
|
||||
if p.is_relative() {
|
||||
/* We checked that path is ok above so we can do unwrap here */
|
||||
let prefix = path.parent().unwrap();
|
||||
p = prefix.join(p)
|
||||
}
|
||||
|
||||
ret.push_str(&pp_helper(&p, level + 1).chain_err_related_path(&p)?);
|
||||
} else {
|
||||
ret.push_str(l);
|
||||
ret.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn pp_inner(path: &Path) -> Result<String> {
|
||||
let p_buf: PathBuf = if path.is_relative() {
|
||||
path.expand().canonicalize()?
|
||||
} else {
|
||||
path.expand()
|
||||
};
|
||||
|
||||
let mut ret = expand_config(&p_buf)?;
|
||||
if let Ok(xdg_dirs) = xdg::BaseDirectories::with_prefix("meli") {
|
||||
for theme_mailbox in xdg_dirs.find_config_files("themes") {
|
||||
let read_dir =
|
||||
std::fs::read_dir(&theme_mailbox).chain_err_related_path(&theme_mailbox)?;
|
||||
for theme in read_dir {
|
||||
let theme_path = theme?.path();
|
||||
if let Some(extension) = theme_path.extension() {
|
||||
if extension == "toml" {
|
||||
ret.push_str(
|
||||
&pp_helper(&theme_path, 0).chain_err_related_path(&theme_path)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Expands `include` macros in configuration file and other configuration
|
||||
/// files (eg. themes) in the filesystem.
|
||||
pub fn pp(path: &Path) -> Result<String> {
|
||||
pp_inner(path)
|
||||
.wrap_err(|| "Could not preprocess configuration file")
|
||||
.chain_err_related_path(path)
|
||||
.chain_err_kind(ErrorKind::Configuration)
|
||||
}
|
||||
|
||||
pub fn expand_config(conf_path: &Path) -> Result<String> {
|
||||
fn inner(conf_path: &Path) -> Result<String> {
|
||||
let _paths = get_included_configs(conf_path)?;
|
||||
const M4_PREAMBLE: &str = r#"define(`builtin_include', defn(`include'))dnl
|
||||
define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl
|
||||
"#;
|
||||
let mut contents = String::new();
|
||||
contents.clear();
|
||||
let mut file = std::fs::File::open(conf_path)?;
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let mut handle = Command::new("m4")
|
||||
.current_dir(conf_path.parent().unwrap_or_else(|| Path::new("/")))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
let mut stdin = handle.stdin.take().unwrap();
|
||||
stdin.write_all(M4_PREAMBLE.as_bytes())?;
|
||||
stdin.write_all(contents.as_bytes())?;
|
||||
drop(stdin);
|
||||
let stdout = handle.wait_with_output()?.stdout;
|
||||
Ok(String::from_utf8_lossy(&stdout).to_string())
|
||||
}
|
||||
|
||||
inner(conf_path).chain_err_related_path(conf_path)
|
||||
}
|
||||
|
||||
pub fn get_included_configs(conf_path: &Path) -> Result<Vec<PathBuf>> {
|
||||
const M4_PREAMBLE: &str = r#"divert(-1)dnl
|
||||
define(`include', `divert(0)$1
|
||||
divert(-1)
|
||||
')dnl
|
||||
changequote(`"', `"')dnl
|
||||
"#;
|
||||
let mut ret = vec![];
|
||||
let prefix = conf_path.parent().unwrap().to_path_buf();
|
||||
let mut stack = vec![(None::<PathBuf>, conf_path.to_path_buf())];
|
||||
let mut contents = String::new();
|
||||
while let Some((parent, p)) = stack.pop() {
|
||||
if !p.exists() || p.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
"Path {}{included}{in_parent} {msg}.",
|
||||
p.display(),
|
||||
included = if parent.is_some() {
|
||||
" which is included in "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
in_parent = if let Some(parent) = parent {
|
||||
std::borrow::Cow::Owned(parent.display().to_string())
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed("")
|
||||
},
|
||||
msg = if !p.exists() {
|
||||
"does not exist"
|
||||
} else {
|
||||
"is a directory, not a text file"
|
||||
}
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError));
|
||||
}
|
||||
contents.clear();
|
||||
let mut file = std::fs::File::open(&p).chain_err_related_path(&p)?;
|
||||
file.read_to_string(&mut contents)
|
||||
.chain_err_related_path(&p)?;
|
||||
|
||||
let mut handle = match Command::new("m4")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(handle) => handle,
|
||||
Err(err) => match err.kind() {
|
||||
io::ErrorKind::NotFound => {
|
||||
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::Platform))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut stdin = handle.stdin.take().unwrap();
|
||||
stdin.write_all(M4_PREAMBLE.as_bytes())?;
|
||||
stdin.write_all(contents.as_bytes())?;
|
||||
drop(stdin);
|
||||
let stdout = handle.wait_with_output()?.stdout.clone();
|
||||
for subpath in stdout.lines() {
|
||||
let subpath = subpath?;
|
||||
let path = &Path::new(&subpath);
|
||||
if path.is_absolute() {
|
||||
stack.push((Some(p.to_path_buf()), path.to_path_buf()));
|
||||
} else {
|
||||
stack.push((Some(p.to_path_buf()), prefix.join(path)));
|
||||
}
|
||||
}
|
||||
ret.push(p.to_path_buf());
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
|
@ -83,14 +83,12 @@ impl DotAddressable for Shortcuts {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommandShortcut {
|
||||
pub shortcut: Key,
|
||||
pub command: Vec<String>,
|
||||
|
@ -150,7 +148,7 @@ macro_rules! shortcut_key_values {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self).map_err(|err| err.to_string())?.to_string()),
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -174,12 +172,10 @@ shortcut_key_values! { "listing",
|
|||
search |> "Search within list of e-mails." |> Key::Char('/'),
|
||||
refresh |> "Manually request a mailbox refresh." |> Key::F(5),
|
||||
set_seen |> "Set thread as seen." |> Key::Char('n'),
|
||||
send_to_trash |> "Send entry to trash folder." |> Key::Char('D'),
|
||||
union_modifier |> "Union modifier." |> Key::Ctrl('u'),
|
||||
diff_modifier |> "Difference modifier." |> Key::Ctrl('d'),
|
||||
intersection_modifier |> "Intersection modifier." |> Key::Ctrl('i'),
|
||||
select_entry |> "Select thread entry." |> Key::Char('V'),
|
||||
select_motion |> "Perform select motion with a movement." |> Key::Char('v'),
|
||||
select_entry |> "Select thread entry." |> Key::Char('v'),
|
||||
increase_sidebar |> "Increase sidebar width." |> Key::Ctrl('f'),
|
||||
decrease_sidebar |> "Decrease sidebar width." |> Key::Ctrl('d'),
|
||||
next_entry |> "Focus on next entry." |> Key::Ctrl('n'),
|
||||
|
@ -199,7 +195,6 @@ shortcut_key_values! { "contact-list",
|
|||
scroll_down |> "Scroll down list." |> Key::Char('j'),
|
||||
create_contact |> "Create new contact." |> Key::Char('c'),
|
||||
edit_contact |> "Edit contact under cursor." |> Key::Char('e'),
|
||||
export_contact |> "Export contact under cursor to .vcf." |> Key::Char('E'),
|
||||
delete_contact |> "Delete contact under cursor." |> Key::Char('d'),
|
||||
mail_contact |> "Mail contact under cursor." |> Key::Char('m'),
|
||||
next_account |> "Go to next account." |> Key::Char('H'),
|
||||
|
@ -214,8 +209,7 @@ shortcut_key_values! { "pager",
|
|||
page_down |> "Go to next pager page." |> Key::PageDown,
|
||||
page_up |> "Go to previous pager page." |> Key::PageUp,
|
||||
scroll_down |> "Scroll down pager." |> Key::Char('j'),
|
||||
scroll_up |> "Scroll up pager." |> Key::Char('k'),
|
||||
select_filter |> "Select content filter." |> Key::Char('f')
|
||||
scroll_up |> "Scroll up pager." |> Key::Char('k')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,8 +229,8 @@ shortcut_key_values! { "general",
|
|||
home_page |> "Go to first page. (catch-all setting)" |> Key::Home,
|
||||
end_page |> "Go to last page. (catch-all setting)" |> Key::End,
|
||||
open_entry |> "Open list entry. (catch-all setting)" |> Key::Char('\n'),
|
||||
info_message_next |> "Show next info message, if any." |> Key::Char('>'),
|
||||
info_message_previous |> "Show previous info message, if any." |> Key::Char('<'),
|
||||
info_message_next |> "Show next info message, if any." |> Key::Alt('>'),
|
||||
info_message_previous |> "Show previous info message, if any." |> Key::Alt('<'),
|
||||
focus_in_text_field |> "Focus on a text field." |> Key::Char('\n'),
|
||||
next_search_result |> "Scroll to next search result." |> Key::Char('n'),
|
||||
previous_search_result |> "Scroll to previous search result." |> Key::Char('N')
|
||||
|
|
|
@ -21,22 +21,24 @@
|
|||
|
||||
//! E-mail tag configuration and {de,}serializing.
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use melib::{Error, Result, TagHash};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use crate::{conf::DotAddressable, terminal::Color};
|
||||
use super::DotAddressable;
|
||||
use crate::terminal::Color;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TagsSettings {
|
||||
#[serde(default, deserialize_with = "tag_color_de")]
|
||||
pub colors: IndexMap<TagHash, Color>,
|
||||
pub colors: HashMap<TagHash, Color>,
|
||||
#[serde(default, deserialize_with = "tag_set_de", alias = "ignore-tags")]
|
||||
pub ignore_tags: IndexSet<TagHash>,
|
||||
pub ignore_tags: HashSet<TagHash>,
|
||||
}
|
||||
|
||||
pub fn tag_set_de<'de, D, T: std::convert::From<IndexSet<TagHash>>>(
|
||||
pub fn tag_set_de<'de, D, T: std::convert::From<HashSet<TagHash>>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<T, D::Error>
|
||||
where
|
||||
|
@ -45,11 +47,11 @@ where
|
|||
Ok(<Vec<String>>::deserialize(deserializer)?
|
||||
.into_iter()
|
||||
.map(|tag| TagHash::from_bytes(tag.as_bytes()))
|
||||
.collect::<IndexSet<TagHash>>()
|
||||
.collect::<HashSet<TagHash>>()
|
||||
.into())
|
||||
}
|
||||
|
||||
pub fn tag_color_de<'de, D, T: std::convert::From<IndexMap<TagHash, Color>>>(
|
||||
pub fn tag_color_de<'de, D, T: std::convert::From<HashMap<TagHash, Color>>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<T, D::Error>
|
||||
where
|
||||
|
@ -62,7 +64,7 @@ where
|
|||
C(Color),
|
||||
}
|
||||
|
||||
Ok(<IndexMap<String, _Color>>::deserialize(deserializer)?
|
||||
Ok(<HashMap<String, _Color>>::deserialize(deserializer)?
|
||||
.into_iter()
|
||||
.map(|(tag, color)| {
|
||||
(
|
||||
|
@ -73,7 +75,7 @@ where
|
|||
},
|
||||
)
|
||||
})
|
||||
.collect::<IndexMap<TagHash, Color>>()
|
||||
.collect::<HashMap<TagHash, Color>>()
|
||||
.into())
|
||||
}
|
||||
|
||||
|
@ -91,9 +93,7 @@ impl DotAddressable for TagsSettings {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,6 @@ pub struct TerminalSettings {
|
|||
pub themes: Themes,
|
||||
pub ascii_drawing: bool,
|
||||
pub use_color: ToggleFlag,
|
||||
/// Try forcing text presentations of symbols and emoji as much as possible.
|
||||
/// Might not work on all non-text symbols and is experimental.
|
||||
pub force_text_presentation: ToggleFlag,
|
||||
/// Use mouse events. This will disable text selection, but you will be able
|
||||
/// to resize some widgets.
|
||||
/// Default: False
|
||||
|
@ -62,7 +59,6 @@ impl Default for TerminalSettings {
|
|||
theme: "dark".to_string(),
|
||||
themes: Themes::default(),
|
||||
ascii_drawing: false,
|
||||
force_text_presentation: ToggleFlag::InternalVal(false),
|
||||
use_color: ToggleFlag::InternalVal(true),
|
||||
use_mouse: ToggleFlag::InternalVal(false),
|
||||
mouse_flag: Some("🖱️ ".to_string()),
|
||||
|
@ -74,19 +70,15 @@ impl Default for TerminalSettings {
|
|||
}
|
||||
|
||||
impl TerminalSettings {
|
||||
#[inline]
|
||||
pub fn use_color(&self) -> bool {
|
||||
// Don't use color if
|
||||
// - Either NO_COLOR is set and user hasn't explicitly set use_colors or
|
||||
// - User has explicitly set use_colors to false
|
||||
!((std::env::var_os("NO_COLOR").is_some()
|
||||
/* Don't use color if
|
||||
* - Either NO_COLOR is set and user hasn't explicitly set use_colors or
|
||||
* - User has explicitly set use_colors to false
|
||||
*/
|
||||
!((std::env::var("NO_COLOR").is_ok()
|
||||
&& (self.use_color.is_false() || self.use_color.is_internal()))
|
||||
|| (self.use_color.is_false() && !self.use_color.is_internal()))
|
||||
}
|
||||
|
||||
pub fn use_text_presentation(&self) -> bool {
|
||||
self.force_text_presentation.is_true() || !self.use_color()
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for TerminalSettings {
|
||||
|
@ -98,7 +90,6 @@ impl DotAddressable for TerminalSettings {
|
|||
"theme" => self.theme.lookup(field, tail),
|
||||
"themes" => Err(Error::new("unimplemented")),
|
||||
"ascii_drawing" => self.ascii_drawing.lookup(field, tail),
|
||||
"force_text_presentation" => self.force_text_presentation.lookup(field, tail),
|
||||
"use_color" => self.use_color.lookup(field, tail),
|
||||
"use_mouse" => self.use_mouse.lookup(field, tail),
|
||||
"mouse_flag" => self.mouse_flag.lookup(field, tail),
|
||||
|
@ -113,103 +104,24 @@ impl DotAddressable for TerminalSettings {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ProgressSpinnerSequence {
|
||||
Integer(usize),
|
||||
Custom {
|
||||
frames: Vec<String>,
|
||||
#[serde(default = "interval_ms_val")]
|
||||
interval_ms: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProgressSpinnerSequence {
|
||||
pub const fn interval_ms(&self) -> u64 {
|
||||
match self {
|
||||
Self::Integer(_) => interval_ms_val(),
|
||||
Self::Custom {
|
||||
frames: _,
|
||||
interval_ms,
|
||||
} => *interval_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
const fn interval_ms_val() -> u64 {
|
||||
crate::utilities::ProgressSpinner::INTERVAL_MS
|
||||
}
|
||||
|
||||
impl DotAddressable for ProgressSpinnerSequence {}
|
||||
|
||||
impl<'de> Deserialize<'de> for ProgressSpinnerSequence {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum Inner {
|
||||
Integer(usize),
|
||||
Frames(Vec<String>),
|
||||
Custom {
|
||||
frames: Vec<String>,
|
||||
#[serde(default = "interval_ms_val")]
|
||||
interval_ms: u64,
|
||||
},
|
||||
}
|
||||
let s = <Inner>::deserialize(deserializer)?;
|
||||
match s {
|
||||
Inner::Integer(i) => Ok(Self::Integer(i)),
|
||||
Inner::Frames(frames) => Ok(Self::Custom {
|
||||
frames,
|
||||
interval_ms: interval_ms_val(),
|
||||
}),
|
||||
Inner::Custom {
|
||||
frames,
|
||||
interval_ms,
|
||||
} => Ok(Self::Custom {
|
||||
frames,
|
||||
interval_ms,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ProgressSpinnerSequence {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Integer(i) => serializer.serialize_i64(*i as i64),
|
||||
Self::Custom {
|
||||
frames,
|
||||
interval_ms,
|
||||
} => {
|
||||
if *interval_ms == interval_ms_val() {
|
||||
use serde::ser::SerializeSeq;
|
||||
let mut seq = serializer.serialize_seq(Some(frames.len()))?;
|
||||
for element in frames {
|
||||
seq.serialize_element(element)?;
|
||||
}
|
||||
seq.end()
|
||||
} else {
|
||||
use serde::ser::SerializeMap;
|
||||
|
||||
let mut map = serializer.serialize_map(Some(2))?;
|
||||
map.serialize_entry("frames", frames)?;
|
||||
map.serialize_entry("interval_ms", interval_ms)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,384 +0,0 @@
|
|||
//
|
||||
// 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::{
|
||||
borrow::Cow,
|
||||
fmt::Write as FmtWrite,
|
||||
fs::{self, OpenOptions},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
conf::{themes::*, FileSettings},
|
||||
terminal::Color,
|
||||
};
|
||||
|
||||
pub struct ConfigFile {
|
||||
pub path: PathBuf,
|
||||
pub file: fs::File,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
pub fn new(
|
||||
content: &str,
|
||||
dir: &tempfile::TempDir,
|
||||
) -> std::result::Result<Self, std::io::Error> {
|
||||
let mut filename = String::with_capacity(2 * 16);
|
||||
for byte in melib::utils::random::random_u64().to_be_bytes() {
|
||||
write!(&mut filename, "{:02X}", byte).unwrap();
|
||||
}
|
||||
let mut path = dir.path().to_path_buf();
|
||||
path.push(&*filename);
|
||||
let mut file = OpenOptions::new()
|
||||
.create_new(true)
|
||||
.append(true)
|
||||
.open(&path)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
Ok(Self { path, file })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConfigFile {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
pub const TEST_CONFIG: &str = r#"
|
||||
[accounts.account-name]
|
||||
root_mailbox = "/path/to/root/mailbox"
|
||||
format = "Maildir"
|
||||
send_mail = 'false'
|
||||
listing.index_style = "Conversations" # or [plain, threaded, compact]
|
||||
identity="email@example.com"
|
||||
display_name = "Name"
|
||||
subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
|
||||
# Set mailbox-specific settings
|
||||
[accounts.account-name.mailboxes]
|
||||
"INBOX" = { rename="Inbox" }
|
||||
"drafts" = { rename="Drafts" }
|
||||
"foobar-devel" = { ignore = true } # don't show notifications for this mailbox
|
||||
|
||||
# Setting up an mbox account
|
||||
[accounts.mbox]
|
||||
root_mailbox = "/var/mail/username"
|
||||
format = "mbox"
|
||||
send_mail = 'false'
|
||||
listing.index_style = "Compact"
|
||||
identity="username@hostname.local"
|
||||
"#;
|
||||
|
||||
pub const EXTRA_CONFIG: &str = r#"
|
||||
[accounts.mbox]
|
||||
root_mailbox = "/"
|
||||
format = "mbox"
|
||||
send_mail = 'false'
|
||||
index_style = "Compact"
|
||||
identity="username@hostname.local"
|
||||
"#;
|
||||
pub const IMAP_CONFIG: &str = r#"
|
||||
[accounts.imap]
|
||||
root_mailbox = "INBOX"
|
||||
format = "imap"
|
||||
send_mail = 'false'
|
||||
identity="username@example.com"
|
||||
server_username = "null"
|
||||
server_hostname = "example.com"
|
||||
server_password_command = "false"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn test_conf_config_parse() {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let new_file = ConfigFile::new(TEST_CONFIG, &tempdir).unwrap();
|
||||
let err = FileSettings::validate(new_file.path.clone(), true).unwrap_err();
|
||||
assert_eq!(
|
||||
err.summary.as_ref(),
|
||||
"Configuration error (account-name): root_mailbox `/path/to/root/mailbox` is not a valid \
|
||||
directory."
|
||||
);
|
||||
|
||||
/* Test unrecognised configuration entries error */
|
||||
|
||||
let new_file = ConfigFile::new(EXTRA_CONFIG, &tempdir).unwrap();
|
||||
let err = FileSettings::validate(new_file.path.clone(), true).unwrap_err();
|
||||
assert_eq!(
|
||||
err.summary.as_ref(),
|
||||
"Unrecognised configuration values: {\"index_style\": \"Compact\"}"
|
||||
);
|
||||
|
||||
/* Test IMAP config */
|
||||
|
||||
let new_file = ConfigFile::new(IMAP_CONFIG, &tempdir).unwrap();
|
||||
FileSettings::validate(new_file.path.clone(), true).expect("could not parse IMAP config");
|
||||
|
||||
/* Test sample config */
|
||||
|
||||
// Sample config contains `crate::conf::composing::SendMail::Smtp` variant which
|
||||
// only exists if meli is build with `smtp` feature.
|
||||
if cfg!(feature = "smtp") {
|
||||
let example_config = FileSettings::EXAMPLE_CONFIG.replace("\n#", "\n");
|
||||
let re = regex::Regex::new(r#"root_mailbox\s*=\s*"[^"]*""#).unwrap();
|
||||
let example_config = re.replace_all(
|
||||
&example_config,
|
||||
&format!(r#"root_mailbox = "{}""#, tempdir.path().to_str().unwrap()),
|
||||
);
|
||||
|
||||
let new_file = ConfigFile::new(&example_config, &tempdir).unwrap();
|
||||
let config = FileSettings::validate(new_file.path.clone(), true)
|
||||
.expect("Could not parse example config!");
|
||||
for (accname, acc) in config.accounts.iter() {
|
||||
if !acc.extra.is_empty() {
|
||||
panic!(
|
||||
"In example config, account `{}` has unrecognised configuration entries: {:?}",
|
||||
accname, acc.extra
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = tempdir.close() {
|
||||
eprintln!("Could not cleanup tempdir: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conf_theme_parsing() {
|
||||
/* MUST SUCCEED: default themes should be valid */
|
||||
let def = Themes::default();
|
||||
def.validate().unwrap();
|
||||
/* MUST SUCCEED: new user theme `hunter2`, theme `dark` has user
|
||||
* redefinitions */
|
||||
const TEST_STR: &str = r#"[dark]
|
||||
"mail.listing.tag_default" = { fg = "White", bg = "HotPink3" }
|
||||
"mail.listing.attachment_flag" = { fg = "mail.listing.tag_default.bg" }
|
||||
"mail.view.headers" = { bg = "mail.listing.tag_default.fg" }
|
||||
|
||||
["hunter2"]
|
||||
"mail.view.body" = { fg = "Black", bg = "White"}"#;
|
||||
let parsed: Themes = toml::from_str(TEST_STR).unwrap();
|
||||
assert!(parsed.other_themes.contains_key("hunter2"));
|
||||
assert_eq!(
|
||||
unlink_bg(
|
||||
&parsed.dark,
|
||||
&ColorField::Bg,
|
||||
&Cow::from("mail.listing.tag_default")
|
||||
),
|
||||
Color::Byte(132)
|
||||
);
|
||||
assert_eq!(
|
||||
unlink_fg(
|
||||
&parsed.dark,
|
||||
&ColorField::Fg,
|
||||
&Cow::from("mail.listing.attachment_flag")
|
||||
),
|
||||
Color::Byte(132)
|
||||
);
|
||||
assert_eq!(
|
||||
unlink_bg(
|
||||
&parsed.dark,
|
||||
&ColorField::Bg,
|
||||
&Cow::from("mail.view.headers")
|
||||
),
|
||||
Color::Byte(15), // White
|
||||
);
|
||||
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();
|
||||
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);
|
||||
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();
|
||||
parsed.validate().unwrap();
|
||||
assert_eq!(
|
||||
unlink_fg(
|
||||
&parsed.dark,
|
||||
&ColorField::Fg,
|
||||
&Cow::from("mail.listing.tag_default")
|
||||
),
|
||||
Color::Rgb(180, 218, 85)
|
||||
);
|
||||
/* 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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
parsed.validate().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conf_theme_key_values() {
|
||||
use std::{collections::VecDeque, fs::File, io::Read, path::PathBuf};
|
||||
let mut rust_files: VecDeque<PathBuf> = VecDeque::new();
|
||||
let mut dirs_queue: VecDeque<PathBuf> = VecDeque::new();
|
||||
dirs_queue.push_back("src/".into());
|
||||
let re_whitespace = regex::Regex::new(r"\s*").unwrap();
|
||||
let re_conf = regex::Regex::new(r#"value\([&]?context,"([^"]*)""#).unwrap();
|
||||
|
||||
while let Some(dir) = dirs_queue.pop_front() {
|
||||
for entry in std::fs::read_dir(&dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
dirs_queue.push_back(path);
|
||||
} else if path.extension().map(|os_s| os_s == "rs").unwrap_or(false) {
|
||||
rust_files.push_back(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
for file_path in rust_files {
|
||||
let mut file = File::open(&file_path).unwrap();
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content).unwrap();
|
||||
let content = re_whitespace.replace_all(&content, "");
|
||||
for mat in re_conf.captures_iter(&content) {
|
||||
let theme_key = &mat[1];
|
||||
if !DEFAULT_KEYS.contains(&theme_key) {
|
||||
panic!(
|
||||
"Source file {} contains a hardcoded theme key str, {:?}, that is not \
|
||||
included in the DEFAULT_KEYS table.",
|
||||
file_path.display(),
|
||||
theme_key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conf_progress_spinner_sequence() {
|
||||
use crate::{conf::terminal::ProgressSpinnerSequence, utilities::ProgressSpinner};
|
||||
|
||||
let int_0 = ProgressSpinnerSequence::Integer(5);
|
||||
assert_eq!(
|
||||
toml::Value::try_from(&int_0).unwrap(),
|
||||
toml::Value::try_from(5).unwrap()
|
||||
);
|
||||
|
||||
let frames = ProgressSpinnerSequence::Custom {
|
||||
frames: vec![
|
||||
"⠁".to_string(),
|
||||
"⠂".to_string(),
|
||||
"⠄".to_string(),
|
||||
"⡀".to_string(),
|
||||
"⢀".to_string(),
|
||||
"⠠".to_string(),
|
||||
"⠐".to_string(),
|
||||
"⠈".to_string(),
|
||||
],
|
||||
interval_ms: ProgressSpinner::INTERVAL_MS,
|
||||
};
|
||||
assert_eq!(frames.interval_ms(), ProgressSpinner::INTERVAL_MS);
|
||||
assert_eq!(
|
||||
toml::Value::try_from(&frames).unwrap(),
|
||||
toml::Value::try_from(["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]).unwrap()
|
||||
);
|
||||
let frames = ProgressSpinnerSequence::Custom {
|
||||
frames: vec![
|
||||
"⠁".to_string(),
|
||||
"⠂".to_string(),
|
||||
"⠄".to_string(),
|
||||
"⡀".to_string(),
|
||||
"⢀".to_string(),
|
||||
"⠠".to_string(),
|
||||
"⠐".to_string(),
|
||||
"⠈".to_string(),
|
||||
],
|
||||
interval_ms: ProgressSpinner::INTERVAL_MS + 1,
|
||||
};
|
||||
assert_eq!(
|
||||
toml::Value::try_from(&frames).unwrap(),
|
||||
toml::Value::try_from(indexmap::indexmap! {
|
||||
"frames" => toml::Value::try_from(["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]).unwrap(),
|
||||
"interval_ms" => toml::Value::try_from(ProgressSpinner::INTERVAL_MS + 1).unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
toml::from_str::<ProgressSpinnerSequence>(
|
||||
r#"frames = ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]
|
||||
interval_ms = 51"#
|
||||
)
|
||||
.unwrap(),
|
||||
frames
|
||||
);
|
||||
assert_eq!(
|
||||
toml::from_str::<indexmap::IndexMap<String, ProgressSpinnerSequence>>(
|
||||
r#"sequence = { frames = ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"], interval_ms = 51 }"#
|
||||
)
|
||||
.unwrap(),
|
||||
indexmap::indexmap! {
|
||||
"sequence".to_string() => frames,
|
||||
},
|
||||
);
|
||||
}
|
|
@ -43,19 +43,15 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
|||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
conf::data_types::regex_pattern::{RegexOptions, RegexValue},
|
||||
terminal::{Attr, Color},
|
||||
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() {
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -70,8 +66,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() {
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -86,8 +82,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() {
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -102,8 +98,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() {
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -116,7 +112,7 @@ pub fn attrs(context: &Context, key: &'static str) -> Attr {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn unlink<'k, 't: 'k>(theme: &'t Theme, key: &'k str) -> ThemeAttribute {
|
||||
fn unlink<'k, 't: 'k>(theme: &'t Theme, key: &'k str) -> ThemeAttribute {
|
||||
ThemeAttribute {
|
||||
fg: unlink_fg(theme, &ColorField::Fg, key),
|
||||
bg: unlink_bg(theme, &ColorField::Bg, key),
|
||||
|
@ -125,11 +121,7 @@ pub fn unlink<'k, 't: 'k>(theme: &'t Theme, key: &'k str) -> ThemeAttribute {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn unlink_fg<'k, 't: 'k>(
|
||||
theme: &'t Theme,
|
||||
mut field: &'k ColorField,
|
||||
mut key: &'k str,
|
||||
) -> Color {
|
||||
fn unlink_fg<'k, 't: 'k>(theme: &'t Theme, mut field: &'k ColorField, mut key: &'k str) -> Color {
|
||||
loop {
|
||||
match field {
|
||||
ColorField::LikeSelf | ColorField::Fg => match &theme[key].fg {
|
||||
|
@ -180,11 +172,7 @@ pub fn unlink_fg<'k, 't: 'k>(
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn unlink_bg<'k, 't: 'k>(
|
||||
theme: &'t Theme,
|
||||
mut field: &'k ColorField,
|
||||
mut key: &'k str,
|
||||
) -> Color {
|
||||
fn unlink_bg<'k, 't: 'k>(theme: &'t Theme, mut field: &'k ColorField, mut key: &'k str) -> Color {
|
||||
loop {
|
||||
match field {
|
||||
ColorField::LikeSelf | ColorField::Bg => match &theme[key].bg {
|
||||
|
@ -234,7 +222,7 @@ pub fn unlink_bg<'k, 't: 'k>(
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn unlink_attrs<'k, 't: 'k>(theme: &'t Theme, mut key: &'k str) -> Attr {
|
||||
fn unlink_attrs<'k, 't: 'k>(theme: &'t Theme, mut key: &'k str) -> Attr {
|
||||
loop {
|
||||
match &theme[key].attrs {
|
||||
ThemeValue::Link(ref new_key, ()) => key = new_key,
|
||||
|
@ -256,7 +244,7 @@ pub fn unlink_attrs<'k, 't: 'k>(theme: &'t Theme, mut key: &'k str) -> Attr {
|
|||
}
|
||||
}
|
||||
|
||||
pub const DEFAULT_KEYS: &[&str] = &[
|
||||
const DEFAULT_KEYS: &[&str] = &[
|
||||
"theme_default",
|
||||
"text.normal",
|
||||
"text.unfocused",
|
||||
|
@ -317,7 +305,6 @@ pub const DEFAULT_KEYS: &[&str] = &[
|
|||
"mail.listing.conversations.highlighted",
|
||||
"mail.listing.conversations.selected",
|
||||
"mail.listing.conversations.highlighted_selected",
|
||||
"mail.view.divider",
|
||||
"mail.view.headers",
|
||||
"mail.view.headers_names",
|
||||
"mail.view.headers_area",
|
||||
|
@ -331,7 +318,6 @@ pub 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",
|
||||
];
|
||||
|
@ -359,16 +345,10 @@ pub struct ThemeAttributeInner {
|
|||
|
||||
impl Default for ThemeAttributeInner {
|
||||
fn default() -> Self {
|
||||
Self::inherited("theme_default")
|
||||
}
|
||||
}
|
||||
|
||||
impl ThemeAttributeInner {
|
||||
pub fn inherited(key: &'static str) -> Self {
|
||||
Self {
|
||||
fg: key.into(),
|
||||
bg: key.into(),
|
||||
attrs: key.into(),
|
||||
fg: "theme_default".into(),
|
||||
bg: "theme_default".into(),
|
||||
attrs: "theme_default".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -406,14 +386,8 @@ enum ThemeValue<T: ThemeLink> {
|
|||
}
|
||||
|
||||
impl From<&'static str> for ThemeValue<Color> {
|
||||
fn from(s: &'static str) -> Self {
|
||||
if let Some(stripped) = s.strip_suffix(".fg") {
|
||||
Self::Link(Cow::Borrowed(stripped), ColorField::Fg)
|
||||
} else if let Some(stripped) = s.strip_suffix(".bg") {
|
||||
Self::Link(Cow::Borrowed(stripped), ColorField::Bg)
|
||||
} else {
|
||||
Self::Link(s.into(), ColorField::LikeSelf)
|
||||
}
|
||||
fn from(from: &'static str) -> Self {
|
||||
Self::Link(from.into(), ColorField::LikeSelf)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -534,23 +508,30 @@ pub struct Themes {
|
|||
pub struct Theme {
|
||||
color_aliases: IndexMap<Cow<'static, str>, ThemeValue<Color>>,
|
||||
attr_aliases: IndexMap<Cow<'static, str>, ThemeValue<Attr>>,
|
||||
#[cfg(feature = "regexp")]
|
||||
text_format_regexps: IndexMap<Cow<'static, str>, SmallVec<[TextFormatterSetting; 32]>>,
|
||||
pub keys: IndexMap<Cow<'static, str>, ThemeAttributeInner>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "regexp")]
|
||||
pub use regexp::text_format_regexps;
|
||||
#[cfg(feature = "regexp")]
|
||||
use regexp::*;
|
||||
|
||||
#[cfg(feature = "regexp")]
|
||||
mod regexp {
|
||||
use super::*;
|
||||
use crate::{conf::data_types::regex_pattern::RegexValue, terminal::FormatTag};
|
||||
use crate::terminal::FormatTag;
|
||||
|
||||
pub(super) const DEFAULT_TEXT_FORMATTER_KEYS: &[&str] =
|
||||
&["pager.envelope.body", "listing.from", "listing.subject"];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RegexpWrapper(pub pcre2::bytes::Regex);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct TextFormatterSetting {
|
||||
pub(super) regexp: RegexValue,
|
||||
pub(super) regexp: RegexpWrapper,
|
||||
pub(super) fg: Option<ThemeValue<Color>>,
|
||||
pub(super) bg: Option<ThemeValue<Color>>,
|
||||
pub(super) attrs: Option<ThemeValue<Attr>>,
|
||||
|
@ -559,18 +540,116 @@ mod regexp {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TextFormatter<'r> {
|
||||
pub regexp: &'r RegexValue,
|
||||
pub regexp: &'r RegexpWrapper,
|
||||
pub tag: FormatTag,
|
||||
}
|
||||
|
||||
impl Default for RegexpWrapper {
|
||||
fn default() -> Self {
|
||||
Self(pcre2::bytes::Regex::new("").unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RegexpWrapper {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
std::fmt::Debug::fmt(self.0.as_str(), fmt)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for RegexpWrapper {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.as_str().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RegexpWrapper {}
|
||||
|
||||
impl PartialEq for RegexpWrapper {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.as_str().eq(other.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl RegexpWrapper {
|
||||
pub(super) fn new(
|
||||
pattern: &str,
|
||||
caseless: bool,
|
||||
dotall: bool,
|
||||
extended: bool,
|
||||
multi_line: bool,
|
||||
ucp: bool,
|
||||
jit_if_available: bool,
|
||||
) -> std::result::Result<Self, pcre2::Error> {
|
||||
Ok(Self(
|
||||
pcre2::bytes::RegexBuilder::new()
|
||||
.caseless(caseless)
|
||||
.dotall(dotall)
|
||||
.extended(extended)
|
||||
.multi_line(multi_line)
|
||||
.ucp(ucp)
|
||||
.jit_if_available(jit_if_available)
|
||||
.build(pattern)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn find_iter<'w, 's>(&'w self, s: &'s str) -> FindIter<'w, 's> {
|
||||
FindIter {
|
||||
pcre_iter: self.0.find_iter(s.as_bytes()),
|
||||
char_indices: s.char_indices(),
|
||||
char_offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FindIter<'r, 's> {
|
||||
pcre_iter: pcre2::bytes::Matches<'r, 's>,
|
||||
char_indices: std::str::CharIndices<'s>,
|
||||
char_offset: usize,
|
||||
}
|
||||
|
||||
impl<'r, 's> Iterator for FindIter<'r, 's> {
|
||||
type Item = (usize, usize);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let next_byte_offset = self.pcre_iter.next()?;
|
||||
if next_byte_offset.is_err() {
|
||||
continue;
|
||||
}
|
||||
let next_byte_offset = next_byte_offset.unwrap();
|
||||
|
||||
let mut next_char_index = self.char_indices.next()?;
|
||||
|
||||
while next_byte_offset.start() < next_char_index.0 {
|
||||
self.char_offset += 1;
|
||||
next_char_index = self.char_indices.next()?;
|
||||
}
|
||||
let start = self.char_offset;
|
||||
|
||||
while next_byte_offset.end()
|
||||
> self
|
||||
.char_indices
|
||||
.next()
|
||||
.map(|(v, _)| v)
|
||||
.unwrap_or_else(|| next_byte_offset.end())
|
||||
{
|
||||
self.char_offset += 1;
|
||||
}
|
||||
let end = self.char_offset + 1;
|
||||
|
||||
return Some((start, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn text_format_regexps<'ctx>(
|
||||
context: &'ctx Context,
|
||||
key: &'static str,
|
||||
) -> SmallVec<[TextFormatter<'ctx>; 64]> {
|
||||
let theme = match context.settings.terminal.theme.as_str() {
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -684,6 +763,14 @@ impl<'de> Deserialize<'de> for Themes {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[cfg(feature = "regexp")]
|
||||
const fn false_val() -> bool {
|
||||
false
|
||||
}
|
||||
#[cfg(feature = "regexp")]
|
||||
const fn true_val() -> bool {
|
||||
true
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct ThemesOptions {
|
||||
#[serde(default)]
|
||||
|
@ -699,15 +786,27 @@ impl<'de> Deserialize<'de> for Themes {
|
|||
color_aliases: IndexMap<Cow<'static, str>, ThemeValue<Color>>,
|
||||
#[serde(default)]
|
||||
attr_aliases: IndexMap<Cow<'static, str>, ThemeValue<Attr>>,
|
||||
#[cfg(feature = "regexp")]
|
||||
#[serde(default)]
|
||||
text_format_regexps: IndexMap<Cow<'static, str>, IndexMap<String, RegexpOptions>>,
|
||||
#[serde(flatten, default)]
|
||||
keys: IndexMap<Cow<'static, str>, ThemeAttributeInnerOptions>,
|
||||
}
|
||||
#[cfg(feature = "regexp")]
|
||||
#[derive(Default, Deserialize)]
|
||||
struct RegexpOptions {
|
||||
#[serde(flatten)]
|
||||
o: RegexOptions,
|
||||
#[serde(default = "false_val")]
|
||||
caseless: bool,
|
||||
#[serde(default = "false_val")]
|
||||
dotall: bool,
|
||||
#[serde(default = "false_val")]
|
||||
extended: bool,
|
||||
#[serde(default = "false_val")]
|
||||
multi_line: bool,
|
||||
#[serde(default = "true_val")]
|
||||
ucp: bool,
|
||||
#[serde(default = "false_val")]
|
||||
jit_if_available: bool,
|
||||
#[serde(default)]
|
||||
priority: u8,
|
||||
#[serde(flatten)]
|
||||
|
@ -716,8 +815,6 @@ impl<'de> Deserialize<'de> for Themes {
|
|||
#[derive(Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ThemeAttributeInnerOptions {
|
||||
#[serde(default)]
|
||||
from: Option<Cow<'static, str>>,
|
||||
#[serde(default)]
|
||||
fg: Option<ThemeValue<Color>>,
|
||||
#[serde(default)]
|
||||
|
@ -727,62 +824,163 @@ impl<'de> Deserialize<'de> for Themes {
|
|||
}
|
||||
|
||||
let mut ret = Self::default();
|
||||
let ThemesOptions {
|
||||
light,
|
||||
dark,
|
||||
other_themes,
|
||||
} = <ThemesOptions>::deserialize(deserializer)?;
|
||||
let mut s = <ThemesOptions>::deserialize(deserializer)?;
|
||||
for tk in s.other_themes.keys() {
|
||||
ret.other_themes.insert(tk.clone(), ret.dark.clone());
|
||||
}
|
||||
|
||||
fn construct_theme<'de, D>(
|
||||
name: Cow<'_, str>,
|
||||
theme: &mut Theme,
|
||||
mut s: ThemeOptions,
|
||||
) -> std::result::Result<(), D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
for (k, v) in theme.iter_mut() {
|
||||
if let Some(ThemeAttributeInnerOptions {
|
||||
from,
|
||||
fg,
|
||||
bg,
|
||||
attrs,
|
||||
}) = s.keys.shift_remove(k)
|
||||
{
|
||||
if let Some(att) = fg {
|
||||
v.fg = att;
|
||||
} else if let Some(ref parent) = from {
|
||||
v.fg = ThemeValue::Link(parent.clone(), ColorField::LikeSelf);
|
||||
for (k, v) in ret.light.iter_mut() {
|
||||
if let Some(mut att) = s.light.keys.remove(k) {
|
||||
if let Some(att) = att.fg.take() {
|
||||
v.fg = att;
|
||||
}
|
||||
if let Some(att) = att.bg.take() {
|
||||
v.bg = att;
|
||||
}
|
||||
if let Some(att) = att.attrs.take() {
|
||||
v.attrs = att;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !s.light.keys.is_empty() {
|
||||
return Err(de::Error::custom(format!(
|
||||
"light theme contains unrecognized theme keywords: {}",
|
||||
s.light
|
||||
.keys
|
||||
.keys()
|
||||
.map(|k| k.as_ref())
|
||||
.collect::<SmallVec<[_; 128]>>()
|
||||
.join(", ")
|
||||
)));
|
||||
}
|
||||
ret.light.color_aliases = s.light.color_aliases;
|
||||
ret.light.attr_aliases = s.light.attr_aliases;
|
||||
#[cfg(feature = "regexp")]
|
||||
for (k, v) in s.light.text_format_regexps {
|
||||
let mut acc = SmallVec::new();
|
||||
for (rs, v) in v {
|
||||
match RegexpWrapper::new(
|
||||
&rs,
|
||||
v.caseless,
|
||||
v.dotall,
|
||||
v.extended,
|
||||
v.multi_line,
|
||||
v.ucp,
|
||||
v.jit_if_available,
|
||||
) {
|
||||
Ok(regexp) => {
|
||||
acc.push(TextFormatterSetting {
|
||||
regexp,
|
||||
fg: v.rest.fg,
|
||||
bg: v.rest.bg,
|
||||
attrs: v.rest.attrs,
|
||||
priority: v.priority,
|
||||
});
|
||||
}
|
||||
if let Some(att) = bg {
|
||||
v.bg = att;
|
||||
} else if let Some(ref parent) = from {
|
||||
v.bg = ThemeValue::Link(parent.clone(), ColorField::LikeSelf);
|
||||
}
|
||||
if let Some(att) = attrs {
|
||||
v.attrs = att;
|
||||
} else if let Some(parent) = from {
|
||||
v.attrs = ThemeValue::Link(parent, ());
|
||||
Err(err) => {
|
||||
return Err(de::Error::custom(err.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !s.keys.is_empty() {
|
||||
ret.light.text_format_regexps.insert(k, acc);
|
||||
}
|
||||
for (k, v) in ret.dark.iter_mut() {
|
||||
if let Some(mut att) = s.dark.keys.remove(k) {
|
||||
if let Some(att) = att.fg.take() {
|
||||
v.fg = att;
|
||||
}
|
||||
if let Some(att) = att.bg.take() {
|
||||
v.bg = att;
|
||||
}
|
||||
if let Some(att) = att.attrs.take() {
|
||||
v.attrs = att;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !s.dark.keys.is_empty() {
|
||||
return Err(de::Error::custom(format!(
|
||||
"dark theme contains unrecognized theme keywords: {}",
|
||||
s.dark
|
||||
.keys
|
||||
.keys()
|
||||
.map(|k| k.as_ref())
|
||||
.collect::<SmallVec<[_; 128]>>()
|
||||
.join(", ")
|
||||
)));
|
||||
}
|
||||
ret.dark.color_aliases = s.dark.color_aliases;
|
||||
ret.dark.attr_aliases = s.dark.attr_aliases;
|
||||
#[cfg(feature = "regexp")]
|
||||
for (k, v) in s.dark.text_format_regexps {
|
||||
let mut acc = SmallVec::new();
|
||||
for (rs, v) in v {
|
||||
match RegexpWrapper::new(
|
||||
&rs,
|
||||
v.caseless,
|
||||
v.dotall,
|
||||
v.extended,
|
||||
v.multi_line,
|
||||
v.ucp,
|
||||
v.jit_if_available,
|
||||
) {
|
||||
Ok(regexp) => {
|
||||
acc.push(TextFormatterSetting {
|
||||
regexp,
|
||||
fg: v.rest.fg,
|
||||
bg: v.rest.bg,
|
||||
attrs: v.rest.attrs,
|
||||
priority: v.priority,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(de::Error::custom(err.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
ret.dark.text_format_regexps.insert(k, acc);
|
||||
}
|
||||
for (tk, t) in ret.other_themes.iter_mut() {
|
||||
let mut theme = s.other_themes.remove(tk).unwrap();
|
||||
for (k, v) in t.iter_mut() {
|
||||
if let Some(mut att) = theme.keys.remove(k) {
|
||||
if let Some(att) = att.fg.take() {
|
||||
v.fg = att;
|
||||
}
|
||||
if let Some(att) = att.bg.take() {
|
||||
v.bg = att;
|
||||
}
|
||||
if let Some(att) = att.attrs.take() {
|
||||
v.attrs = att;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !theme.keys.is_empty() {
|
||||
return Err(de::Error::custom(format!(
|
||||
"{} theme contains unrecognized theme keywords: {}",
|
||||
name,
|
||||
s.keys
|
||||
tk,
|
||||
theme
|
||||
.keys
|
||||
.keys()
|
||||
.map(|k| k.as_ref())
|
||||
.collect::<SmallVec<[_; 128]>>()
|
||||
.join(", ")
|
||||
)));
|
||||
}
|
||||
theme.color_aliases = s.color_aliases;
|
||||
theme.attr_aliases = s.attr_aliases;
|
||||
for (k, v) in s.text_format_regexps {
|
||||
t.color_aliases = theme.color_aliases;
|
||||
t.attr_aliases = theme.attr_aliases;
|
||||
#[cfg(feature = "regexp")]
|
||||
for (k, v) in theme.text_format_regexps {
|
||||
let mut acc = SmallVec::new();
|
||||
for (rs, v) in v {
|
||||
match RegexValue::new_with_options(&rs, v.o) {
|
||||
match RegexpWrapper::new(
|
||||
&rs,
|
||||
v.caseless,
|
||||
v.dotall,
|
||||
v.extended,
|
||||
v.multi_line,
|
||||
v.ucp,
|
||||
v.jit_if_available,
|
||||
) {
|
||||
Ok(regexp) => {
|
||||
acc.push(TextFormatterSetting {
|
||||
regexp,
|
||||
|
@ -797,17 +995,8 @@ impl<'de> Deserialize<'de> for Themes {
|
|||
}
|
||||
}
|
||||
}
|
||||
theme.text_format_regexps.insert(k, acc);
|
||||
t.text_format_regexps.insert(k, acc);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
construct_theme::<D>(Cow::Borrowed(self::DARK), &mut ret.dark, dark)?;
|
||||
construct_theme::<D>(Cow::Borrowed(self::LIGHT), &mut ret.light, light)?;
|
||||
for (name, theme_opts) in other_themes {
|
||||
let mut theme = ret.dark.clone();
|
||||
construct_theme::<D>(Cow::Borrowed(&name), &mut theme, theme_opts)?;
|
||||
ret.other_themes.insert(name, theme);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
@ -925,6 +1114,7 @@ impl Themes {
|
|||
}
|
||||
}))
|
||||
.collect::<SmallVec<[(Option<_>, &'_ str, &'_ str, &'_ str); 128]>>();
|
||||
#[cfg(feature = "regexp")]
|
||||
{
|
||||
for (key, v) in &theme.text_format_regexps {
|
||||
if !regexp::DEFAULT_TEXT_FORMATTER_KEYS.contains(&key.as_ref()) {
|
||||
|
@ -1023,8 +1213,8 @@ impl Themes {
|
|||
}
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
let hash_set: HashSet<&'static str> = DEFAULT_KEYS.iter().copied().collect();
|
||||
Self::validate_keys(self::LIGHT, &self.light, &hash_set)?;
|
||||
Self::validate_keys(self::DARK, &self.dark, &hash_set)?;
|
||||
Self::validate_keys("light", &self.light, &hash_set)?;
|
||||
Self::validate_keys("dark", &self.dark, &hash_set)?;
|
||||
for (name, t) in self.other_themes.iter() {
|
||||
Self::validate_keys(name, t, &hash_set)?;
|
||||
}
|
||||
|
@ -1044,8 +1234,8 @@ impl Themes {
|
|||
|
||||
pub fn key_to_string(&self, key: &str, unlink: bool) -> String {
|
||||
let theme = match key {
|
||||
self::LIGHT => &self.light,
|
||||
self::DARK => &self.dark,
|
||||
"light" => &self.light,
|
||||
"dark" => &self.dark,
|
||||
t => self.other_themes.get(t).unwrap_or(&self.dark),
|
||||
};
|
||||
let mut ret = String::new();
|
||||
|
@ -1056,12 +1246,9 @@ impl Themes {
|
|||
ret,
|
||||
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}",
|
||||
k,
|
||||
toml::Value::try_from(unlink_fg(theme, &ColorField::Fg, k))
|
||||
.expect("Could not serialize Color"),
|
||||
toml::Value::try_from(unlink_bg(theme, &ColorField::Bg, k))
|
||||
.expect("Could not serialize Color"),
|
||||
toml::Value::try_from(unlink_attrs(theme, k))
|
||||
.expect("Could not serialize Attribute"),
|
||||
toml::to_string(&unlink_fg(theme, &ColorField::Fg, k)).unwrap(),
|
||||
toml::to_string(&unlink_bg(theme, &ColorField::Bg, k)).unwrap(),
|
||||
toml::to_string(&unlink_attrs(theme, k)).unwrap(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -1070,9 +1257,9 @@ impl Themes {
|
|||
ret,
|
||||
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}",
|
||||
k,
|
||||
toml::Value::try_from(&theme[k].fg).expect("Could not serialize Color"),
|
||||
toml::Value::try_from(&theme[k].bg).expect("Could not serialize Color"),
|
||||
toml::Value::try_from(&theme[k].attrs).expect("Could not serialize Attribute")
|
||||
toml::to_string(&theme[k].fg).unwrap(),
|
||||
toml::to_string(&theme[k].bg).unwrap(),
|
||||
toml::to_string(&theme[k].attrs).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1083,10 +1270,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(self::DARK, true));
|
||||
ret.push_str(&self.key_to_string("dark", true));
|
||||
|
||||
ret.push_str("\n\n");
|
||||
ret.push_str(&self.key_to_string(self::LIGHT, true));
|
||||
ret.push_str(&self.key_to_string("light", true));
|
||||
for name in self.other_themes.keys() {
|
||||
ret.push_str("\n\n");
|
||||
ret.push_str(&self.key_to_string(name, true));
|
||||
|
@ -1103,12 +1290,6 @@ impl Default for Themes {
|
|||
let other_themes = IndexMap::default();
|
||||
|
||||
macro_rules! add {
|
||||
($key:literal from $parent_key:literal, $($theme:ident={ $($name:ident : $val:expr),*$(,)? }),*$(,)?) => {
|
||||
add!($key);
|
||||
$($theme.insert($key.into(), ThemeAttributeInner {
|
||||
$($name: $val.into()),*
|
||||
,..ThemeAttributeInner::inherited($parent_key) }));*
|
||||
};
|
||||
($key:literal, $($theme:ident={ $($name:ident : $val:expr),*$(,)? }),*$(,)?) => {
|
||||
add!($key);
|
||||
$($theme.insert($key.into(), ThemeAttributeInner {
|
||||
|
@ -1135,7 +1316,7 @@ impl Default for Themes {
|
|||
add!("text.highlight", dark = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE }, light = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE });
|
||||
|
||||
/* rest */
|
||||
add!("highlight", dark = { fg: "theme_default.bg", bg: "theme_default.fg", attrs: Attr::BOLD }, light = { fg: Color::Byte(240), bg: Color::Byte(237), attrs: Attr::BOLD });
|
||||
add!("highlight", dark = { fg: Color::Byte(240), bg: Color::Byte(237), attrs: Attr::BOLD }, light = { fg: Color::Byte(240), bg: Color::Byte(237), attrs: Attr::BOLD });
|
||||
|
||||
add!("status.bar", dark = { fg: Color::Byte(123), bg: Color::Byte(26) }, light = { fg: Color::Byte(123), bg: Color::Byte(26) });
|
||||
add!("status.command_bar", dark = { fg: Color::Byte(219), bg: Color::Byte(88) }, light = { fg: Color::Byte(219), bg: Color::Byte(88) });
|
||||
|
@ -1177,11 +1358,11 @@ impl Default for Themes {
|
|||
attrs: Attr::BOLD,
|
||||
}
|
||||
);
|
||||
add!("mail.sidebar_unread_count" from "mail.sidebar", dark = { fg: Color::Byte(243) });
|
||||
add!("mail.sidebar_index" from "mail.sidebar", dark = { fg: Color::Byte(243) });
|
||||
add!("mail.sidebar_highlighted" from "mail.sidebar", dark = { fg: Color::Byte(233), bg: Color::Byte(15) });
|
||||
add!("mail.sidebar_unread_count", dark = { fg: Color::Byte(243) });
|
||||
add!("mail.sidebar_index", dark = { fg: Color::Byte(243) });
|
||||
add!("mail.sidebar_highlighted", dark = { fg: Color::Byte(233), bg: Color::Byte(15) });
|
||||
add!(
|
||||
"mail.sidebar_highlighted_unread_count" from "mail.sidebar_highlighted",
|
||||
"mail.sidebar_highlighted_unread_count",
|
||||
light = {
|
||||
fg: "mail.sidebar_highlighted",
|
||||
bg: "mail.sidebar_highlighted"
|
||||
|
@ -1192,7 +1373,7 @@ impl Default for Themes {
|
|||
}
|
||||
);
|
||||
add!(
|
||||
"mail.sidebar_highlighted_index" from "mail.sidebar_highlighted",
|
||||
"mail.sidebar_highlighted_index",
|
||||
light = {
|
||||
fg: "mail.sidebar_index",
|
||||
bg: "mail.sidebar_highlighted",
|
||||
|
@ -1203,14 +1384,14 @@ impl Default for Themes {
|
|||
},
|
||||
);
|
||||
add!(
|
||||
"mail.sidebar_highlighted_account" from "mail.sidebar_highlighted",
|
||||
"mail.sidebar_highlighted_account",
|
||||
dark = {
|
||||
fg: Color::Byte(15),
|
||||
bg: Color::Byte(233),
|
||||
}
|
||||
);
|
||||
add!(
|
||||
"mail.sidebar_highlighted_account_name" from "mail.sidebar_highlighted",
|
||||
"mail.sidebar_highlighted_account_name",
|
||||
dark = {
|
||||
fg: "mail.sidebar_highlighted_account",
|
||||
bg: "mail.sidebar_highlighted_account",
|
||||
|
@ -1223,7 +1404,7 @@ impl Default for Themes {
|
|||
}
|
||||
);
|
||||
add!(
|
||||
"mail.sidebar_highlighted_account_unread_count" from "mail.sidebar_highlighted",
|
||||
"mail.sidebar_highlighted_account_unread_count",
|
||||
light = {
|
||||
fg: "mail.sidebar_unread_count",
|
||||
bg: "mail.sidebar_highlighted_account",
|
||||
|
@ -1234,7 +1415,7 @@ impl Default for Themes {
|
|||
}
|
||||
);
|
||||
add!(
|
||||
"mail.sidebar_highlighted_account_index" from "mail.sidebar_highlighted",
|
||||
"mail.sidebar_highlighted_account_index",
|
||||
light = {
|
||||
fg: "mail.sidebar_index",
|
||||
bg: "mail.sidebar_highlighted_account"
|
||||
|
@ -1244,7 +1425,6 @@ impl Default for Themes {
|
|||
bg: "mail.sidebar_highlighted_account"
|
||||
}
|
||||
);
|
||||
add!("mail.view.divider");
|
||||
|
||||
/* CompactListing */
|
||||
add!("mail.listing.compact.even",
|
||||
|
@ -1547,15 +1727,6 @@ 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 });
|
||||
|
@ -1564,6 +1735,7 @@ impl Default for Themes {
|
|||
keys: light,
|
||||
attr_aliases: Default::default(),
|
||||
color_aliases: Default::default(),
|
||||
#[cfg(feature = "regexp")]
|
||||
text_format_regexps: DEFAULT_TEXT_FORMATTER_KEYS
|
||||
.iter()
|
||||
.map(|&k| (k.into(), SmallVec::new()))
|
||||
|
@ -1573,6 +1745,7 @@ impl Default for Themes {
|
|||
keys: dark,
|
||||
attr_aliases: Default::default(),
|
||||
color_aliases: Default::default(),
|
||||
#[cfg(feature = "regexp")]
|
||||
text_format_regexps: DEFAULT_TEXT_FORMATTER_KEYS
|
||||
.iter()
|
||||
.map(|&k| (k.into(), SmallVec::new()))
|
||||
|
@ -1630,14 +1803,14 @@ impl Serialize for Themes {
|
|||
other_themes.insert(name.to_string(), new_map);
|
||||
}
|
||||
|
||||
other_themes.insert(self::LIGHT.to_string(), light);
|
||||
other_themes.insert(self::DARK.to_string(), dark);
|
||||
other_themes.insert("light".to_string(), light);
|
||||
other_themes.insert("dark".to_string(), dark);
|
||||
other_themes.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/* Check Theme linked values for cycles */
|
||||
pub fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||
fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
enum Course {
|
||||
Fg,
|
||||
|
@ -1903,3 +2076,155 @@ pub fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_parsing() {
|
||||
/* MUST SUCCEED: default themes should be valid */
|
||||
let def = Themes::default();
|
||||
def.validate().unwrap();
|
||||
/* MUST SUCCEED: new user theme `hunter2`, theme `dark` has user
|
||||
* redefinitions */
|
||||
const TEST_STR: &str = r#"[dark]
|
||||
"mail.listing.tag_default" = { fg = "White", bg = "HotPink3" }
|
||||
"mail.listing.attachment_flag" = { fg = "mail.listing.tag_default.bg" }
|
||||
"mail.view.headers" = { bg = "mail.listing.tag_default.fg" }
|
||||
|
||||
["hunter2"]
|
||||
"mail.view.body" = { fg = "Black", bg = "White"}"#;
|
||||
let parsed: Themes = toml::from_str(TEST_STR).unwrap();
|
||||
assert!(parsed.other_themes.contains_key("hunter2"));
|
||||
assert_eq!(
|
||||
unlink_bg(
|
||||
&parsed.dark,
|
||||
&ColorField::Bg,
|
||||
&Cow::from("mail.listing.tag_default")
|
||||
),
|
||||
Color::Byte(132)
|
||||
);
|
||||
assert_eq!(
|
||||
unlink_fg(
|
||||
&parsed.dark,
|
||||
&ColorField::Fg,
|
||||
&Cow::from("mail.listing.attachment_flag")
|
||||
),
|
||||
Color::Byte(132)
|
||||
);
|
||||
assert_eq!(
|
||||
unlink_bg(
|
||||
&parsed.dark,
|
||||
&ColorField::Bg,
|
||||
&Cow::from("mail.view.headers")
|
||||
),
|
||||
Color::Byte(15), // White
|
||||
);
|
||||
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();
|
||||
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);
|
||||
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();
|
||||
parsed.validate().unwrap();
|
||||
assert_eq!(
|
||||
unlink_fg(
|
||||
&parsed.dark,
|
||||
&ColorField::Fg,
|
||||
&Cow::from("mail.listing.tag_default")
|
||||
),
|
||||
Color::Rgb(180, 218, 85)
|
||||
);
|
||||
/* 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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
parsed.validate().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_key_values() {
|
||||
use std::{collections::VecDeque, fs::File, io::Read, path::PathBuf};
|
||||
let mut rust_files: VecDeque<PathBuf> = VecDeque::new();
|
||||
let mut dirs_queue: VecDeque<PathBuf> = VecDeque::new();
|
||||
dirs_queue.push_back("src/".into());
|
||||
let re_whitespace = regex::Regex::new(r"\s*").unwrap();
|
||||
let re_conf = regex::Regex::new(r#"value\([&]?context,"([^"]*)""#).unwrap();
|
||||
|
||||
while let Some(dir) = dirs_queue.pop_front() {
|
||||
for entry in std::fs::read_dir(&dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
dirs_queue.push_back(path);
|
||||
} else if path.extension().map(|os_s| os_s == "rs").unwrap_or(false) {
|
||||
rust_files.push_back(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
for file_path in rust_files {
|
||||
let mut file = File::open(&file_path).unwrap();
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content).unwrap();
|
||||
let content = re_whitespace.replace_all(&content, "");
|
||||
for mat in re_conf.captures_iter(&content) {
|
||||
let theme_key = &mat[1];
|
||||
if !DEFAULT_KEYS.contains(&theme_key) {
|
||||
panic!(
|
||||
"Source file {} contains a hardcoded theme key str, {:?}, that is not \
|
||||
included in the DEFAULT_KEYS table.",
|
||||
file_path.display(),
|
||||
theme_key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,16 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::Card;
|
||||
|
||||
use crate::{
|
||||
terminal::*,
|
||||
utilities::{FormButtonAction, FormWidget},
|
||||
CellBuffer, Component, ComponentId, Context, Field, Key, StatusEvent, ThemeAttribute, UIDialog,
|
||||
UIEvent,
|
||||
terminal::*, CellBuffer, Component, ComponentId, Context, Field, FormWidget, Key, StatusEvent,
|
||||
ThemeAttribute, UIDialog, UIEvent,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -44,7 +40,7 @@ pub struct ContactManager {
|
|||
parent_id: Option<ComponentId>,
|
||||
pub card: Card,
|
||||
mode: ViewMode,
|
||||
form: FormWidget<FormButtonAction>,
|
||||
form: FormWidget<bool>,
|
||||
pub account_pos: usize,
|
||||
content: Screen<Virtual>,
|
||||
theme_default: ThemeAttribute,
|
||||
|
@ -69,7 +65,7 @@ impl ContactManager {
|
|||
mode: ViewMode::Edit,
|
||||
form: FormWidget::default(),
|
||||
account_pos: 0,
|
||||
content: Screen::<Virtual>::new(theme_default),
|
||||
content: Screen::<Virtual>::new(),
|
||||
theme_default,
|
||||
dirty: true,
|
||||
has_changes: false,
|
||||
|
@ -81,7 +77,26 @@ impl ContactManager {
|
|||
if !self.content.resize_with_context(100, 1, context) {
|
||||
return;
|
||||
}
|
||||
let area = self.content.area();
|
||||
let mut area = self.content.area();
|
||||
|
||||
let (x, _) = self.content.grid_mut().write_string(
|
||||
"Last edited: ",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
area = area.skip_cols(x);
|
||||
let (x, y) = self.content.grid_mut().write_string(
|
||||
&self.card.last_edited(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
area = area.skip(x, y);
|
||||
|
||||
if self.card.external_resource() {
|
||||
self.mode = ViewMode::ReadOnly;
|
||||
|
@ -92,45 +107,16 @@ impl ContactManager {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
let (x, _) = self.content.grid_mut().write_string(
|
||||
"Last edited: ",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
self.content.grid_mut().write_string(
|
||||
&self.card.last_edited(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area.skip_cols(x),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
self.form = FormWidget::new(
|
||||
if self.card.external_resource() {
|
||||
("Cancel(Esc)".into(), FormButtonAction::Cancel)
|
||||
} else {
|
||||
("Save".into(), FormButtonAction::Accept)
|
||||
},
|
||||
("Save".into(), true),
|
||||
/* cursor_up_shortcut */ context.settings.shortcuts.general.scroll_up.clone(),
|
||||
/* cursor_down_shortcut */
|
||||
context.settings.shortcuts.general.scroll_down.clone(),
|
||||
);
|
||||
if !self.card.external_resource() {
|
||||
self.form
|
||||
.add_button(("Export".into(), FormButtonAction::Other("Export")));
|
||||
self.form
|
||||
.add_button(("Cancel(Esc)".into(), FormButtonAction::Cancel));
|
||||
}
|
||||
self.form.add_button(("Cancel(Esc)".into(), false));
|
||||
self.form
|
||||
.push(("NAME".into(), self.card.name().to_string()));
|
||||
self.form.push((
|
||||
|
@ -164,12 +150,15 @@ impl Component for ContactManager {
|
|||
|
||||
if self.is_dirty() {
|
||||
grid.clear_area(area, self.theme_default);
|
||||
grid.copy_area(self.content.grid(), area, self.content.area());
|
||||
grid.copy_area(self.content.grid(), area.skip_rows(2), self.content.area());
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
self.form
|
||||
.draw(grid, area.skip_rows(self.content.area().height()), context);
|
||||
self.form.draw(
|
||||
grid,
|
||||
area.skip_rows(2 + self.content.area().height()),
|
||||
context,
|
||||
);
|
||||
if let ViewMode::Discard(ref mut selector) = self.mode {
|
||||
/* Let user choose whether to quit with/without saving or cancel */
|
||||
selector.draw(grid, area, context);
|
||||
|
@ -208,8 +197,8 @@ impl Component for ContactManager {
|
|||
if self.form.process_event(event, context) {
|
||||
match self.form.buttons_result() {
|
||||
None => {}
|
||||
Some(FormButtonAction::Accept) => {
|
||||
let fields = std::mem::take(&mut self.form).collect();
|
||||
Some(true) => {
|
||||
let fields = std::mem::take(&mut self.form).collect().unwrap();
|
||||
let fields: IndexMap<String, String> = fields
|
||||
.into_iter()
|
||||
.map(|(s, v)| {
|
||||
|
@ -225,43 +214,16 @@ impl Component for ContactManager {
|
|||
let mut new_card = Card::from(fields);
|
||||
new_card.set_id(*self.card.id());
|
||||
context.accounts[self.account_pos]
|
||||
.contacts
|
||||
.address_book
|
||||
.add_card(new_card);
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage("Saved.".into()),
|
||||
));
|
||||
self.unrealize(context);
|
||||
}
|
||||
Some(FormButtonAction::Cancel) => {
|
||||
Some(false) => {
|
||||
self.unrealize(context);
|
||||
}
|
||||
Some(FormButtonAction::Other("Export")) => {
|
||||
let card = if self.has_changes {
|
||||
let fields = self.form.clone().collect();
|
||||
let fields: IndexMap<String, String> = fields
|
||||
.into_iter()
|
||||
.map(|(s, v)| {
|
||||
(
|
||||
s.to_string(),
|
||||
match v {
|
||||
Field::Text(v) => v.as_str().to_string(),
|
||||
Field::Choice(mut v, c, _) => {
|
||||
v.remove(c).to_string()
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut card = Card::from(fields);
|
||||
card.set_id(*self.card.id());
|
||||
Cow::Owned(card)
|
||||
} else {
|
||||
Cow::Borrowed(&self.card)
|
||||
};
|
||||
super::export_to_vcard(&card, self.account_pos, context);
|
||||
return true;
|
||||
}
|
||||
Some(FormButtonAction::Reset | FormButtonAction::Other(_)) => {}
|
||||
}
|
||||
self.set_dirty(true);
|
||||
if matches!(event, UIEvent::InsertInput(_)) {
|
||||
|
|
|
@ -88,7 +88,6 @@ impl ContactList {
|
|||
index: i,
|
||||
})
|
||||
.collect();
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
Self {
|
||||
accounts,
|
||||
cursor_pos: 0,
|
||||
|
@ -97,8 +96,8 @@ impl ContactList {
|
|||
account_pos: 0,
|
||||
id_positions: Vec::new(),
|
||||
mode: ViewMode::List,
|
||||
data_columns: DataColumns::new(theme_default),
|
||||
theme_default,
|
||||
data_columns: DataColumns::default(),
|
||||
theme_default: crate::conf::value(context, "theme_default"),
|
||||
highlight_theme: crate::conf::value(context, "highlight"),
|
||||
initialized: false,
|
||||
dirty: true,
|
||||
|
@ -122,17 +121,17 @@ impl ContactList {
|
|||
fn initialize(&mut self, context: &Context) {
|
||||
self.data_columns.clear();
|
||||
let account = &context.accounts[self.account_pos];
|
||||
let contacts = &account.contacts;
|
||||
self.length = contacts.len();
|
||||
let book = &account.address_book;
|
||||
self.length = book.len();
|
||||
|
||||
self.id_positions.clear();
|
||||
if self.id_positions.capacity() < contacts.len() {
|
||||
self.id_positions.reserve(contacts.len());
|
||||
if self.id_positions.capacity() < book.len() {
|
||||
self.id_positions.reserve(book.len());
|
||||
}
|
||||
self.dirty = true;
|
||||
let mut min_width = ("Name".len(), "E-mail".len(), 0, "external".len(), 0, 0);
|
||||
|
||||
for c in contacts.values() {
|
||||
for c in book.values() {
|
||||
/* name */
|
||||
let name = c.name().split_graphemes().len();
|
||||
if name > 0 {
|
||||
|
@ -164,8 +163,8 @@ impl ContactList {
|
|||
);
|
||||
|
||||
let account = &context.accounts[self.account_pos];
|
||||
let contacts = &account.contacts;
|
||||
let mut book_values = contacts.values().collect::<Vec<&Card>>();
|
||||
let book = &account.address_book;
|
||||
let mut book_values = book.values().collect::<Vec<&Card>>();
|
||||
book_values.sort_unstable_by_key(|c| c.name());
|
||||
for (idx, c) in book_values.iter().enumerate() {
|
||||
self.id_positions.push(*c.id());
|
||||
|
@ -179,7 +178,6 @@ impl ContactList {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -192,7 +190,6 @@ impl ContactList {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -205,7 +202,6 @@ impl ContactList {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -222,7 +218,6 @@ impl ContactList {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
@ -239,19 +234,19 @@ impl ContactList {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize) {
|
||||
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize) {
|
||||
/* Reset previously highlighted line */
|
||||
let mut theme = if idx == self.new_cursor_pos {
|
||||
self.highlight_theme
|
||||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
theme.fg = self.theme_default.fg;
|
||||
if !grid.use_color {
|
||||
theme.attrs |= Attr::REVERSE;
|
||||
}
|
||||
|
@ -294,7 +289,7 @@ impl ContactList {
|
|||
};
|
||||
|
||||
grid.change_theme(area, account_attrs);
|
||||
let s = format!(" [{}]", context.accounts[a.index].contacts.len());
|
||||
let s = format!(" [{}]", context.accounts[a.index].address_book.len());
|
||||
/* Print account name */
|
||||
grid.write_string(
|
||||
&a.name,
|
||||
|
@ -303,7 +298,6 @@ impl ContactList {
|
|||
account_attrs.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
grid.write_string(
|
||||
&s,
|
||||
|
@ -312,7 +306,6 @@ impl ContactList {
|
|||
account_attrs.attrs,
|
||||
area.skip_cols(area.width().saturating_sub(s.len())),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
if a.name.grapheme_len() + s.len() > width + 1 {
|
||||
|
@ -323,7 +316,6 @@ impl ContactList {
|
|||
account_attrs.attrs,
|
||||
area.skip_cols(area.width().saturating_sub(s.len() + 1)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -457,7 +449,6 @@ impl ContactList {
|
|||
.skip_cols(x)
|
||||
.take_cols(x + (self.data_columns.widths[i])),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
x += self.data_columns.widths[i] + 2; // + SEPARATOR
|
||||
|
@ -582,6 +573,7 @@ impl Component for ContactList {
|
|||
)));
|
||||
return true;
|
||||
}
|
||||
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["edit_contact"]) =>
|
||||
{
|
||||
|
@ -589,8 +581,8 @@ impl Component for ContactList {
|
|||
return true;
|
||||
}
|
||||
let account = &mut context.accounts[self.account_pos];
|
||||
let contacts = &mut account.contacts;
|
||||
let card = contacts[&self.id_positions[self.cursor_pos]].clone();
|
||||
let book = &mut account.address_book;
|
||||
let card = book[&self.id_positions[self.cursor_pos]].clone();
|
||||
let mut manager = Box::new(ContactManager::new(context));
|
||||
manager.set_parent_id(self.id);
|
||||
manager.card = card;
|
||||
|
@ -604,20 +596,6 @@ impl Component for ContactList {
|
|||
)));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["export_contact"]) =>
|
||||
{
|
||||
if self.length == 0 {
|
||||
return true;
|
||||
}
|
||||
let card = {
|
||||
let account = &context.accounts[self.account_pos];
|
||||
let contacts = &account.contacts;
|
||||
contacts[&self.id_positions[self.cursor_pos]].clone()
|
||||
};
|
||||
super::export_to_vcard(&card, self.account_pos, context);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["mail_contact"]) =>
|
||||
{
|
||||
|
@ -626,8 +604,8 @@ impl Component for ContactList {
|
|||
}
|
||||
let account = &context.accounts[self.account_pos];
|
||||
let account_hash = account.hash();
|
||||
let contacts = &account.contacts;
|
||||
let card = &contacts[&self.id_positions[self.cursor_pos]];
|
||||
let book = &account.address_book;
|
||||
let card = &book[&self.id_positions[self.cursor_pos]];
|
||||
let mut draft: Draft = Draft::default();
|
||||
*draft.headers_mut().get_mut("To").unwrap() =
|
||||
format!("{} <{}>", &card.name(), &card.email());
|
||||
|
@ -649,7 +627,7 @@ impl Component for ContactList {
|
|||
}
|
||||
// [ref:TODO]: add a confirmation dialog?
|
||||
context.accounts[self.account_pos]
|
||||
.contacts
|
||||
.address_book
|
||||
.remove_card(self.id_positions[self.cursor_pos]);
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
|
@ -737,7 +715,7 @@ impl Component for ContactList {
|
|||
self.menu_visibility = !self.menu_visibility;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('\x1b'))
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
||||
if !self.cmd_buf.is_empty() =>
|
||||
{
|
||||
self.cmd_buf.clear();
|
||||
|
@ -936,7 +914,7 @@ impl Component for ContactList {
|
|||
fn status(&self, context: &Context) -> String {
|
||||
format!(
|
||||
"{} entries",
|
||||
context.accounts[self.account_pos].contacts.len()
|
||||
context.accounts[self.account_pos].address_book.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,57 +19,5 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use melib::Card;
|
||||
|
||||
use crate::{
|
||||
types::{sanitize_filename, File, NotificationType, UIEvent},
|
||||
Context,
|
||||
};
|
||||
|
||||
pub mod editor;
|
||||
pub mod list;
|
||||
|
||||
pub fn export_to_vcard(card: &Card, account_pos: usize, context: &mut Context) {
|
||||
let mut output_dir = context.accounts[account_pos]
|
||||
.settings
|
||||
.account
|
||||
.vcard_folder()
|
||||
.map(|s| std::path::Path::new(s).to_path_buf());
|
||||
let filename = sanitize_filename(format!(
|
||||
"{prefix}{name}{suffix}{space}{additionalname}",
|
||||
prefix = card.name_prefix(),
|
||||
name = card.name(),
|
||||
suffix = card.name_suffix(),
|
||||
space = if card.additionalname().trim().is_empty() {
|
||||
""
|
||||
} else {
|
||||
" "
|
||||
},
|
||||
additionalname = card.additionalname()
|
||||
));
|
||||
let res = File::create_temp_file(
|
||||
card.to_vcard_string().as_bytes(),
|
||||
filename.as_deref(),
|
||||
output_dir.as_mut(),
|
||||
Some("vcf"),
|
||||
false,
|
||||
);
|
||||
match res {
|
||||
Ok(f) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Exported .vcf".into()),
|
||||
body: format!("Exported contact to vcard file to\n{}", f.path().display()).into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
source: None,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not export contact.".into()),
|
||||
body: err.to_string().into(),
|
||||
kind: Some(NotificationType::Error(err.kind)),
|
||||
source: Some(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
274
meli/src/jobs.rs
274
meli/src/jobs.rs
|
@ -26,10 +26,7 @@ use std::{
|
|||
future::Future,
|
||||
iter,
|
||||
panic::catch_unwind,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
@ -45,44 +42,8 @@ use melib::{log, smol, utils::datetime, uuid::Uuid, UnixTimestamp};
|
|||
|
||||
use crate::types::{StatusEvent, ThreadEvent, UIEvent};
|
||||
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub enum IsAsync {
|
||||
Async,
|
||||
Blocking,
|
||||
}
|
||||
|
||||
type AsyncTask = async_task::Runnable;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FinishedTimestamp(Arc<Mutex<UnixTimestamp>>);
|
||||
|
||||
impl FinishedTimestamp {
|
||||
fn finished(&self) -> Option<UnixTimestamp> {
|
||||
match self.0.lock() {
|
||||
Ok(v) if *v == 0 => None,
|
||||
Ok(v) => Some(*v),
|
||||
Err(poison) => {
|
||||
let mut guard = poison.into_inner();
|
||||
if *guard == 0 {
|
||||
*guard = UnixTimestamp::default();
|
||||
}
|
||||
Some(*guard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_finished(&self, new_value: Option<UnixTimestamp>) {
|
||||
let new_value = new_value.unwrap_or_default();
|
||||
match self.0.lock() {
|
||||
Ok(mut f) => *f = new_value,
|
||||
Err(poison) => {
|
||||
let mut guard = poison.into_inner();
|
||||
*guard = new_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_task(
|
||||
local: &Worker<MeliTask>,
|
||||
global: &Injector<MeliTask>,
|
||||
|
@ -152,38 +113,12 @@ pub struct MeliTask {
|
|||
#[derive(Clone, Debug)]
|
||||
/// A spawned future's metadata for book-keeping.
|
||||
pub struct JobMetadata {
|
||||
id: JobId,
|
||||
desc: Cow<'static, str>,
|
||||
timer: bool,
|
||||
started: UnixTimestamp,
|
||||
finished: FinishedTimestamp,
|
||||
succeeded: bool,
|
||||
}
|
||||
|
||||
impl JobMetadata {
|
||||
pub fn id(&self) -> &JobId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
&self.desc
|
||||
}
|
||||
|
||||
pub fn is_timer(&self) -> bool {
|
||||
self.timer
|
||||
}
|
||||
|
||||
pub fn started(&self) -> UnixTimestamp {
|
||||
self.started
|
||||
}
|
||||
|
||||
pub fn finished(&self) -> Option<UnixTimestamp> {
|
||||
self.finished.finished()
|
||||
}
|
||||
|
||||
pub fn succeeded(&self) -> bool {
|
||||
self.succeeded
|
||||
}
|
||||
pub id: JobId,
|
||||
pub desc: Cow<'static, str>,
|
||||
pub timer: bool,
|
||||
pub started: UnixTimestamp,
|
||||
pub finished: Option<UnixTimestamp>,
|
||||
pub succeeded: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -204,7 +139,7 @@ struct TimerPrivate {
|
|||
value: Duration,
|
||||
active: bool,
|
||||
handle: Option<async_task::Task<()>>,
|
||||
cancel: Arc<AtomicBool>,
|
||||
cancel: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -303,26 +238,7 @@ impl JobExecutor {
|
|||
|
||||
/// Spawns a future with a generic return value `R`
|
||||
#[inline(always)]
|
||||
pub fn spawn<F, R>(
|
||||
&self,
|
||||
desc: Cow<'static, str>,
|
||||
future: F,
|
||||
is_async: IsAsync,
|
||||
) -> JoinHandle<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
if matches!(is_async, IsAsync::Async) {
|
||||
self.spawn_specialized(desc, future)
|
||||
} else {
|
||||
self.spawn_blocking(desc, future)
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a future with a generic return value `R`
|
||||
#[inline(always)]
|
||||
fn spawn_specialized<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
pub fn spawn_specialized<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
R: Send + 'static,
|
||||
|
@ -331,10 +247,8 @@ impl JobExecutor {
|
|||
let finished_sender = self.sender.clone();
|
||||
let job_id = JobId::new();
|
||||
let injector = self.global_queue.clone();
|
||||
// We do not use `AtomicU64` because it's not portable, so ignore the lint.
|
||||
#[allow(clippy::mutex_integer)]
|
||||
let finished = FinishedTimestamp(Arc::new(Mutex::new(0)));
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
let cancel = Arc::new(Mutex::new(false));
|
||||
let cancel2 = cancel.clone();
|
||||
|
||||
self.jobs.lock().unwrap().insert(
|
||||
job_id,
|
||||
|
@ -342,41 +256,34 @@ impl JobExecutor {
|
|||
id: job_id,
|
||||
desc: desc.clone(),
|
||||
started: datetime::now(),
|
||||
finished: finished.clone(),
|
||||
finished: None,
|
||||
succeeded: true,
|
||||
timer: false,
|
||||
},
|
||||
);
|
||||
|
||||
// Create a task and schedule it for execution.
|
||||
let (handle, task) = {
|
||||
let cancel = cancel.clone();
|
||||
let finished = finished.clone();
|
||||
async_task::spawn(
|
||||
async move {
|
||||
let res = future.await;
|
||||
let _ = sender.send(res);
|
||||
if let Ok(mut guard) = finished.0.lock() {
|
||||
*guard = datetime::now();
|
||||
}
|
||||
finished_sender
|
||||
.send(ThreadEvent::JobFinished(job_id))
|
||||
.unwrap();
|
||||
},
|
||||
move |task| {
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
let desc = desc.clone();
|
||||
injector.push(MeliTask {
|
||||
task,
|
||||
id: job_id,
|
||||
desc,
|
||||
timer: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
};
|
||||
let (handle, task) = async_task::spawn(
|
||||
async move {
|
||||
let res = future.await;
|
||||
let _ = sender.send(res);
|
||||
finished_sender
|
||||
.send(ThreadEvent::JobFinished(job_id))
|
||||
.unwrap();
|
||||
},
|
||||
move |task| {
|
||||
if *cancel.lock().unwrap() {
|
||||
return;
|
||||
}
|
||||
let desc = desc.clone();
|
||||
injector.push(MeliTask {
|
||||
task,
|
||||
id: job_id,
|
||||
desc,
|
||||
timer: false,
|
||||
})
|
||||
},
|
||||
);
|
||||
handle.schedule();
|
||||
for unparker in self.parkers.iter() {
|
||||
unparker.unpark();
|
||||
|
@ -384,8 +291,7 @@ impl JobExecutor {
|
|||
|
||||
JoinHandle {
|
||||
task: Arc::new(Mutex::new(Some(task))),
|
||||
cancel,
|
||||
finished,
|
||||
cancel: cancel2,
|
||||
chan: receiver,
|
||||
job_id,
|
||||
}
|
||||
|
@ -394,7 +300,7 @@ impl JobExecutor {
|
|||
/// Spawns a future with a generic return value `R` that might block on a
|
||||
/// new thread
|
||||
#[inline(always)]
|
||||
fn spawn_blocking<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
pub fn spawn_blocking<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
R: Send + 'static,
|
||||
|
@ -408,7 +314,7 @@ impl JobExecutor {
|
|||
pub fn create_timer(self: Arc<Self>, interval: Duration, value: Duration) -> Timer {
|
||||
let timer = TimerPrivate {
|
||||
interval,
|
||||
cancel: Arc::new(AtomicBool::new(false)),
|
||||
cancel: Arc::new(Mutex::new(false)),
|
||||
value,
|
||||
active: true,
|
||||
handle: None,
|
||||
|
@ -436,48 +342,46 @@ impl JobExecutor {
|
|||
let sender = self.sender.clone();
|
||||
let injector = self.global_queue.clone();
|
||||
let timers = self.timers.clone();
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
let (task, handle) = {
|
||||
let cancel = cancel.clone();
|
||||
async_task::spawn(
|
||||
async move {
|
||||
let mut value = value;
|
||||
loop {
|
||||
smol::Timer::after(value).await;
|
||||
sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Timer(id)))
|
||||
.unwrap();
|
||||
if let Some(interval) = timers.lock().unwrap().get(&id).and_then(|timer| {
|
||||
if timer.interval.as_millis() == 0 && timer.interval.as_secs() == 0 {
|
||||
None
|
||||
} else if timer.active {
|
||||
Some(timer.interval)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
value = interval;
|
||||
let cancel = Arc::new(Mutex::new(false));
|
||||
let cancel2 = cancel.clone();
|
||||
let (task, handle) = async_task::spawn(
|
||||
async move {
|
||||
let mut value = value;
|
||||
loop {
|
||||
smol::Timer::after(value).await;
|
||||
sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Timer(id)))
|
||||
.unwrap();
|
||||
if let Some(interval) = timers.lock().unwrap().get(&id).and_then(|timer| {
|
||||
if timer.interval.as_millis() == 0 && timer.interval.as_secs() == 0 {
|
||||
None
|
||||
} else if timer.active {
|
||||
Some(timer.interval)
|
||||
} else {
|
||||
break;
|
||||
None
|
||||
}
|
||||
}) {
|
||||
value = interval;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
move |task| {
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
injector.push(MeliTask {
|
||||
task,
|
||||
id: job_id,
|
||||
desc: Cow::Borrowed("timer"),
|
||||
timer: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
},
|
||||
move |task| {
|
||||
if *cancel.lock().unwrap() {
|
||||
return;
|
||||
}
|
||||
injector.push(MeliTask {
|
||||
task,
|
||||
id: job_id,
|
||||
desc: "timer".into(),
|
||||
timer: true,
|
||||
})
|
||||
},
|
||||
);
|
||||
self.timers.lock().unwrap().entry(id).and_modify(|timer| {
|
||||
timer.handle = Some(handle);
|
||||
timer.cancel = cancel;
|
||||
timer.cancel = cancel2;
|
||||
timer.active = true;
|
||||
});
|
||||
task.schedule();
|
||||
|
@ -490,7 +394,7 @@ impl JobExecutor {
|
|||
let mut timers_lck = self.timers.lock().unwrap();
|
||||
if let Some(timer) = timers_lck.get_mut(&id) {
|
||||
timer.active = false;
|
||||
timer.cancel.store(true, Ordering::SeqCst);
|
||||
*timer.cancel.lock().unwrap() = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -503,7 +407,7 @@ impl JobExecutor {
|
|||
|
||||
pub fn set_job_finished(&self, id: JobId) {
|
||||
self.jobs.lock().unwrap().entry(id).and_modify(|entry| {
|
||||
entry.finished.set_finished(Some(datetime::now()));
|
||||
entry.finished = Some(datetime::now());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -521,29 +425,20 @@ pub type JobChannel<T> = oneshot::Receiver<T>;
|
|||
pub struct JoinHandle<T> {
|
||||
pub task: Arc<Mutex<Option<async_task::Task<()>>>>,
|
||||
pub chan: JobChannel<T>,
|
||||
pub cancel: Arc<AtomicBool>,
|
||||
finished: FinishedTimestamp,
|
||||
pub cancel: Arc<Mutex<bool>>,
|
||||
pub job_id: JobId,
|
||||
}
|
||||
|
||||
impl<T> JoinHandle<T> {
|
||||
pub fn cancel(&self) -> Option<StatusEvent> {
|
||||
let was_active = self.cancel.swap(true, Ordering::SeqCst);
|
||||
if was_active {
|
||||
self.finished.set_finished(Some(datetime::now()));
|
||||
let mut lck = self.cancel.lock().unwrap();
|
||||
if !*lck {
|
||||
*lck = true;
|
||||
Some(StatusEvent::JobCanceled(self.job_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finished(&self) -> Option<UnixTimestamp> {
|
||||
self.finished.finished()
|
||||
}
|
||||
|
||||
pub fn is_canceled(&self) -> bool {
|
||||
self.cancel.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::cmp::PartialEq<JobId> for JoinHandle<T> {
|
||||
|
@ -552,8 +447,17 @@ impl<T> std::cmp::PartialEq<JobId> for JoinHandle<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for JoinHandle<T> {
|
||||
fn drop(&mut self) {
|
||||
_ = self.cancel();
|
||||
/*
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
impl Future for JoinHandle {
|
||||
type Output = Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match Pin::new(&mut self.inner).poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(output) => Poll::Ready(output.expect("task failed")),
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -19,9 +19,18 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use datetime::formats::RFC3339_DATETIME_AND_SPACE;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::components::prelude::*;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
jobs::{JobId, JobMetadata},
|
||||
melib::{
|
||||
utils::datetime::{self, formats::RFC3339_DATETIME_AND_SPACE},
|
||||
SortOrder,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
|
@ -81,7 +90,7 @@ impl JobManager {
|
|||
..ThemeAttribute::default()
|
||||
}
|
||||
};
|
||||
let mut data_columns = DataColumns::new(theme_default);
|
||||
let mut data_columns = DataColumns::default();
|
||||
data_columns.theme_config.set_single_theme(theme_default);
|
||||
Self {
|
||||
cursor_pos: 0,
|
||||
|
@ -108,23 +117,23 @@ impl JobManager {
|
|||
|
||||
self.length = entries.len();
|
||||
entries.sort_by(|_, a, _, b| match (self.sort_col, self.sort_order) {
|
||||
(Column::_0, SortOrder::Asc) => a.id().cmp(b.id()),
|
||||
(Column::_0, SortOrder::Desc) => b.id().cmp(b.id()),
|
||||
(Column::_1, SortOrder::Asc) => a.description().cmp(b.description()),
|
||||
(Column::_1, SortOrder::Desc) => b.description().cmp(a.description()),
|
||||
(Column::_2, SortOrder::Asc) => a.started().cmp(&b.started()),
|
||||
(Column::_2, SortOrder::Desc) => b.started().cmp(&a.started()),
|
||||
(Column::_3, SortOrder::Asc) => a.finished().cmp(&b.finished()),
|
||||
(Column::_3, SortOrder::Desc) => b.finished().cmp(&a.finished()),
|
||||
(Column::_4, SortOrder::Asc) if a.finished().is_some() && b.finished().is_some() => {
|
||||
a.succeeded().cmp(&b.succeeded())
|
||||
(Column::_0, SortOrder::Asc) => a.id.cmp(&b.id),
|
||||
(Column::_0, SortOrder::Desc) => b.id.cmp(&b.id),
|
||||
(Column::_1, SortOrder::Asc) => a.desc.cmp(&b.desc),
|
||||
(Column::_1, SortOrder::Desc) => b.desc.cmp(&a.desc),
|
||||
(Column::_2, SortOrder::Asc) => a.started.cmp(&b.started),
|
||||
(Column::_2, SortOrder::Desc) => b.started.cmp(&a.started),
|
||||
(Column::_3, SortOrder::Asc) => a.finished.cmp(&b.finished),
|
||||
(Column::_3, SortOrder::Desc) => b.finished.cmp(&a.finished),
|
||||
(Column::_4, SortOrder::Asc) if a.finished.is_some() && b.finished.is_some() => {
|
||||
a.succeeded.cmp(&b.succeeded)
|
||||
}
|
||||
(Column::_4, SortOrder::Desc) if a.finished().is_some() && b.finished().is_some() => {
|
||||
b.succeeded().cmp(&a.succeeded())
|
||||
(Column::_4, SortOrder::Desc) if a.finished.is_some() && b.finished.is_some() => {
|
||||
b.succeeded.cmp(&a.succeeded)
|
||||
}
|
||||
(Column::_4, SortOrder::Asc) if a.finished().is_none() => std::cmp::Ordering::Less,
|
||||
(Column::_4, SortOrder::Asc) if a.finished.is_none() => std::cmp::Ordering::Less,
|
||||
(Column::_4, SortOrder::Asc) => std::cmp::Ordering::Greater,
|
||||
(Column::_4, SortOrder::Desc) if a.finished().is_none() => std::cmp::Ordering::Greater,
|
||||
(Column::_4, SortOrder::Desc) if a.finished.is_none() => std::cmp::Ordering::Greater,
|
||||
(Column::_4, SortOrder::Desc) => std::cmp::Ordering::Less,
|
||||
});
|
||||
self.entries = entries;
|
||||
|
@ -138,9 +147,9 @@ impl JobManager {
|
|||
|
||||
for c in self.entries.values() {
|
||||
// title
|
||||
self.min_width[0] = self.min_width[0].max(c.id().to_string().len());
|
||||
self.min_width[0] = self.min_width[0].max(c.id.to_string().len());
|
||||
// desc
|
||||
self.min_width[1] = self.min_width[1].max(c.description().len());
|
||||
self.min_width[1] = self.min_width[1].max(c.desc.len());
|
||||
}
|
||||
self.min_width[2] = "1970-01-01 00:00:00".len();
|
||||
self.min_width[3] = self.min_width[2];
|
||||
|
@ -184,26 +193,24 @@ impl JobManager {
|
|||
{
|
||||
let area = self.data_columns.columns[0].area().nth_row(idx);
|
||||
self.data_columns.columns[0].grid_mut().write_string(
|
||||
&e.id().to_string(),
|
||||
&e.id.to_string(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let area = self.data_columns.columns[1].area().nth_row(idx);
|
||||
self.data_columns.columns[1].grid_mut().write_string(
|
||||
e.description(),
|
||||
&e.desc,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -211,7 +218,7 @@ impl JobManager {
|
|||
let area = self.data_columns.columns[2].area().nth_row(idx);
|
||||
self.data_columns.columns[2].grid_mut().write_string(
|
||||
&datetime::timestamp_to_string(
|
||||
e.started(),
|
||||
e.started,
|
||||
Some(RFC3339_DATETIME_AND_SPACE),
|
||||
true,
|
||||
),
|
||||
|
@ -220,14 +227,13 @@ impl JobManager {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let area = self.data_columns.columns[3].area().nth_row(idx);
|
||||
self.data_columns.columns[3].grid_mut().write_string(
|
||||
&if let Some(t) = e.finished() {
|
||||
&if let Some(t) = e.finished {
|
||||
Cow::Owned(datetime::timestamp_to_string(
|
||||
t,
|
||||
Some(RFC3339_DATETIME_AND_SPACE),
|
||||
|
@ -241,15 +247,14 @@ impl JobManager {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let area = self.data_columns.columns[4].area().nth_row(idx);
|
||||
self.data_columns.columns[4].grid_mut().write_string(
|
||||
&if e.finished().is_some() {
|
||||
Cow::Owned(format!("{:?}", e.succeeded()))
|
||||
&if e.finished.is_some() {
|
||||
Cow::Owned(format!("{:?}", e.succeeded))
|
||||
} else {
|
||||
Cow::Borrowed("-")
|
||||
},
|
||||
|
@ -258,7 +263,6 @@ impl JobManager {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +279,6 @@ impl JobManager {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -441,7 +444,6 @@ impl Component for JobManager {
|
|||
self.theme_default.attrs | Attr::BOLD,
|
||||
area.skip_cols(x_offset),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if self.sort_col as usize == i {
|
||||
use SortOrder::*;
|
||||
|
@ -458,7 +460,6 @@ impl Component for JobManager {
|
|||
self.theme_default.attrs,
|
||||
area.skip_cols(x_offset + h.len()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
x_offset += w + 2;
|
||||
|
@ -582,20 +583,6 @@ impl Component for JobManager {
|
|||
self.movement = Some(PageMovement::End);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) =>
|
||||
{
|
||||
self.set_dirty(true);
|
||||
self.movement = Some(PageMovement::Right(1));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) =>
|
||||
{
|
||||
self.set_dirty(true);
|
||||
self.movement = Some(PageMovement::Left(1));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Resize => {
|
||||
self.set_dirty(true);
|
||||
}
|
||||
|
@ -614,11 +601,7 @@ impl Component for JobManager {
|
|||
|
||||
fn kill(&mut self, uuid: ComponentId, context: &mut Context) {
|
||||
debug_assert!(uuid == self.id);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::Action(crate::command::Action::Tab(
|
||||
crate::command::TabAction::Kill(uuid),
|
||||
)));
|
||||
context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
|
@ -21,7 +21,6 @@
|
|||
|
||||
#![deny(
|
||||
rustdoc::redundant_explicit_links,
|
||||
unsafe_op_in_unsafe_fn,
|
||||
/* groups */
|
||||
clippy::correctness,
|
||||
clippy::suspicious,
|
||||
|
@ -43,7 +42,6 @@
|
|||
//clippy::ptr_as_ptr,
|
||||
clippy::doc_markdown,
|
||||
clippy::expect_fun_call,
|
||||
clippy::or_fun_call,
|
||||
clippy::bool_to_int_with_if,
|
||||
clippy::borrow_as_ptr,
|
||||
clippy::cast_ptr_alignment,
|
||||
|
@ -109,7 +107,6 @@ pub use melib::{
|
|||
pub mod args;
|
||||
#[cfg(feature = "cli-docs")]
|
||||
pub mod manpages;
|
||||
pub mod signal_handlers;
|
||||
pub mod subcommands;
|
||||
|
||||
#[macro_use]
|
||||
|
@ -141,17 +138,19 @@ pub use crate::mail::*;
|
|||
|
||||
pub mod notifications;
|
||||
|
||||
pub mod manage;
|
||||
pub use manage::*;
|
||||
pub mod mailbox_management;
|
||||
pub use mailbox_management::*;
|
||||
|
||||
// #[cfg(feature = "svgscreenshot")]
|
||||
// pub mod svg;
|
||||
pub mod jobs_view;
|
||||
pub use jobs_view::*;
|
||||
|
||||
#[cfg(feature = "svgscreenshot")]
|
||||
pub mod svg;
|
||||
|
||||
#[macro_use]
|
||||
pub mod conf;
|
||||
pub use crate::conf::{
|
||||
data_types::{IndexStyle, SearchBackend},
|
||||
DotAddressable, Settings, Shortcuts, ThemeAttribute,
|
||||
DotAddressable, IndexStyle, SearchBackend, Settings, Shortcuts, ThemeAttribute,
|
||||
};
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
|
@ -162,7 +161,3 @@ pub mod mailcap;
|
|||
|
||||
pub mod accounts;
|
||||
pub use self::accounts::Account;
|
||||
|
||||
pub mod patch_retrieve;
|
||||
|
||||
pub mod version_migrations;
|
||||
|
|
|
@ -21,21 +21,18 @@
|
|||
|
||||
//! Entities that handle Mail specific functions.
|
||||
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{
|
||||
backends::{AccountHash, Mailbox, MailboxHash},
|
||||
email::{attachment_types::*, attachments::*},
|
||||
text::{TextProcessing, Truncate},
|
||||
thread::ThreadNodeHash,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub type AttachmentBoxFuture = Pin<Box<dyn Future<Output = Result<AttachmentBuilder>> + Send>>;
|
||||
pub type AttachmentFilterBox = Box<dyn FnOnce(AttachmentBuilder) -> AttachmentBoxFuture + Send>;
|
||||
use crate::{
|
||||
melib::text::{TextProcessing, Truncate},
|
||||
uuid::Uuid,
|
||||
};
|
||||
|
||||
pub mod listing;
|
||||
pub use crate::listing::*;
|
||||
|
|
|
@ -20,9 +20,7 @@
|
|||
*/
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
convert::TryInto,
|
||||
fmt::Write as _,
|
||||
future::Future,
|
||||
io::Write,
|
||||
pin::Pin,
|
||||
|
@ -33,19 +31,13 @@ use std::{
|
|||
use indexmap::IndexSet;
|
||||
use melib::{
|
||||
email::attachment_types::{ContentType, MultipartType},
|
||||
list_management,
|
||||
parser::BytesExt,
|
||||
Address, Contacts, Draft, HeaderName, SpecialUsageMailbox, SubjectPrefix, UnixTimestamp,
|
||||
list_management, Address, AddressBook, Draft, HeaderName, SpecialUsageMailbox, SubjectPrefix,
|
||||
UnixTimestamp,
|
||||
};
|
||||
use nix::sys::wait::WaitStatus;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
accounts::JobRequest,
|
||||
jobs::{IsAsync, JoinHandle},
|
||||
terminal::embedded::Terminal,
|
||||
types::{sanitize_filename, File},
|
||||
};
|
||||
use crate::{accounts::JobRequest, jobs::JoinHandle, terminal::embedded::Terminal};
|
||||
|
||||
#[cfg(feature = "gpgme")]
|
||||
pub mod gpg;
|
||||
|
@ -134,7 +126,7 @@ enum ViewMode {
|
|||
EmbeddedPty,
|
||||
SelectRecipients(UIDialog<Address>),
|
||||
#[cfg(feature = "gpgme")]
|
||||
SelectKey(bool, gpg::KeySelection),
|
||||
SelectEncryptKey(bool, gpg::KeySelection),
|
||||
Send(UIConfirmationDialog),
|
||||
WaitingForSendResult(UIDialog<char>, JoinHandle<Result<()>>),
|
||||
}
|
||||
|
@ -244,41 +236,7 @@ impl Composer {
|
|||
format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")),
|
||||
);
|
||||
}
|
||||
let format_flowed = *account_settings!(context[account_hash].composing.format_flowed);
|
||||
if *account_settings!(context[account_hash].composing.use_signature) {
|
||||
let override_value = account_settings!(context[account_hash].composing.signature_file)
|
||||
.as_deref()
|
||||
.map(Cow::Borrowed)
|
||||
.filter(|p| p.as_ref().is_file());
|
||||
let account_value = || {
|
||||
context.accounts[&account_hash]
|
||||
.signature_file()
|
||||
.map(Cow::Owned)
|
||||
};
|
||||
if let Some(path) = override_value.or_else(account_value) {
|
||||
match std::fs::read_to_string(path.as_ref()).chain_err_related_path(path.as_ref()) {
|
||||
Ok(sig) => {
|
||||
let mut delimiter =
|
||||
account_settings!(context[account_hash].composing.signature_delimiter)
|
||||
.as_deref()
|
||||
.map(Cow::Borrowed)
|
||||
.unwrap_or_else(|| Cow::Borrowed("\n\n-- \n"));
|
||||
if format_flowed {
|
||||
delimiter = Cow::Owned(delimiter.replace(" \n", " \n\n"));
|
||||
}
|
||||
_ = write!(&mut ret.draft.body, "{}{}", delimiter.as_ref(), sig);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Could not open signature file for account `{}`: {}.",
|
||||
context.accounts[&account_hash].name(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if format_flowed {
|
||||
if *account_settings!(context[account_hash].composing.format_flowed) {
|
||||
ret.pager.set_reflow(melib::text::Reflow::FormatFlowed);
|
||||
}
|
||||
ret
|
||||
|
@ -306,7 +264,7 @@ impl Composer {
|
|||
});
|
||||
let envelope: EnvelopeRef = context.accounts[&account_hash].collection.get_env(env_hash);
|
||||
|
||||
ret.draft = Draft::edit(&envelope, bytes, Text::Plain)?;
|
||||
ret.draft = Draft::edit(&envelope, bytes)?;
|
||||
|
||||
ret.account_hash = account_hash;
|
||||
Ok(ret)
|
||||
|
@ -373,15 +331,15 @@ impl Composer {
|
|||
if !acc.is_empty() {
|
||||
acc.push(' ');
|
||||
}
|
||||
acc.push_str(&x.display_brackets().to_string());
|
||||
acc.push_str(&x.to_string());
|
||||
acc
|
||||
}),
|
||||
envelope.message_id().display_brackets()
|
||||
envelope.message_id_display()
|
||||
),
|
||||
);
|
||||
ret.draft.set_header(
|
||||
HeaderName::IN_REPLY_TO,
|
||||
envelope.message_id().display_brackets().to_string(),
|
||||
envelope.message_id_display().into(),
|
||||
);
|
||||
|
||||
if let Some(reply_to) = envelope.other_headers().get(HeaderName::TO) {
|
||||
|
@ -432,8 +390,8 @@ impl Composer {
|
|||
let ours = context.accounts[&coordinates.0]
|
||||
.settings
|
||||
.account()
|
||||
.main_identity_address();
|
||||
to.shift_remove(&ours);
|
||||
.make_display_name();
|
||||
to.remove(&ours);
|
||||
ret.draft.set_header(HeaderName::TO, {
|
||||
let mut ret: String =
|
||||
to.into_iter()
|
||||
|
@ -457,7 +415,7 @@ impl Composer {
|
|||
.set_header(HeaderName::TO, envelope.field_from_to_string());
|
||||
}
|
||||
ret.draft.body = {
|
||||
let mut quoted = attribution_string(
|
||||
let mut ret = attribution_string(
|
||||
account_settings!(
|
||||
context[ret.account_hash]
|
||||
.composing
|
||||
|
@ -474,12 +432,11 @@ impl Composer {
|
|||
),
|
||||
);
|
||||
for l in reply_body.lines() {
|
||||
quoted.push('>');
|
||||
quoted.push_str(l);
|
||||
quoted.push('\n');
|
||||
ret.push('>');
|
||||
ret.push_str(l);
|
||||
ret.push('\n');
|
||||
}
|
||||
_ = write!(&mut quoted, "{}", ret.draft.body);
|
||||
quoted
|
||||
ret
|
||||
};
|
||||
|
||||
ret.account_hash = coordinates.0;
|
||||
|
@ -579,7 +536,7 @@ To: {}
|
|||
let mut attachment = AttachmentBuilder::new(b"");
|
||||
let mut disposition: ContentDisposition = ContentDispositionKind::Attachment.into();
|
||||
{
|
||||
disposition.filename = Some(format!("{}.eml", env.message_id()));
|
||||
disposition.filename = Some(format!("{}.eml", env.message_id_raw()));
|
||||
}
|
||||
attachment
|
||||
.set_raw(bytes.to_vec())
|
||||
|
@ -675,7 +632,7 @@ To: {}
|
|||
k.into(),
|
||||
headers[k].to_string(),
|
||||
Box::new(move |c, term| {
|
||||
let book: &Contacts = &c.accounts[&account_hash].contacts;
|
||||
let book: &AddressBook = &c.accounts[&account_hash].address_book;
|
||||
let results: Vec<String> = book.search(term);
|
||||
results
|
||||
.into_iter()
|
||||
|
@ -691,23 +648,24 @@ To: {}
|
|||
c.accounts
|
||||
.values()
|
||||
.map(|acc| {
|
||||
let addr = acc.settings.account.main_identity_address();
|
||||
let desc = match account_settings!(c[acc.hash()].send_mail) {
|
||||
crate::conf::composing::SendMail::ShellCommand(ref cmd) => {
|
||||
let mut cmd = cmd.as_str();
|
||||
cmd.truncate_at_boundary(10);
|
||||
format!("{} [exec: {}]", acc.name(), cmd)
|
||||
}
|
||||
#[cfg(feature = "smtp")]
|
||||
crate::conf::composing::SendMail::Smtp(ref inner) => {
|
||||
let mut hostname = inner.hostname.as_str();
|
||||
hostname.truncate_at_boundary(10);
|
||||
format!("{} [smtp: {}]", acc.name(), hostname)
|
||||
}
|
||||
crate::conf::composing::SendMail::ServerSubmission => {
|
||||
format!("{} [server submission]", acc.name())
|
||||
}
|
||||
};
|
||||
let addr = acc.settings.account.make_display_name();
|
||||
let desc =
|
||||
match account_settings!(c[acc.hash()].composing.send_mail) {
|
||||
crate::conf::composing::SendMail::ShellCommand(ref cmd) => {
|
||||
let mut cmd = cmd.as_str();
|
||||
cmd.truncate_at_boundary(10);
|
||||
format!("{} [exec: {}]", acc.name(), cmd)
|
||||
}
|
||||
#[cfg(feature = "smtp")]
|
||||
crate::conf::composing::SendMail::Smtp(ref inner) => {
|
||||
let mut hostname = inner.hostname.as_str();
|
||||
hostname.truncate_at_boundary(10);
|
||||
format!("{} [smtp: {}]", acc.name(), hostname)
|
||||
}
|
||||
crate::conf::composing::SendMail::ServerSubmission => {
|
||||
format!("{} [server submission]", acc.name())
|
||||
}
|
||||
};
|
||||
|
||||
(addr.to_string(), desc)
|
||||
})
|
||||
|
@ -725,12 +683,6 @@ To: {}
|
|||
let attachments_no = self.draft.attachments().len();
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
grid.clear_area(area, theme_default);
|
||||
let our_map: ShortcutMap =
|
||||
account_settings!(context[self.account_hash].shortcuts.composing).key_values();
|
||||
let mut shortcuts: ShortcutMaps = Default::default();
|
||||
shortcuts.insert(Shortcuts::COMPOSING, our_map);
|
||||
let toggle_shortcut = Key::Char('\n');
|
||||
let edit_shortcut = &shortcuts[Shortcuts::COMPOSING]["edit"];
|
||||
#[cfg(feature = "gpgme")]
|
||||
if self
|
||||
.gpg_state
|
||||
|
@ -742,20 +694,18 @@ To: {}
|
|||
.gpg_state
|
||||
.sign_keys
|
||||
.iter()
|
||||
.map(|k| k.to_string())
|
||||
.map(|k| k.fingerprint())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
grid.write_string(
|
||||
&format!(
|
||||
"☑ sign with [toggle: {}, edit: {}] {}",
|
||||
toggle_shortcut,
|
||||
edit_shortcut,
|
||||
"☑ sign with {}",
|
||||
if self.gpg_state.sign_keys.is_empty() {
|
||||
"default key"
|
||||
} else {
|
||||
key_list.as_str()
|
||||
},
|
||||
}
|
||||
),
|
||||
theme_default.fg,
|
||||
if self.cursor == Cursor::Sign {
|
||||
|
@ -766,14 +716,10 @@ To: {}
|
|||
theme_default.attrs,
|
||||
area.skip_rows(1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
grid.write_string(
|
||||
&format!(
|
||||
"☐ don't sign [toggle: {}, edit: {}]",
|
||||
toggle_shortcut, edit_shortcut,
|
||||
),
|
||||
"☐ don't sign",
|
||||
theme_default.fg,
|
||||
if self.cursor == Cursor::Sign {
|
||||
crate::conf::value(context, "highlight").bg
|
||||
|
@ -783,7 +729,6 @@ To: {}
|
|||
theme_default.attrs,
|
||||
area.skip_rows(1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
|
@ -797,28 +742,18 @@ To: {}
|
|||
.gpg_state
|
||||
.encrypt_keys
|
||||
.iter()
|
||||
.map(|k| k.to_string())
|
||||
.map(|k| k.fingerprint())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
grid.write_string(
|
||||
&format!(
|
||||
"{}{}{}",
|
||||
"{}{}",
|
||||
if self.gpg_state.encrypt_keys.is_empty() {
|
||||
"☐ no keys selected to encrypt with"
|
||||
"☐ no keys to encrypt with!"
|
||||
} else {
|
||||
"☑ encrypt with"
|
||||
"☑ encrypt with "
|
||||
},
|
||||
&format!(
|
||||
" [toggle: {}, edit: {}]{}",
|
||||
toggle_shortcut,
|
||||
edit_shortcut,
|
||||
if self.gpg_state.encrypt_keys.is_empty() {
|
||||
""
|
||||
} else {
|
||||
" "
|
||||
}
|
||||
),
|
||||
if self.gpg_state.encrypt_keys.is_empty() {
|
||||
""
|
||||
} else {
|
||||
|
@ -834,14 +769,10 @@ To: {}
|
|||
theme_default.attrs,
|
||||
area.skip_rows(2),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
grid.write_string(
|
||||
&format!(
|
||||
"☐ don't encrypt [toggle: {}, edit: {}]",
|
||||
toggle_shortcut, edit_shortcut,
|
||||
),
|
||||
"☐ don't encrypt",
|
||||
theme_default.fg,
|
||||
if self.cursor == Cursor::Encrypt {
|
||||
crate::conf::value(context, "highlight").bg
|
||||
|
@ -851,12 +782,11 @@ To: {}
|
|||
theme_default.attrs,
|
||||
area.skip_rows(2),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
if attachments_no == 0 {
|
||||
grid.write_string(
|
||||
&format!("no attachments [edit: {}]", edit_shortcut),
|
||||
"no attachments",
|
||||
theme_default.fg,
|
||||
if self.cursor == Cursor::Attachments {
|
||||
crate::conf::value(context, "highlight").bg
|
||||
|
@ -866,11 +796,10 @@ To: {}
|
|||
theme_default.attrs,
|
||||
area.skip_rows(3),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
grid.write_string(
|
||||
&format!("{} attachments [edit: {}]", attachments_no, edit_shortcut),
|
||||
&format!("{} attachments ", attachments_no),
|
||||
theme_default.fg,
|
||||
if self.cursor == Cursor::Attachments {
|
||||
crate::conf::value(context, "highlight").bg
|
||||
|
@ -880,7 +809,6 @@ To: {}
|
|||
theme_default.attrs,
|
||||
area.skip_rows(3),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for (i, a) in self.draft.attachments().iter().enumerate() {
|
||||
grid.write_string(
|
||||
|
@ -905,7 +833,6 @@ To: {}
|
|||
theme_default.attrs,
|
||||
area.skip_rows(4 + i),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -936,36 +863,6 @@ To: {}
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpgme")]
|
||||
fn create_key_selection_widget(
|
||||
&self,
|
||||
secret: bool,
|
||||
header: &HeaderName,
|
||||
context: &Context,
|
||||
) -> Result<gpg::KeySelectionLoading> {
|
||||
let (_, mut list) = melib::email::parser::address::rfc2822address_list(
|
||||
self.form.values()[header.as_str()].as_str().as_bytes(),
|
||||
)
|
||||
.map_err(|_err| -> Error { format!("No valid address in `{header}:`").into() })?;
|
||||
if list.is_empty() {
|
||||
return Err(format!("No valid address in `{header}:`").into());
|
||||
}
|
||||
let first = list.remove(0);
|
||||
let patterns = (
|
||||
first.get_email(),
|
||||
list.into_iter()
|
||||
.map(|addr| addr.get_email())
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
gpg::KeySelectionLoading::new(
|
||||
secret,
|
||||
account_settings!(context[self.account_hash].pgp.allow_remote_lookup).is_true(),
|
||||
patterns,
|
||||
*account_settings!(context[self.account_hash].pgp.allow_remote_lookup),
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Composer {
|
||||
|
@ -980,11 +877,6 @@ impl Component for Composer {
|
|||
self.gpg_state.sign_mail =
|
||||
Some(*account_settings!(context[self.account_hash].pgp.auto_sign));
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
{
|
||||
self.gpg_state.encrypt_for_self =
|
||||
*account_settings!(context[self.account_hash].pgp.encrypt_for_self);
|
||||
}
|
||||
if !self.draft.headers().contains_key(HeaderName::FROM)
|
||||
|| self.draft.headers()[HeaderName::FROM].is_empty()
|
||||
{
|
||||
|
@ -993,7 +885,7 @@ impl Component for Composer {
|
|||
context.accounts[&self.account_hash]
|
||||
.settings
|
||||
.account()
|
||||
.main_identity_address()
|
||||
.make_display_name()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
@ -1032,33 +924,17 @@ impl Component for Composer {
|
|||
|
||||
if self.dirty {
|
||||
grid.clear_area(area.nth_row(0), crate::conf::value(context, "highlight"));
|
||||
let our_map: ShortcutMap =
|
||||
account_settings!(context[self.account_hash].shortcuts.composing).key_values();
|
||||
let mut shortcuts: ShortcutMaps = Default::default();
|
||||
shortcuts.insert(Shortcuts::COMPOSING, our_map);
|
||||
let scroll_down_shortcut = &shortcuts[Shortcuts::COMPOSING]["scroll_down"];
|
||||
let scroll_up_shortcut = &shortcuts[Shortcuts::COMPOSING]["scroll_up"];
|
||||
let field_shortcut = Key::Char('\n');
|
||||
let edit_shortcut = &shortcuts[Shortcuts::COMPOSING]["edit"];
|
||||
grid.write_string(
|
||||
&format!(
|
||||
"COMPOSING {} [scroll down: {}, scroll up: {}, edit fields: {}, edit body: {}]",
|
||||
if self.reply_context.is_some() {
|
||||
"REPLY"
|
||||
} else {
|
||||
"MESSAGE"
|
||||
},
|
||||
scroll_down_shortcut,
|
||||
scroll_up_shortcut,
|
||||
field_shortcut,
|
||||
edit_shortcut
|
||||
),
|
||||
if self.reply_context.is_some() {
|
||||
"COMPOSING REPLY"
|
||||
} else {
|
||||
"COMPOSING MESSAGE"
|
||||
},
|
||||
crate::conf::value(context, "highlight").fg,
|
||||
crate::conf::value(context, "highlight").bg,
|
||||
crate::conf::value(context, "highlight").attrs,
|
||||
area.nth_row(0),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1118,11 +994,11 @@ impl Component for Composer {
|
|||
let stopped_message: String =
|
||||
format!("Process with PID {} has stopped.", guard.child_pid);
|
||||
let stopped_message_2: String = format!(
|
||||
"- re-activate '{}' (edit shortcut)",
|
||||
"-press '{}' (edit shortcut) to re-activate.",
|
||||
shortcuts[Shortcuts::COMPOSING]["edit"]
|
||||
);
|
||||
const STOPPED_MESSAGE_3: &str =
|
||||
"- press Ctrl-C to forcefully kill it and return to editor";
|
||||
"-press Ctrl-C to forcefully kill it and return to editor.";
|
||||
let max_len = std::cmp::max(
|
||||
stopped_message.len(),
|
||||
std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()),
|
||||
|
@ -1144,7 +1020,6 @@ impl Component for Composer {
|
|||
theme_default.attrs,
|
||||
inner_area.skip_rows(i),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
context.dirty_areas.push_back(area);
|
||||
|
@ -1185,50 +1060,30 @@ impl Component for Composer {
|
|||
.draw(grid, inner_area, context);
|
||||
}
|
||||
ViewMode::Send(ref mut s) => {
|
||||
let inner_area = area.center_inside((
|
||||
area.width().saturating_sub(2),
|
||||
area.height().saturating_sub(2),
|
||||
));
|
||||
s.draw(grid, inner_area, context);
|
||||
s.draw(grid, body_area, context);
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
ViewMode::SelectKey(
|
||||
ViewMode::SelectEncryptKey(
|
||||
_,
|
||||
gpg::KeySelection::Loaded {
|
||||
ref mut widget,
|
||||
keys: _,
|
||||
},
|
||||
) => {
|
||||
let inner_area = area.center_inside((
|
||||
area.width().saturating_sub(2),
|
||||
area.height().saturating_sub(2),
|
||||
));
|
||||
widget.draw(grid, inner_area, context);
|
||||
widget.draw(grid, body_area, context);
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
ViewMode::SelectKey(_, _) => {}
|
||||
ViewMode::SelectEncryptKey(_, _) => {}
|
||||
ViewMode::SelectRecipients(ref mut s) => {
|
||||
let inner_area = area.center_inside((
|
||||
area.width().saturating_sub(2),
|
||||
area.height().saturating_sub(2),
|
||||
));
|
||||
s.draw(grid, inner_area, context);
|
||||
s.draw(grid, body_area, context);
|
||||
}
|
||||
ViewMode::Discard(_, ref mut s) => {
|
||||
let inner_area = area.center_inside((
|
||||
area.width().saturating_sub(2),
|
||||
area.height().saturating_sub(2),
|
||||
));
|
||||
/* Let user choose whether to quit with/without saving or cancel */
|
||||
s.draw(grid, inner_area, context);
|
||||
s.draw(grid, body_area, context);
|
||||
}
|
||||
ViewMode::WaitingForSendResult(ref mut s, _) => {
|
||||
let inner_area = area.center_inside((
|
||||
area.width().saturating_sub(2),
|
||||
area.height().saturating_sub(2),
|
||||
));
|
||||
/* Let user choose whether to wait for success or cancel */
|
||||
s.draw(grid, inner_area, context);
|
||||
s.draw(grid, body_area, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1311,7 +1166,7 @@ impl Component for Composer {
|
|||
{
|
||||
if matches!(
|
||||
widget.buttons.result(),
|
||||
Some(FormButtonAction::Cancel | FormButtonAction::Accept)
|
||||
Some(FormButtonActions::Cancel | FormButtonActions::Accept)
|
||||
) {
|
||||
self.mode = ViewMode::Edit;
|
||||
}
|
||||
|
@ -1334,11 +1189,10 @@ impl Component for Composer {
|
|||
Flag::SEEN,
|
||||
) {
|
||||
Ok(job) => {
|
||||
let handle = context.main_loop_handler.job_executor.spawn(
|
||||
"compose::submit".into(),
|
||||
job,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let handle = context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_blocking("compose::submit".into(), job);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::NewJob(
|
||||
|
@ -1408,9 +1262,10 @@ impl Component for Composer {
|
|||
self.set_dirty(true);
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
(ViewMode::SelectKey(_, ref mut selector), UIEvent::ComponentUnrealize(ref id))
|
||||
if *id == selector.id() =>
|
||||
{
|
||||
(
|
||||
ViewMode::SelectEncryptKey(_, ref mut selector),
|
||||
UIEvent::ComponentUnrealize(ref id),
|
||||
) if *id == selector.id() => {
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
|
@ -1539,16 +1394,16 @@ impl Component for Composer {
|
|||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
(
|
||||
ViewMode::SelectKey(is_encrypt, ref mut selector),
|
||||
ViewMode::SelectEncryptKey(is_encrypt, ref mut selector),
|
||||
UIEvent::FinishedUIDialog(id, result),
|
||||
) if *id == selector.id() => {
|
||||
if let Some(Some(keys)) = result.downcast_mut::<Option<Vec<melib::gpgme::Key>>>() {
|
||||
if let Some(Some(key)) = result.downcast_mut::<Option<melib::gpgme::Key>>() {
|
||||
if *is_encrypt {
|
||||
self.gpg_state.encrypt_keys.clear();
|
||||
self.gpg_state.encrypt_keys = std::mem::take(keys);
|
||||
self.gpg_state.encrypt_keys.push(key.clone());
|
||||
} else {
|
||||
self.gpg_state.sign_keys.clear();
|
||||
self.gpg_state.sign_keys = std::mem::take(keys);
|
||||
self.gpg_state.sign_keys.push(key.clone());
|
||||
}
|
||||
}
|
||||
self.mode = ViewMode::Edit;
|
||||
|
@ -1556,7 +1411,7 @@ impl Component for Composer {
|
|||
return true;
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
(ViewMode::SelectKey(_, ref mut selector), _) => {
|
||||
(ViewMode::SelectEncryptKey(_, ref mut selector), _) => {
|
||||
if selector.process_event(event, context) {
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
|
@ -1825,28 +1680,39 @@ impl Component for Composer {
|
|||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
|
||||
{
|
||||
#[cfg(feature = "gpgme")]
|
||||
{
|
||||
match self
|
||||
.create_key_selection_widget(false, &HeaderName::FROM, context)
|
||||
.map(Into::into)
|
||||
{
|
||||
Ok(widget) => {
|
||||
self.gpg_state.sign_mail = Some(ActionFlag::from(true));
|
||||
self.mode = ViewMode::SelectKey(false, widget);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not list keys.".into()),
|
||||
source: None,
|
||||
body: format!("libgpgme error: {err}").into(),
|
||||
kind: Some(NotificationType::Error(
|
||||
melib::error::ErrorKind::External,
|
||||
)),
|
||||
});
|
||||
}
|
||||
match melib::email::parser::address::rfc2822address_list(
|
||||
self.form.values()["From"].as_str().as_bytes(),
|
||||
)
|
||||
.map_err(|_err| -> Error { "No valid sender address in `From:`".into() })
|
||||
.and_then(|(_, list)| {
|
||||
list.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| "No valid sender address in `From:`".into())
|
||||
})
|
||||
.and_then(|addr| {
|
||||
gpg::KeySelection::new(
|
||||
false,
|
||||
account_settings!(context[self.account_hash].pgp.allow_remote_lookup)
|
||||
.is_true(),
|
||||
addr.get_email(),
|
||||
*account_settings!(context[self.account_hash].pgp.allow_remote_lookup),
|
||||
context,
|
||||
)
|
||||
}) {
|
||||
Ok(widget) => {
|
||||
self.gpg_state.sign_mail = Some(ActionFlag::from(true));
|
||||
self.mode = ViewMode::SelectEncryptKey(false, widget);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not list keys.".into()),
|
||||
source: None,
|
||||
body: format!("libgpgme error: {err}").into(),
|
||||
kind: Some(NotificationType::Error(melib::error::ErrorKind::External)),
|
||||
});
|
||||
}
|
||||
self.set_dirty(true);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
|
@ -1855,63 +1721,39 @@ impl Component for Composer {
|
|||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
|
||||
{
|
||||
#[cfg(feature = "gpgme")]
|
||||
{
|
||||
let mut result =
|
||||
self.create_key_selection_widget(false, &HeaderName::TO, context);
|
||||
if !self.form.values()[HeaderName::CC.as_str()]
|
||||
.as_str()
|
||||
.is_empty()
|
||||
{
|
||||
result = result.and_then(|mut to_result| {
|
||||
let cc_result =
|
||||
self.create_key_selection_widget(false, &HeaderName::CC, context)?;
|
||||
to_result.merge(cc_result);
|
||||
Ok(to_result)
|
||||
match melib::email::parser::address::rfc2822address_list(
|
||||
self.form.values()["To"].as_str().as_bytes(),
|
||||
)
|
||||
.map_err(|_err| -> Error { "No valid recipient addresses in `To:`".into() })
|
||||
.and_then(|(_, list)| {
|
||||
list.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| "No valid recipient addresses in `To:`".into())
|
||||
})
|
||||
.and_then(|addr| {
|
||||
gpg::KeySelection::new(
|
||||
false,
|
||||
account_settings!(context[self.account_hash].pgp.allow_remote_lookup)
|
||||
.is_true(),
|
||||
addr.get_email(),
|
||||
*account_settings!(context[self.account_hash].pgp.allow_remote_lookup),
|
||||
context,
|
||||
)
|
||||
}) {
|
||||
Ok(widget) => {
|
||||
self.gpg_state.encrypt_mail = Some(ActionFlag::from(true));
|
||||
self.mode = ViewMode::SelectEncryptKey(true, widget);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not list keys.".into()),
|
||||
source: None,
|
||||
body: format!("libgpgme error: {err}").into(),
|
||||
kind: Some(NotificationType::Error(melib::error::ErrorKind::External)),
|
||||
});
|
||||
}
|
||||
if !self.form.values()[HeaderName::BCC.as_str()]
|
||||
.as_str()
|
||||
.is_empty()
|
||||
{
|
||||
result = result.and_then(|mut to_result| {
|
||||
let bcc_result =
|
||||
self.create_key_selection_widget(false, &HeaderName::BCC, context)?;
|
||||
to_result.merge(bcc_result);
|
||||
Ok(to_result)
|
||||
});
|
||||
}
|
||||
if !self.form.values()[HeaderName::FROM.as_str()]
|
||||
.as_str()
|
||||
.is_empty()
|
||||
{
|
||||
result = result.and_then(|mut to_result| {
|
||||
let from_result = self.create_key_selection_widget(
|
||||
false,
|
||||
&HeaderName::FROM,
|
||||
context,
|
||||
)?;
|
||||
to_result.merge(from_result);
|
||||
Ok(to_result)
|
||||
});
|
||||
}
|
||||
match result.map(Into::into) {
|
||||
Ok(widget) => {
|
||||
self.gpg_state.encrypt_mail = Some(ActionFlag::from(true));
|
||||
self.mode = ViewMode::SelectKey(true, widget);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not list keys.".into()),
|
||||
source: None,
|
||||
body: format!("libgpgme error: {err}").into(),
|
||||
kind: Some(NotificationType::Error(
|
||||
melib::error::ErrorKind::External,
|
||||
)),
|
||||
});
|
||||
}
|
||||
}
|
||||
self.set_dirty(true);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
|
@ -2012,25 +1854,10 @@ impl Component for Composer {
|
|||
account_settings!(context[self.account_hash].composing.wrap_header_preamble)
|
||||
.clone(),
|
||||
);
|
||||
let filename = format!(
|
||||
"{date}_{subject}_{to}_{in_reply_to}",
|
||||
date = self.draft.headers.get(HeaderName::DATE).unwrap_or_default(),
|
||||
subject = self
|
||||
.draft
|
||||
.headers
|
||||
.get(HeaderName::SUBJECT)
|
||||
.unwrap_or_default(),
|
||||
to = self.draft.headers.get(HeaderName::TO).unwrap_or_default(),
|
||||
in_reply_to = self
|
||||
.draft
|
||||
.headers
|
||||
.get(HeaderName::IN_REPLY_TO)
|
||||
.unwrap_or_default()
|
||||
);
|
||||
|
||||
let f = match File::create_temp_file(
|
||||
self.draft.to_edit_string().as_bytes(),
|
||||
sanitize_filename(filename).as_deref(),
|
||||
None,
|
||||
None,
|
||||
Some("eml"),
|
||||
true,
|
||||
|
@ -2153,8 +1980,8 @@ impl Component for Composer {
|
|||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Action(Action::Tab(ComposerAction(ref a))) => match a {
|
||||
ComposerTabAction::AddAttachmentPipe(ref command) => {
|
||||
UIEvent::Action(ref a) => match a {
|
||||
Action::Compose(ComposeAction::AddAttachmentPipe(ref command)) => {
|
||||
if command.is_empty() {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: None,
|
||||
|
@ -2223,7 +2050,7 @@ impl Component for Composer {
|
|||
}
|
||||
}
|
||||
}
|
||||
ComposerTabAction::AddAttachment(ref path) => {
|
||||
Action::Compose(ComposeAction::AddAttachment(ref path)) => {
|
||||
let attachment = match melib::email::compose::attachment_from_file(path) {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
|
@ -2242,7 +2069,7 @@ impl Component for Composer {
|
|||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
ComposerTabAction::AddAttachmentFilePicker(ref command) => {
|
||||
Action::Compose(ComposeAction::AddAttachmentFilePicker(ref command)) => {
|
||||
let command = if let Some(cmd) =
|
||||
command
|
||||
.as_ref()
|
||||
|
@ -2270,16 +2097,14 @@ impl Component for Composer {
|
|||
match Command::new("sh")
|
||||
.args(["-c", command])
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.and_then(|child| Ok(child.wait_with_output()?.stdout))
|
||||
.and_then(|child| Ok(child.wait_with_output()?.stderr))
|
||||
{
|
||||
Ok(stdout) => {
|
||||
for path in stdout
|
||||
.split(|c| [b'\0', b'\t', b'\n'].contains(c))
|
||||
.filter(|p| !p.trim().is_empty())
|
||||
{
|
||||
Ok(stderr) => {
|
||||
log::trace!("stderr: {}", &String::from_utf8_lossy(&stderr));
|
||||
for path in stderr.split(|c| [b'\0', b'\t', b'\n'].contains(c)) {
|
||||
match melib::email::compose::attachment_from_file(
|
||||
&String::from_utf8_lossy(path).as_ref(),
|
||||
) {
|
||||
|
@ -2327,7 +2152,7 @@ impl Component for Composer {
|
|||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
ComposerTabAction::RemoveAttachment(idx) => {
|
||||
Action::Compose(ComposeAction::RemoveAttachment(idx)) => {
|
||||
if *idx + 1 > self.draft.attachments().len() {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(
|
||||
|
@ -2346,7 +2171,7 @@ impl Component for Composer {
|
|||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
ComposerTabAction::SaveDraft => {
|
||||
Action::Compose(ComposeAction::SaveDraft) => {
|
||||
save_draft(
|
||||
self.draft.clone().finalise().unwrap().as_bytes(),
|
||||
context,
|
||||
|
@ -2357,15 +2182,8 @@ impl Component for Composer {
|
|||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
ComposerTabAction::DiscardDraft => {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::Action(Tab(Kill(self.id))));
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
ComposerTabAction::ToggleSign => {
|
||||
Action::Compose(ComposeAction::ToggleSign) => {
|
||||
let is_true = self
|
||||
.gpg_state
|
||||
.sign_mail
|
||||
|
@ -2376,7 +2194,7 @@ impl Component for Composer {
|
|||
return true;
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
ComposerTabAction::ToggleEncrypt => {
|
||||
Action::Compose(ComposeAction::ToggleEncrypt) => {
|
||||
let is_true = self
|
||||
.gpg_state
|
||||
.encrypt_mail
|
||||
|
@ -2386,6 +2204,7 @@ impl Component for Composer {
|
|||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
UIEvent::Input(ref key)
|
||||
if context
|
||||
|
@ -2436,7 +2255,7 @@ impl Component for Composer {
|
|||
widget.is_dirty() || self.pager.is_dirty() || self.form.is_dirty()
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
ViewMode::SelectKey(_, ref widget) => {
|
||||
ViewMode::SelectEncryptKey(_, ref widget) => {
|
||||
widget.is_dirty() || self.pager.is_dirty() || self.form.is_dirty()
|
||||
}
|
||||
ViewMode::Send(ref widget) => {
|
||||
|
@ -2460,7 +2279,7 @@ impl Component for Composer {
|
|||
widget.set_dirty(value);
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
ViewMode::SelectKey(_, ref mut widget) => {
|
||||
ViewMode::SelectEncryptKey(_, ref mut widget) => {
|
||||
widget.set_dirty(value);
|
||||
}
|
||||
ViewMode::Send(ref mut widget) => {
|
||||
|
@ -2661,7 +2480,7 @@ pub fn send_draft(
|
|||
}
|
||||
}
|
||||
let bytes = draft.finalise().unwrap();
|
||||
let send_mail = account_settings!(context[account_hash].send_mail).clone();
|
||||
let send_mail = account_settings!(context[account_hash].composing.send_mail).clone();
|
||||
let ret =
|
||||
context.accounts[&account_hash].send(bytes.clone(), send_mail, complete_in_background);
|
||||
save_draft(bytes.as_bytes(), context, mailbox_type, flags, account_hash);
|
||||
|
@ -2716,7 +2535,16 @@ pub fn send_draft_async(
|
|||
let format_flowed = *account_settings!(context[account_hash].composing.format_flowed);
|
||||
let event_sender = context.main_loop_handler.sender.clone();
|
||||
#[cfg(feature = "gpgme")]
|
||||
let mut filters_stack: Vec<AttachmentFilterBox> = vec![];
|
||||
#[allow(clippy::type_complexity)]
|
||||
let mut filters_stack: Vec<
|
||||
Box<
|
||||
dyn FnOnce(
|
||||
AttachmentBuilder,
|
||||
)
|
||||
-> Pin<Box<dyn Future<Output = Result<AttachmentBuilder>> + Send>>
|
||||
+ Send,
|
||||
>,
|
||||
> = vec![];
|
||||
#[cfg(feature = "gpgme")]
|
||||
if gpg_state.sign_mail.unwrap_or(ActionFlag::False).is_true()
|
||||
&& !gpg_state
|
||||
|
@ -2725,10 +2553,6 @@ pub fn send_draft_async(
|
|||
.is_true()
|
||||
{
|
||||
filters_stack.push(Box::new(crate::mail::pgp::sign_filter(
|
||||
(account_settings!(context[account_hash].pgp.auto_sign).is_true()
|
||||
&& gpg_state.sign_keys.is_empty())
|
||||
.then(|| account_settings!(context[account_hash].pgp.sign_key).clone())
|
||||
.flatten(),
|
||||
gpg_state.sign_keys,
|
||||
)?));
|
||||
} else if gpg_state
|
||||
|
@ -2737,43 +2561,24 @@ pub fn send_draft_async(
|
|||
.is_true()
|
||||
{
|
||||
filters_stack.push(Box::new(crate::mail::pgp::encrypt_filter(
|
||||
gpg_state.encrypt_for_self.then_some(()).map_or_else(
|
||||
|| Ok(None),
|
||||
|()| {
|
||||
draft.headers().get(HeaderName::FROM).map_or_else(
|
||||
|| Ok(None),
|
||||
|s| Some(melib::Address::try_from(s)).transpose(),
|
||||
)
|
||||
},
|
||||
)?,
|
||||
(gpg_state.sign_mail.unwrap_or(ActionFlag::False).is_true()
|
||||
&& gpg_state.sign_keys.is_empty())
|
||||
.then(|| account_settings!(context[account_hash].pgp.sign_key).clone())
|
||||
.flatten(),
|
||||
gpg_state
|
||||
.sign_mail
|
||||
.unwrap_or(ActionFlag::False)
|
||||
.is_true()
|
||||
.then(|| gpg_state.sign_keys.clone()),
|
||||
gpg_state
|
||||
.encrypt_keys
|
||||
.is_empty()
|
||||
.then(|| account_settings!(context[account_hash].pgp.encrypt_key).clone())
|
||||
.flatten(),
|
||||
if gpg_state.sign_mail.unwrap_or(ActionFlag::False).is_true() {
|
||||
Some(gpg_state.sign_keys.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
gpg_state.encrypt_keys,
|
||||
)?));
|
||||
}
|
||||
let send_mail = account_settings!(context[account_hash].send_mail).clone();
|
||||
let send_mail = account_settings!(context[account_hash].composing.send_mail).clone();
|
||||
let send_cb = context.accounts[&account_hash].send_async(send_mail);
|
||||
let mut content_type = ContentType::default();
|
||||
if let (
|
||||
true,
|
||||
ContentType::Text {
|
||||
if format_flowed {
|
||||
if let ContentType::Text {
|
||||
ref mut parameters, ..
|
||||
},
|
||||
) = (format_flowed, &mut content_type)
|
||||
{
|
||||
parameters.push((b"format".to_vec(), b"flowed".to_vec()));
|
||||
} = content_type
|
||||
{
|
||||
parameters.push((b"format".to_vec(), b"flowed".to_vec()));
|
||||
}
|
||||
}
|
||||
let mut body: AttachmentBuilder = Attachment::new(
|
||||
content_type,
|
||||
|
|
|
@ -32,7 +32,7 @@ pub enum EditAttachmentCursor {
|
|||
pub enum EditAttachmentMode {
|
||||
Overview,
|
||||
Edit {
|
||||
inner: Box<FormWidget<FormButtonAction>>,
|
||||
inner: Box<FormWidget<FormButtonActions>>,
|
||||
no: usize,
|
||||
},
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ pub struct EditAttachments {
|
|||
/// For shortcut setting retrieval.
|
||||
pub account_hash: Option<AccountHash>,
|
||||
pub mode: EditAttachmentMode,
|
||||
pub buttons: ButtonWidget<FormButtonAction>,
|
||||
pub buttons: ButtonWidget<FormButtonActions>,
|
||||
pub cursor: EditAttachmentCursor,
|
||||
pub dirty: bool,
|
||||
pub id: ComponentId,
|
||||
|
@ -50,8 +50,8 @@ pub struct EditAttachments {
|
|||
|
||||
impl EditAttachments {
|
||||
pub fn new(account_hash: Option<AccountHash>) -> Self {
|
||||
//ButtonWidget::new(("Add".into(), FormButtonAction::Other("add")));
|
||||
let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonAction::Cancel));
|
||||
//ButtonWidget::new(("Add".into(), FormButtonActions::Other("add")));
|
||||
let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonActions::Cancel));
|
||||
buttons.set_focus(true);
|
||||
buttons.set_cursor(1);
|
||||
Self {
|
||||
|
@ -70,7 +70,7 @@ impl EditAttachmentsRefMut<'_, '_> {
|
|||
&self,
|
||||
no: usize,
|
||||
context: &Context,
|
||||
) -> Option<Box<FormWidget<FormButtonAction>>> {
|
||||
) -> Option<Box<FormWidget<FormButtonActions>>> {
|
||||
if no >= self.draft.attachments().len() {
|
||||
return None;
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ impl EditAttachmentsRefMut<'_, '_> {
|
|||
let shortcuts = self.shortcuts(context);
|
||||
|
||||
let mut ret = FormWidget::new(
|
||||
("Save".into(), FormButtonAction::Accept),
|
||||
("Save".into(), FormButtonActions::Accept),
|
||||
/* cursor_up_shortcut */
|
||||
shortcuts
|
||||
.get(Shortcuts::COMPOSING)
|
||||
|
@ -92,8 +92,8 @@ impl EditAttachmentsRefMut<'_, '_> {
|
|||
.unwrap_or_else(|| context.settings.shortcuts.composing.scroll_down.clone()),
|
||||
);
|
||||
|
||||
ret.add_button(("Reset".into(), FormButtonAction::Reset));
|
||||
ret.add_button(("Cancel".into(), FormButtonAction::Cancel));
|
||||
ret.add_button(("Reset".into(), FormButtonActions::Reset));
|
||||
ret.add_button(("Cancel".into(), FormButtonActions::Cancel));
|
||||
ret.push(("Filename".into(), filename.unwrap_or_default().to_string()));
|
||||
ret.push(("Mime type".into(), mime_type.to_string()));
|
||||
Some(Box::new(ret))
|
||||
|
@ -132,7 +132,6 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
grid.write_string(
|
||||
|
@ -142,7 +141,6 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for (i, a) in self.draft.attachments().iter().enumerate() {
|
||||
let bg = if let EditAttachmentCursor::AttachmentNo(u) = self.inner.cursor {
|
||||
|
@ -176,7 +174,6 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
theme_default.attrs,
|
||||
area.skip(2, 2 + i),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -198,10 +195,10 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
{
|
||||
if inner.process_event(event, context) {
|
||||
match inner.buttons_result() {
|
||||
Some(FormButtonAction::Accept) | Some(FormButtonAction::Cancel) => {
|
||||
Some(FormButtonActions::Accept) | Some(FormButtonActions::Cancel) => {
|
||||
self.inner.mode = EditAttachmentMode::Overview;
|
||||
}
|
||||
Some(FormButtonAction::Reset) => {
|
||||
Some(FormButtonActions::Reset) => {
|
||||
let no = *no;
|
||||
if let Some(inner) = self.new_edit_widget(no, context) {
|
||||
self.inner.mode = EditAttachmentMode::Edit { inner, no };
|
||||
|
|
|
@ -21,94 +21,15 @@
|
|||
|
||||
use super::*;
|
||||
|
||||
type KeylistJoinHandle = JoinHandle<Result<Vec<melib::gpgme::Key>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeySelectionLoading {
|
||||
handles: (KeylistJoinHandle, Vec<KeylistJoinHandle>),
|
||||
progress_spinner: ProgressSpinner,
|
||||
secret: bool,
|
||||
local: bool,
|
||||
patterns: (String, Vec<String>),
|
||||
allow_remote_lookup: ActionFlag,
|
||||
}
|
||||
|
||||
impl KeySelectionLoading {
|
||||
pub fn new(
|
||||
secret: bool,
|
||||
local: bool,
|
||||
patterns: (String, Vec<String>),
|
||||
allow_remote_lookup: ActionFlag,
|
||||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
use melib::gpgme::{self, *};
|
||||
let mut ctx = gpgme::Context::new()?;
|
||||
if local {
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
} else {
|
||||
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
|
||||
}
|
||||
let (pattern, other_patterns) = patterns;
|
||||
let main_job = ctx.keylist(secret, Some(pattern.clone()))?;
|
||||
let main_handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::keylist".into(),
|
||||
main_job,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let other_handles = other_patterns
|
||||
.iter()
|
||||
.map(|pattern| {
|
||||
let job = ctx.keylist(secret, Some(pattern.clone()))?;
|
||||
Ok(context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::keylist".into(),
|
||||
job,
|
||||
IsAsync::Blocking,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<KeylistJoinHandle>>>()?;
|
||||
let mut progress_spinner = ProgressSpinner::new(8, context);
|
||||
progress_spinner.start();
|
||||
Ok(Self {
|
||||
handles: (main_handle, other_handles),
|
||||
secret,
|
||||
local,
|
||||
patterns: (pattern, other_patterns),
|
||||
allow_remote_lookup,
|
||||
progress_spinner,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, rhs: Self) {
|
||||
let Self {
|
||||
handles: (_, ref mut other_handles),
|
||||
secret: _,
|
||||
local: _,
|
||||
patterns: (_, ref mut other_patterns),
|
||||
allow_remote_lookup: _,
|
||||
progress_spinner: _,
|
||||
} = self;
|
||||
let Self {
|
||||
handles: (rhs_handle, rhs_other_handles),
|
||||
patterns: (rhs_pattern, rhs_other_patterns),
|
||||
secret: _,
|
||||
local: _,
|
||||
allow_remote_lookup: _,
|
||||
progress_spinner: _,
|
||||
} = rhs;
|
||||
other_handles.push(rhs_handle);
|
||||
other_handles.extend(rhs_other_handles);
|
||||
other_patterns.push(rhs_pattern);
|
||||
other_patterns.extend(rhs_other_patterns);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum KeySelection {
|
||||
Loading {
|
||||
inner: KeySelectionLoading,
|
||||
/// Accumulate results from intermediate results (i.e. not the main
|
||||
/// pattern)
|
||||
keys_accumulator: Vec<melib::gpgme::Key>,
|
||||
LoadingKeys {
|
||||
handle: JoinHandle<Result<Vec<melib::gpgme::Key>>>,
|
||||
progress_spinner: ProgressSpinner,
|
||||
secret: bool,
|
||||
local: bool,
|
||||
pattern: String,
|
||||
allow_remote_lookup: ActionFlag,
|
||||
},
|
||||
Error {
|
||||
id: ComponentId,
|
||||
|
@ -120,31 +41,51 @@ pub enum KeySelection {
|
|||
},
|
||||
}
|
||||
|
||||
impl From<KeySelectionLoading> for KeySelection {
|
||||
fn from(inner: KeySelectionLoading) -> Self {
|
||||
Self::Loading {
|
||||
inner,
|
||||
keys_accumulator: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeySelection {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "select pgp keys")
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySelection {
|
||||
pub fn new(
|
||||
secret: bool,
|
||||
local: bool,
|
||||
pattern: String,
|
||||
allow_remote_lookup: ActionFlag,
|
||||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
use melib::gpgme::*;
|
||||
let mut ctx = Context::new()?;
|
||||
if local {
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
} else {
|
||||
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
|
||||
}
|
||||
let job = ctx.keylist(secret, Some(pattern.clone()))?;
|
||||
let handle = context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("gpg::keylist".into(), job);
|
||||
let mut progress_spinner = ProgressSpinner::new(8, context);
|
||||
progress_spinner.start();
|
||||
Ok(Self::LoadingKeys {
|
||||
handle,
|
||||
secret,
|
||||
local,
|
||||
pattern,
|
||||
allow_remote_lookup,
|
||||
progress_spinner,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for KeySelection {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
match self {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
Self::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
} => progress_spinner.draw(grid, area.center_inside((2, 2)), context),
|
||||
Self::Error { ref err, .. } => {
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
|
@ -154,7 +95,6 @@ impl Component for KeySelection {
|
|||
theme_default.bg,
|
||||
theme_default.attrs,
|
||||
area.center_inside((15, 2)),
|
||||
None,
|
||||
Some(0),
|
||||
);
|
||||
}
|
||||
|
@ -164,31 +104,16 @@ impl Component for KeySelection {
|
|||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
match self {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref mut progress_spinner,
|
||||
handles: (ref mut main_handle, ref mut other_handles),
|
||||
secret,
|
||||
local,
|
||||
patterns: (ref mut pattern, ref mut other_patterns),
|
||||
allow_remote_lookup,
|
||||
..
|
||||
},
|
||||
ref mut keys_accumulator,
|
||||
Self::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
ref mut handle,
|
||||
secret,
|
||||
local,
|
||||
ref mut pattern,
|
||||
allow_remote_lookup,
|
||||
..
|
||||
} => match event {
|
||||
UIEvent::StatusEvent(StatusEvent::JobFinished(ref id))
|
||||
if *id == main_handle.job_id
|
||||
|| other_handles.iter().any(|h| h.job_id == *id) =>
|
||||
{
|
||||
let mut main_handle_ref = &mut (*main_handle);
|
||||
let is_main = *id == main_handle_ref.job_id;
|
||||
let other_handle_ref_opt = other_handles.iter_mut().find(|h| h.job_id == *id);
|
||||
let handle = if is_main {
|
||||
&mut main_handle_ref
|
||||
} else {
|
||||
&mut (*other_handle_ref_opt.unwrap())
|
||||
};
|
||||
UIEvent::StatusEvent(StatusEvent::JobFinished(ref id)) if *id == handle.job_id => {
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => { /* Job was canceled */ }
|
||||
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
|
||||
|
@ -196,19 +121,15 @@ impl Component for KeySelection {
|
|||
if keys.is_empty() {
|
||||
let id = progress_spinner.id();
|
||||
if allow_remote_lookup.is_true() {
|
||||
match KeySelectionLoading::new(
|
||||
match Self::new(
|
||||
*secret,
|
||||
*local,
|
||||
(std::mem::take(pattern), std::mem::take(other_patterns)),
|
||||
std::mem::take(pattern),
|
||||
*allow_remote_lookup,
|
||||
context,
|
||||
) {
|
||||
Ok(inner) => {
|
||||
let keys_accumulator = std::mem::take(keys_accumulator);
|
||||
*self = Self::Loading {
|
||||
inner,
|
||||
keys_accumulator,
|
||||
};
|
||||
Ok(w) => {
|
||||
*self = w;
|
||||
}
|
||||
Err(err) => *self = Self::Error { err, id },
|
||||
}
|
||||
|
@ -222,11 +143,7 @@ impl Component for KeySelection {
|
|||
}
|
||||
} else {
|
||||
*self = Self::Error {
|
||||
err: if pattern.is_empty() {
|
||||
Error::new("No keys found.")
|
||||
} else {
|
||||
Error::new(format!("No keys found for {}.", pattern))
|
||||
},
|
||||
err: Error::new(format!("No keys found for {}.", pattern)),
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
@ -234,74 +151,42 @@ impl Component for KeySelection {
|
|||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(err.to_string()),
|
||||
));
|
||||
// Even in case of error, we should send a FinishedUIDialog
|
||||
// event so that the component parent knows we're done.
|
||||
let res: Option<Vec<melib::gpgme::Key>> = None;
|
||||
let res: Option<melib::gpgme::Key> = None;
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::FinishedUIDialog(id, Box::new(res)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
keys_accumulator.extend(keys);
|
||||
if !is_main {
|
||||
other_handles.retain(|h| h.job_id != *id);
|
||||
return false;
|
||||
}
|
||||
if other_handles.is_empty() {
|
||||
// We are done with all Futures, so finally transition into the
|
||||
// "show the user the list of keys to select" state.
|
||||
let mut widget = Box::new(UIDialog::new(
|
||||
"select key",
|
||||
keys_accumulator
|
||||
.iter()
|
||||
.map(|k| {
|
||||
(
|
||||
k.clone(),
|
||||
if let Some(primary_uid) = k.primary_uid() {
|
||||
format!("{} {}", k.fingerprint(), primary_uid)
|
||||
} else {
|
||||
k.fingerprint().to_string()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(melib::gpgme::Key, String)>>(),
|
||||
false,
|
||||
Some(Box::new(
|
||||
move |id: ComponentId, results: &[melib::gpgme::Key]| {
|
||||
Some(UIEvent::FinishedUIDialog(
|
||||
id,
|
||||
Box::new(if results.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(results.to_vec())
|
||||
}),
|
||||
))
|
||||
},
|
||||
)),
|
||||
context,
|
||||
));
|
||||
widget.set_dirty(true);
|
||||
*self = Self::Loaded {
|
||||
widget,
|
||||
keys: std::mem::take(keys_accumulator),
|
||||
};
|
||||
} else {
|
||||
// Main handle has finished, replace it with some other one from
|
||||
// other_handles.
|
||||
*main_handle = other_handles.remove(0);
|
||||
}
|
||||
let mut widget = Box::new(UIDialog::new(
|
||||
"select key",
|
||||
keys.iter()
|
||||
.map(|k| {
|
||||
(
|
||||
k.clone(),
|
||||
if let Some(primary_uid) = k.primary_uid() {
|
||||
format!("{} {}", k.fingerprint(), primary_uid)
|
||||
} else {
|
||||
k.fingerprint().to_string()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(melib::gpgme::Key, String)>>(),
|
||||
true,
|
||||
Some(Box::new(
|
||||
move |id: ComponentId, results: &[melib::gpgme::Key]| {
|
||||
Some(UIEvent::FinishedUIDialog(
|
||||
id,
|
||||
Box::new(results.first().cloned()),
|
||||
))
|
||||
},
|
||||
)),
|
||||
context,
|
||||
));
|
||||
widget.set_dirty(true);
|
||||
*self = Self::Loaded { widget, keys };
|
||||
}
|
||||
Ok(Some(Err(err))) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(err.to_string()),
|
||||
));
|
||||
// Even in case of error, we should send a FinishedUIDialog
|
||||
// event so that the component parent knows we're done.
|
||||
let res: Option<Vec<melib::gpgme::Key>> = None;
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::FinishedUIDialog(self.id(), Box::new(res)));
|
||||
*self = Self::Error {
|
||||
err,
|
||||
id: ComponentId::default(),
|
||||
|
@ -319,13 +204,9 @@ impl Component for KeySelection {
|
|||
|
||||
fn is_dirty(&self) -> bool {
|
||||
match self {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
Self::LoadingKeys {
|
||||
ref progress_spinner,
|
||||
..
|
||||
} => progress_spinner.is_dirty(),
|
||||
Self::Error { .. } => true,
|
||||
Self::Loaded { ref widget, .. } => widget.is_dirty(),
|
||||
|
@ -334,13 +215,9 @@ impl Component for KeySelection {
|
|||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
match self {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
Self::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
} => progress_spinner.set_dirty(value),
|
||||
Self::Error { .. } => {}
|
||||
Self::Loaded { ref mut widget, .. } => widget.set_dirty(value),
|
||||
|
@ -351,20 +228,16 @@ impl Component for KeySelection {
|
|||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
match self {
|
||||
Self::Loading { .. } | Self::Error { .. } => ShortcutMaps::default(),
|
||||
Self::LoadingKeys { .. } | Self::Error { .. } => ShortcutMaps::default(),
|
||||
Self::Loaded { ref widget, .. } => widget.shortcuts(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
match self {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
Self::LoadingKeys {
|
||||
ref progress_spinner,
|
||||
..
|
||||
} => progress_spinner.id(),
|
||||
Self::Error { ref id, .. } => *id,
|
||||
Self::Loaded { ref widget, .. } => widget.id(),
|
||||
|
@ -392,183 +265,3 @@ impl Default for GpgComposeState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{borrow::Cow, ffi::CString, thread::sleep, time::Duration};
|
||||
|
||||
use melib::gpgme::{EngineInfo, LocateKey, Protocol};
|
||||
use rusty_fork::rusty_fork_test;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl KeySelection {
|
||||
fn new_mock(
|
||||
secret: bool,
|
||||
local: bool,
|
||||
pattern: String,
|
||||
allow_remote_lookup: ActionFlag,
|
||||
context: &Context,
|
||||
ctx: &mut melib::gpgme::Context,
|
||||
) -> Result<Self> {
|
||||
if local {
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
} else {
|
||||
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
|
||||
}
|
||||
let job = ctx.keylist(secret, Some(pattern.clone()))?;
|
||||
let handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::keylist".into(),
|
||||
job,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let mut progress_spinner = ProgressSpinner::new(8, context);
|
||||
progress_spinner.start();
|
||||
Ok(Self::Loading {
|
||||
inner: KeySelectionLoading {
|
||||
handles: (handle, vec![]),
|
||||
secret,
|
||||
local,
|
||||
patterns: (pattern, vec![]),
|
||||
allow_remote_lookup,
|
||||
progress_spinner,
|
||||
},
|
||||
keys_accumulator: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const PUBKEY: &[u8]=b"-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: GnuPG v2.1.0-gitb3c71eb (GNU/Linux)\r\n\r\nmQGiBDo41NoRBADSfQazKGYf8nokq6zUKH/6INtV6MypSzSGmX2XErnARkIIPPYj\r\ncQRQ8zCbGV7ZU2ezVbzhFLUSJveE8PZUzzCrLp1O2NSyBTRcR5HVSXW95nJfY8eV\r\npOvZRAKul0BVLh81kYTsrfzaaCjh9VWNP26LoeN2r+PjZyktXe7gM3C4SwCgoTxK\r\nWUVi9HoT2HCLY7p7oig5hEcEALdCJal0UYomX3nJapIVLVZg3vkidr1RICYMb2vz\r\n58i17h8sxEtobD1vdIKNejulntaRAXs4n0tDYD9z7pRlwG1CLz1R9WxYzeOOqUDr\r\nfnVXdmU8L/oVWABat8v1V7QQhjMMf+41fuzVwDMMGqjVPLhu4X6wp3A8uyM3YDnQ\r\nVMN1A/4n2G5gHoOvjqxn8Ch5tBAdMGfO8gH4RjQOwzm2R1wPQss/yzUN1+tlMZGX\r\nK2dQ2FCWC/hDUSNaEQRlI15wxxBNZ2RQwlzE2A8v113DpvyzOtv0QO95gJ1teCXC\r\n7j/BN9asgHaBBc39JLO/TcpuI7Hf8PQ5VcP2F0UE3lczGhXbLLRESm9lIFJhbmRv\r\nbSBIYWNrZXIgKHRlc3Qga2V5IHdpdGggcGFzc3BocmFzZSAiYWJjIikgPGpvZUBl\r\neGFtcGxlLmNvbT6IYgQTEQIAIgUCTbdXqQIbIwYLCQgHAwIGFQgCCQoLBBYCAwEC\r\nHgECF4AACgkQr4IkT5zZ/VUcCACfQvSPi//9/gBv8SVrK6O4DiyD+jAAn3LEnfF1\r\n4j6MjwlqXTqol2VgQn1yuQENBDo41N0QBACedJb7Qhm50JSPe1V+rSZKLHT5nc3l\r\n2k1n7//wNsJkgDW2J7snIRjGtSzeNxMPh+hVzFidzAf3sbOlARQoBrMPPKpnJWtm\r\n6LEDf2lSwO36l0/bo6qDRmiFRJoHWytTJEjxVwRclVt4bXqHfNw9FKhZZbcKeAN2\r\noHgmBVSU6edHdwADBQP+OGAkEG4PcfSb8x191R+wkV/q2hA5Ay9z289Dx2rO28CO\r\n4M2fhhcjSmgr6x0DsrkfESCiG47UGJ169eu+QqJwk3HiF4crGN9rE5+VelBVFtrd\r\nMWkX2rPLGQWyw8iCZKbeH8g/ujmkaLovSmalzDcLe4v1xSLaP7Fnfzit0iIGZAGI\r\nRgQYEQIABgUCOjjU3QAKCRCvgiRPnNn9VVSaAJ9+rj1lIQnRl20i8Rom2Hwbe3re\r\n9QCfSYFnkZUw0yKF2DfCfqrDzdGAsbaIRgQYEQIABgUCOjjU3gAKCRCvgiRPnNn9\r\nVe4iAJ9FrGMlFR7s+GWf1scTeeyrthKrPQCfSpc/Yps72aFI7hPfyIa9MuerVZ4=\r\n=QRit\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n";
|
||||
|
||||
rusty_fork_test! {
|
||||
#[test]
|
||||
fn test_gpg_verify_sig() {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
{
|
||||
#[allow(unused_unsafe)]
|
||||
unsafe {
|
||||
std::env::set_var("GNUPGHOME", tempdir.path());
|
||||
}
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
unsafe {
|
||||
std::env::set_var("GPG_AGENT_INFO", "");
|
||||
}
|
||||
}
|
||||
|
||||
let mut ctx = Context::new_mock(&tempdir);
|
||||
let mut gpgme_ctx = match melib::gpgme::Context::new() {
|
||||
Ok(v) => v,
|
||||
Err(err) if err.kind.is_not_found() => {
|
||||
eprintln!("INFO: libgpgme could not be loaded, skipping this test.");
|
||||
return;
|
||||
}
|
||||
err => err.unwrap(),
|
||||
};
|
||||
let current_engine_info = gpgme_ctx.engine_info().unwrap();
|
||||
let prev_len = current_engine_info.len();
|
||||
let Some(EngineInfo {
|
||||
file_name: Some(engine_file_name),
|
||||
..
|
||||
}) = current_engine_info
|
||||
.into_iter()
|
||||
.find(|eng| eng.protocol == Protocol::OpenPGP)
|
||||
else {
|
||||
eprintln!("WARN: No openpg protocol engine returned from gpgme.");
|
||||
return;
|
||||
};
|
||||
gpgme_ctx
|
||||
.set_engine_info(
|
||||
Protocol::OpenPGP,
|
||||
Some(Cow::Owned(CString::new(engine_file_name).unwrap())),
|
||||
Some(Cow::Owned(
|
||||
CString::new(tempdir.path().display().to_string()).unwrap(),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
let new_engine_info = gpgme_ctx.engine_info().unwrap();
|
||||
assert_eq!(
|
||||
new_engine_info.len(),
|
||||
prev_len,
|
||||
"new_engine_info was expected to have {} entry/ies but has {}: {:#?}",
|
||||
prev_len,
|
||||
new_engine_info.len(),
|
||||
new_engine_info
|
||||
);
|
||||
assert_eq!(
|
||||
new_engine_info[0].home_dir,
|
||||
Some(tempdir.path().display().to_string()),
|
||||
"new_engine_info was expected to have temp dir as home_dir but has: {:#?}",
|
||||
new_engine_info[0].home_dir
|
||||
);
|
||||
let mut pubkey_data = Some(gpgme_ctx.new_data_mem(PUBKEY).unwrap());
|
||||
for _ in 0..2 {
|
||||
let mut key_sel = KeySelection::new_mock(
|
||||
false,
|
||||
true,
|
||||
"".to_string(),
|
||||
false.into(),
|
||||
&ctx,
|
||||
&mut gpgme_ctx,
|
||||
)
|
||||
.unwrap();
|
||||
let component_id = key_sel.id();
|
||||
|
||||
for _ in 0..2 {
|
||||
sleep(Duration::from_secs(2));
|
||||
}
|
||||
while let Ok(ev) = ctx.receiver.try_recv() {
|
||||
// if !matches!(ev, ThreadEvent::UIEvent(UIEvent::Timer(_))) {
|
||||
// dbg!(&ev);
|
||||
// }
|
||||
if let ThreadEvent::UIEvent(mut ev) = ev {
|
||||
key_sel.process_event(&mut ev, &mut ctx);
|
||||
} else if let ThreadEvent::JobFinished(job_id) = ev {
|
||||
let mut ev = UIEvent::StatusEvent(StatusEvent::JobFinished(job_id));
|
||||
key_sel.process_event(&mut ev, &mut ctx);
|
||||
}
|
||||
}
|
||||
if let Some(pubkey_data) = pubkey_data.take() {
|
||||
assert!(
|
||||
matches!(
|
||||
key_sel,
|
||||
KeySelection::Error {
|
||||
ref id,
|
||||
ref err
|
||||
} if *id == component_id && err.to_string() == melib::Error::new("No keys found.").to_string(),
|
||||
),
|
||||
"key_sel should have been an error but is: {:?}",
|
||||
key_sel
|
||||
);
|
||||
gpgme_ctx.import_key(pubkey_data).unwrap();
|
||||
} else {
|
||||
let assert_key = |key: &melib::gpgme::Key| {
|
||||
key.fingerprint() == "ADAB7FCC1F4DE2616ECFA402AF82244F9CD9FD55"
|
||||
&& key.primary_uid()
|
||||
== Some(melib::Address::new(
|
||||
Some("Joe Random Hacker".into()),
|
||||
"joe@example.com".into(),
|
||||
))
|
||||
&& key.can_encrypt()
|
||||
&& key.can_sign()
|
||||
&& !key.secret()
|
||||
&& !key.revoked()
|
||||
&& !key.expired()
|
||||
&& !key.invalid()
|
||||
};
|
||||
assert!(
|
||||
matches!(
|
||||
key_sel,
|
||||
KeySelection::Loaded {
|
||||
ref keys,
|
||||
widget: _,
|
||||
} if keys.len() == 1 && assert_key(&keys[0]),
|
||||
),
|
||||
"key_sel should have been an error but is: {:?}",
|
||||
key_sel
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
//! Pre-submission hooks for draft validation and/or transformations.
|
||||
pub use std::borrow::Cow;
|
||||
|
||||
use melib::{email::headers::HeaderName, src_err_arc_wrap};
|
||||
use melib::email::headers::HeaderName;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -36,8 +36,8 @@ pub enum HookFn {
|
|||
}
|
||||
|
||||
impl std::fmt::Debug for HookFn {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct(melib::identify!(HookFn))
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_struct(stringify!(HookFn))
|
||||
.field(
|
||||
"kind",
|
||||
&match self {
|
||||
|
@ -102,11 +102,11 @@ impl Hook {
|
|||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|err| -> Error {
|
||||
Error::new(format!(
|
||||
format!(
|
||||
"could not execute `{command}`. Check if its binary is in PATH or if \
|
||||
the command is valid."
|
||||
))
|
||||
.set_source(Some(src_err_arc_wrap! {err}))
|
||||
the command is valid. Original error: {err}"
|
||||
)
|
||||
.into()
|
||||
})?;
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
|
@ -121,8 +121,7 @@ impl Hook {
|
|||
});
|
||||
});
|
||||
let output = child.wait_with_output().map_err(|err| -> Error {
|
||||
Error::new(format!("failed to wait on hook child {name_}"))
|
||||
.set_source(Some(src_err_arc_wrap! {err}))
|
||||
format!("failed to wait on hook child {name_}: {err}").into()
|
||||
})?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
@ -188,10 +187,7 @@ fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
|||
for hdr in [HeaderName::FROM, HeaderName::TO] {
|
||||
match draft.headers.get(&hdr).map(melib::Address::list_try_from) {
|
||||
Some(Ok(_)) => {}
|
||||
Some(Err(err)) => {
|
||||
return Err(Error::new(format!("{hdr} header value is invalid"))
|
||||
.set_source(Some(src_err_arc_wrap! {err})))
|
||||
}
|
||||
Some(Err(err)) => return Err(format!("{hdr} header value is invalid ({err}).").into()),
|
||||
None => return Err(format!("{hdr} header is missing and should be present.").into()),
|
||||
}
|
||||
}
|
||||
|
@ -202,11 +198,8 @@ fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
|||
.get(HeaderName::DATE)
|
||||
.map(melib::utils::datetime::rfc822_to_timestamp)
|
||||
{
|
||||
Some(Err(err)) => {
|
||||
return Err(Error::new("Date header value is invalid.")
|
||||
.set_source(Some(src_err_arc_wrap! {err})))
|
||||
}
|
||||
Some(Ok(0)) => return Err(Error::new("Date header value is invalid.")),
|
||||
Some(Err(err)) => return Err(format!("Date header value is invalid ({err}).").into()),
|
||||
Some(Ok(0)) => return Err("Date header value is invalid.".into()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -218,8 +211,7 @@ fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
|||
.filter(|v| !v.trim().is_empty())
|
||||
.map(melib::Address::list_try_from)
|
||||
{
|
||||
return Err(Error::new(format!("{hdr} header value is invalid"))
|
||||
.set_source(Some(src_err_arc_wrap! {err})));
|
||||
return Err(format!("{hdr} header value is invalid ({err}).").into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -318,9 +310,8 @@ mod tests {
|
|||
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
|
||||
assert_eq!(
|
||||
err_msg,
|
||||
"From header value is invalid\nCaused by:\n[2] Parsing error. In input: \
|
||||
\"...\",\nError: Alternative, Many1, Alternative, atom(): starts with whitespace or \
|
||||
empty",
|
||||
"From header value is invalid (Parsing error. In input: \"...\",\nError: Alternative, \
|
||||
Many1, Alternative, atom(): starts with whitespace or empty).",
|
||||
"HEADERWARN should complain about From value being empty: {}",
|
||||
err_msg
|
||||
);
|
||||
|
@ -330,9 +321,8 @@ mod tests {
|
|||
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
|
||||
assert_eq!(
|
||||
err_msg,
|
||||
"To header value is invalid\nCaused by:\n[2] Parsing error. In input: \
|
||||
\"...\",\nError: Alternative, Many1, Alternative, atom(): starts with whitespace or \
|
||||
empty",
|
||||
"To header value is invalid (Parsing error. In input: \"...\",\nError: Alternative, \
|
||||
Many1, Alternative, atom(): starts with whitespace or empty).",
|
||||
"HEADERWARN should complain about To value being empty: {}",
|
||||
err_msg
|
||||
);
|
||||
|
@ -360,8 +350,8 @@ mod tests {
|
|||
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
|
||||
assert_eq!(
|
||||
err_msg,
|
||||
"From header value is invalid\nCaused by:\n[2] Parsing error. In input: \"user \
|
||||
user@example.com>...\",\nError: Alternative, Tag",
|
||||
"From header value is invalid (Parsing error. In input: \"user \
|
||||
user@example.com>...\",\nError: Alternative, Tag).",
|
||||
"HEADERWARN should complain about From value being invalid: {}",
|
||||
err_msg
|
||||
);
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
*/
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
convert::TryFrom,
|
||||
fs::File,
|
||||
|
@ -41,7 +40,6 @@ use super::*;
|
|||
use crate::{
|
||||
accounts::{JobRequest, MailboxStatus},
|
||||
components::ExtendShortcutsMaps,
|
||||
jobs::IsAsync,
|
||||
};
|
||||
|
||||
pub const DEFAULT_ATTACHMENT_FLAG: &str = concat!("📎", emoji_text_presentation_selector!());
|
||||
|
@ -113,8 +111,9 @@ impl<T> RowsState<T> {
|
|||
self.env_order.insert(env_hash, index);
|
||||
self.all_envelopes.insert(env_hash);
|
||||
}
|
||||
if self.all_threads.insert(thread) {
|
||||
if !self.all_threads.contains(&thread) {
|
||||
self.thread_order.insert(thread, index);
|
||||
self.all_threads.insert(thread);
|
||||
self.thread_to_env.insert(thread, env_hashes);
|
||||
} else {
|
||||
self.thread_to_env
|
||||
|
@ -205,7 +204,6 @@ impl<T> RowsState<T> {
|
|||
self.env_order.insert(new_hash, row);
|
||||
}
|
||||
if let Some(thread) = self.env_to_thread.remove(&old_hash) {
|
||||
self.env_to_thread.insert(new_hash, thread);
|
||||
self.thread_to_env
|
||||
.entry(thread)
|
||||
.or_default()
|
||||
|
@ -215,7 +213,7 @@ impl<T> RowsState<T> {
|
|||
let selection_status = self.selection.remove(&old_hash).unwrap_or(false);
|
||||
self.selection.insert(new_hash, selection_status);
|
||||
self.all_envelopes.remove(&old_hash);
|
||||
self.all_envelopes.insert(new_hash);
|
||||
self.all_envelopes.insert(old_hash);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,17 +248,6 @@ pub enum Modifier {
|
|||
Intersection,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Modifier {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::SymmetricDifference => write!(fmt, "><"),
|
||||
Self::Union => write!(fmt, "+"),
|
||||
Self::Difference => write!(fmt, "-"),
|
||||
Self::Intersection => write!(fmt, "*"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// Save theme colors to avoid looking them up again and again from settings
|
||||
pub struct ColorCache {
|
||||
|
@ -281,7 +268,6 @@ 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,
|
||||
|
@ -291,12 +277,6 @@ 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"),
|
||||
|
@ -318,7 +298,9 @@ impl ColorCache {
|
|||
"mail.listing.plain.even_highlighted_selected",
|
||||
),
|
||||
odd_selected: crate::conf::value(context, "mail.listing.plain.odd_selected"),
|
||||
..default
|
||||
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
|
||||
theme_default: crate::conf::value(context, "theme_default"),
|
||||
..Self::default()
|
||||
},
|
||||
IndexStyle::Threaded => Self {
|
||||
even_unseen: crate::conf::value(context, "mail.listing.plain.even_unseen"),
|
||||
|
@ -340,7 +322,9 @@ impl ColorCache {
|
|||
),
|
||||
even: crate::conf::value(context, "mail.listing.plain.even"),
|
||||
odd: crate::conf::value(context, "mail.listing.plain.odd"),
|
||||
..default
|
||||
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
|
||||
theme_default: crate::conf::value(context, "theme_default"),
|
||||
..Self::default()
|
||||
},
|
||||
IndexStyle::Compact => Self {
|
||||
even_unseen: crate::conf::value(context, "mail.listing.compact.even_unseen"),
|
||||
|
@ -365,9 +349,12 @@ impl ColorCache {
|
|||
),
|
||||
even: crate::conf::value(context, "mail.listing.compact.even"),
|
||||
odd: crate::conf::value(context, "mail.listing.compact.odd"),
|
||||
..default
|
||||
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
|
||||
theme_default: crate::conf::value(context, "theme_default"),
|
||||
..Self::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",
|
||||
),
|
||||
..default
|
||||
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
|
||||
..Self::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,7 +388,6 @@ pub struct EntryStrings {
|
|||
pub flag: FlagString,
|
||||
pub from: FromString,
|
||||
pub tags: TagString,
|
||||
pub unseen: bool,
|
||||
pub highlight_self: bool,
|
||||
}
|
||||
|
||||
|
@ -684,51 +670,6 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
}
|
||||
ListingAction::SendToTrash => {
|
||||
use melib::backends::SpecialUsageMailbox;
|
||||
|
||||
let Some(trash_mbox_hash) = account
|
||||
.special_use_mailbox(SpecialUsageMailbox::Trash)
|
||||
.or_else(|| account.special_use_mailbox(SpecialUsageMailbox::Junk))
|
||||
else {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(
|
||||
"Cannot send mail to trash because no Trash folder is configured."
|
||||
.to_string(),
|
||||
),
|
||||
));
|
||||
return;
|
||||
};
|
||||
let job = account.backend.write().unwrap().copy_messages(
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
trash_mbox_hash,
|
||||
/* move? */ true,
|
||||
);
|
||||
match job {
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(err.to_string()),
|
||||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account.main_loop_handler.job_executor.spawn(
|
||||
"move-to-trash".into(),
|
||||
fut,
|
||||
account.is_async(),
|
||||
);
|
||||
account.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
name: "taking out the trash".into(),
|
||||
handle,
|
||||
on_finish: None,
|
||||
log_level: LogLevel::INFO,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ListingAction::Delete => {
|
||||
let job = account
|
||||
.backend
|
||||
|
@ -742,11 +683,10 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account.main_loop_handler.job_executor.spawn(
|
||||
"delete".into(),
|
||||
fut,
|
||||
account.is_async(),
|
||||
);
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("delete".into(), fut);
|
||||
account.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::DeleteMessages { env_hashes, handle },
|
||||
|
@ -771,11 +711,10 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account.main_loop_handler.job_executor.spawn(
|
||||
"copy-to-mailbox".into(),
|
||||
fut,
|
||||
account.is_async(),
|
||||
);
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("copy_to_mailbox".into(), fut);
|
||||
account.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
|
@ -812,11 +751,10 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account.main_loop_handler.job_executor.spawn(
|
||||
"move-to-mailbox".into(),
|
||||
fut,
|
||||
account.is_async(),
|
||||
);
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("move_to_mailbox".into(), fut);
|
||||
account.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
|
@ -832,7 +770,9 @@ pub trait MailListingTrait: ListingTrait {
|
|||
ListingAction::ExportMbox(format, ref path) => {
|
||||
let futures: Result<Vec<_>> = envs_to_set
|
||||
.iter()
|
||||
.map(|&env_hash| account.operation(env_hash).and_then(|op| op.as_bytes()))
|
||||
.map(|&env_hash| {
|
||||
account.operation(env_hash).and_then(|mut op| op.as_bytes())
|
||||
})
|
||||
.collect::<Result<Vec<_>>>();
|
||||
let mut path = path.to_path_buf();
|
||||
if path.is_relative() {
|
||||
|
@ -855,7 +795,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
.collect();
|
||||
if path.is_dir() {
|
||||
if envs.len() == 1 {
|
||||
path.push(format!("{}.mbox", envs[0].message_id()));
|
||||
path.push(format!("{}.mbox", envs[0].message_id_raw()));
|
||||
} else {
|
||||
let now = datetime::timestamp_to_string(
|
||||
datetime::now(),
|
||||
|
@ -865,18 +805,12 @@ pub trait MailListingTrait: ListingTrait {
|
|||
path.push(format!(
|
||||
"{}-{}-{}_envelopes.mbox",
|
||||
now,
|
||||
envs[0].message_id(),
|
||||
envs[0].message_id_raw(),
|
||||
envs.len(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut file = BufWriter::new(
|
||||
File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&path)?,
|
||||
);
|
||||
let mut file = BufWriter::new(File::create(&path)?);
|
||||
let mut iter = envs.iter().zip(bytes);
|
||||
let tags_lck = collection.tag_index.read().unwrap();
|
||||
if let Some((env, ref bytes)) = iter.next() {
|
||||
|
@ -920,11 +854,10 @@ pub trait MailListingTrait: ListingTrait {
|
|||
let _ = sender.send(r);
|
||||
Ok(())
|
||||
});
|
||||
let handle = account.main_loop_handler.job_executor.spawn(
|
||||
"exporting-mbox".into(),
|
||||
fut,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_blocking("exporting mbox".into(), fut);
|
||||
account.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
|
@ -983,8 +916,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
}
|
||||
|
||||
fn row_updates(&mut self) -> &mut SmallVec<[EnvelopeHash; 8]>;
|
||||
fn selection(&self) -> &HashMap<EnvelopeHash, bool>;
|
||||
fn selection_mut(&mut self) -> &mut HashMap<EnvelopeHash, bool>;
|
||||
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool>;
|
||||
fn get_focused_items(&self, _context: &Context) -> SmallVec<[EnvelopeHash; 8]>;
|
||||
fn redraw_threads_list(
|
||||
&mut self,
|
||||
|
@ -1011,11 +943,16 @@ pub trait ListingTrait: Component {
|
|||
fn prev_entry(&mut self, context: &mut Context);
|
||||
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);
|
||||
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context);
|
||||
fn filter(&mut self, _filter_term: String, _results: Vec<EnvelopeHash>, _context: &Context) {}
|
||||
fn filter(
|
||||
&mut self,
|
||||
_filter_term: String,
|
||||
_results: SmallVec<[EnvelopeHash; 512]>,
|
||||
_context: &Context,
|
||||
) {
|
||||
}
|
||||
fn unfocused(&self) -> bool;
|
||||
fn view_area(&self) -> Option<Area>;
|
||||
fn set_modifier_active(&mut self, _new_val: bool);
|
||||
fn modifier_active(&self) -> bool;
|
||||
fn set_modifier_command(&mut self, _new_val: Option<Modifier>);
|
||||
fn modifier_command(&self) -> Option<Modifier>;
|
||||
fn set_movement(&mut self, mvm: PageMovement);
|
||||
|
@ -1040,7 +977,7 @@ pub trait ListingTrait: Component {
|
|||
let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(epoch);
|
||||
let now: std::time::Duration = std::time::SystemTime::now()
|
||||
.duration_since(d)
|
||||
.unwrap_or_else(|_| std::time::Duration::new(u64::MAX, 0));
|
||||
.unwrap_or_else(|_| std::time::Duration::new(std::u64::MAX, 0));
|
||||
match now.as_secs() {
|
||||
n if context.settings.listing.recent_dates && n < 60 * 60 => format!(
|
||||
"{} minute{} ago",
|
||||
|
@ -1169,13 +1106,11 @@ pub struct Listing {
|
|||
show_menu_scrollbar: ShowMenuScrollbar,
|
||||
startup_checks_rate: RateLimit,
|
||||
id: ComponentId,
|
||||
// Configurable settings
|
||||
theme_default: ThemeAttribute,
|
||||
|
||||
sidebar_divider: char,
|
||||
sidebar_divider_theme: ThemeAttribute,
|
||||
mail_view_divider: char,
|
||||
mail_view_divider_theme: ThemeAttribute,
|
||||
// State
|
||||
|
||||
menu_visibility: bool,
|
||||
cmd_buf: String,
|
||||
/// This is the width of the right container to the entire width.
|
||||
|
@ -1263,22 +1198,6 @@ impl Component for Listing {
|
|||
if self.component.unfocused() {
|
||||
if let Some(ref mut view) = self.view {
|
||||
view.draw(grid, self.component.view_area().unwrap_or(area), context);
|
||||
if let Some(view_area) = self.component.view_area() {
|
||||
if view_area != area {
|
||||
let divider_area =
|
||||
area.nth_col(area.width() - view_area.width() - 1);
|
||||
for row in grid.bounds_iter(divider_area) {
|
||||
for c in row {
|
||||
grid[c]
|
||||
.set_ch(self.mail_view_divider)
|
||||
.set_fg(self.mail_view_divider_theme.fg)
|
||||
.set_bg(self.mail_view_divider_theme.bg)
|
||||
.set_attrs(self.mail_view_divider_theme.attrs);
|
||||
}
|
||||
}
|
||||
context.dirty_areas.push_back(divider_area);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1304,22 +1223,6 @@ impl Component for Listing {
|
|||
if self.component.unfocused() {
|
||||
if let Some(ref mut view) = self.view {
|
||||
view.draw(grid, self.component.view_area().unwrap_or(area), context);
|
||||
if let Some(view_area) = self.component.view_area() {
|
||||
if view_area != area {
|
||||
let divider_area =
|
||||
area.nth_col(area.width() - view_area.width() - 1);
|
||||
for row in grid.bounds_iter(divider_area) {
|
||||
for c in row {
|
||||
grid[c]
|
||||
.set_ch(self.mail_view_divider)
|
||||
.set_fg(self.mail_view_divider_theme.fg)
|
||||
.set_bg(self.mail_view_divider_theme.bg)
|
||||
.set_attrs(self.mail_view_divider_theme.attrs);
|
||||
}
|
||||
}
|
||||
context.dirty_areas.push_back(divider_area);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1335,9 +1238,6 @@ impl Component for Listing {
|
|||
self.sidebar_divider =
|
||||
*account_settings!(context[account_hash].listing.sidebar_divider);
|
||||
self.sidebar_divider_theme = conf::value(context, "mail.sidebar_divider");
|
||||
self.mail_view_divider =
|
||||
*account_settings!(context[account_hash].listing.mail_view_divider);
|
||||
self.mail_view_divider_theme = conf::value(context, "mail.view.divider");
|
||||
self.menu.grid_mut().empty();
|
||||
self.set_dirty(true);
|
||||
}
|
||||
|
@ -1875,8 +1775,7 @@ impl Component for Listing {
|
|||
| Action::Listing(a @ ListingAction::MoveToOtherAccount(_, _))
|
||||
| Action::Listing(a @ ListingAction::ExportMbox(_, _))
|
||||
| Action::Listing(a @ ListingAction::Flag(_))
|
||||
| Action::Listing(a @ ListingAction::Tag(_))
|
||||
| Action::Listing(a @ ListingAction::SendToTrash) => {
|
||||
| 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!(
|
||||
|
@ -1884,10 +1783,9 @@ impl Component for Listing {
|
|||
ListingAction::Delete
|
||||
| ListingAction::MoveTo(_)
|
||||
| ListingAction::MoveToOtherAccount(_, _)
|
||||
| ListingAction::SendToTrash
|
||||
);
|
||||
let mut row_updates: SmallVec<[EnvelopeHash; 8]> = SmallVec::new();
|
||||
for (k, v) in self.component.selection_mut().iter_mut() {
|
||||
for (k, v) in self.component.selection().iter_mut() {
|
||||
if *v {
|
||||
*v = !should_be_unselected;
|
||||
row_updates.push(*k);
|
||||
|
@ -1901,7 +1799,7 @@ impl Component for Listing {
|
|||
let row_updates: SmallVec<[EnvelopeHash; 8]> =
|
||||
self.component.get_focused_items(context);
|
||||
for h in &row_updates {
|
||||
if let Some(val) = self.component.selection_mut().get_mut(h) {
|
||||
if let Some(val) = self.component.selection().get_mut(h) {
|
||||
*val = false;
|
||||
}
|
||||
}
|
||||
|
@ -1912,11 +1810,7 @@ impl Component for Listing {
|
|||
_ => {}
|
||||
},
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::LISTING]["scroll_up"])
|
||||
|| matches!(
|
||||
key,
|
||||
Key::Mouse(MouseEvent::Press(MouseButton::WheelUp, _, __))
|
||||
) =>
|
||||
if shortcut!(key == shortcuts[Shortcuts::LISTING]["scroll_up"]) =>
|
||||
{
|
||||
let amount = if self.cmd_buf.is_empty() {
|
||||
1
|
||||
|
@ -1939,11 +1833,7 @@ impl Component for Listing {
|
|||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::LISTING]["scroll_down"])
|
||||
|| matches!(
|
||||
key,
|
||||
Key::Mouse(MouseEvent::Press(MouseButton::WheelDown, _, __))
|
||||
) =>
|
||||
if shortcut!(key == shortcuts[Shortcuts::LISTING]["scroll_down"]) =>
|
||||
{
|
||||
let amount = if self.cmd_buf.is_empty() {
|
||||
1
|
||||
|
@ -2060,26 +1950,12 @@ impl Component for Listing {
|
|||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
|
||||
{
|
||||
if !self.cmd_buf.is_empty() {
|
||||
self.cmd_buf.clear();
|
||||
self.component.set_modifier_active(false);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
|
||||
}
|
||||
self.component.set_movement(PageMovement::Home);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
|
||||
{
|
||||
if !self.cmd_buf.is_empty() {
|
||||
self.cmd_buf.clear();
|
||||
self.component.set_modifier_active(false);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
|
||||
}
|
||||
self.component.set_movement(PageMovement::End);
|
||||
return true;
|
||||
}
|
||||
|
@ -2102,15 +1978,6 @@ impl Component for Listing {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::LISTING]["send_to_trash"]) =>
|
||||
{
|
||||
let mut event =
|
||||
UIEvent::Action(Action::Listing(ListingAction::SendToTrash));
|
||||
if self.process_event(&mut event, context) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::LISTING]["refresh"]) =>
|
||||
{
|
||||
|
@ -2134,36 +2001,18 @@ impl Component for Listing {
|
|||
&& shortcut!(
|
||||
key == shortcuts[Shortcuts::LISTING]["union_modifier"]
|
||||
)
|
||||
&& self.component.modifier_active() =>
|
||||
&& self.component.modifier_command().is_some() =>
|
||||
{
|
||||
self.component.set_modifier_command(Some(Modifier::Union));
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
|
||||
if let Some(modf) = self.component.modifier_command() {
|
||||
format!("{} {}", modf, self.cmd_buf)
|
||||
} else {
|
||||
self.cmd_buf.clone()
|
||||
},
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if !self.component.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["diff_modifier"])
|
||||
&& self.component.modifier_active() =>
|
||||
&& self.component.modifier_command().is_some() =>
|
||||
{
|
||||
self.component
|
||||
.set_modifier_command(Some(Modifier::Difference));
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
|
||||
if let Some(modf) = self.component.modifier_command() {
|
||||
format!("{} {}", modf, self.cmd_buf)
|
||||
} else {
|
||||
self.cmd_buf.clone()
|
||||
},
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
|
@ -2171,19 +2020,10 @@ impl Component for Listing {
|
|||
&& shortcut!(
|
||||
key == shortcuts[Shortcuts::LISTING]["intersection_modifier"]
|
||||
)
|
||||
&& self.component.modifier_active() =>
|
||||
&& self.component.modifier_command().is_some() =>
|
||||
{
|
||||
self.component
|
||||
.set_modifier_command(Some(Modifier::Intersection));
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
|
||||
if let Some(modf) = self.component.modifier_command() {
|
||||
format!("{} {}", modf, self.cmd_buf)
|
||||
} else {
|
||||
self.cmd_buf.clone()
|
||||
},
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
|
@ -2202,14 +2042,14 @@ impl Component for Listing {
|
|||
self.component.prev_entry(context);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('\x1b'))
|
||||
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_mut().get_mut(h) {
|
||||
if let Some(val) = self.component.selection().get_mut(h) {
|
||||
*val = false;
|
||||
}
|
||||
}
|
||||
|
@ -2217,7 +2057,7 @@ impl Component for Listing {
|
|||
self.component.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('\x1b'))
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
||||
if !self.cmd_buf.is_empty() =>
|
||||
{
|
||||
self.cmd_buf.clear();
|
||||
|
@ -2233,11 +2073,7 @@ impl Component for Listing {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
|
||||
if let Some(modf) = self.component.modifier_command() {
|
||||
format!("{} {}", modf, self.cmd_buf)
|
||||
} else {
|
||||
self.cmd_buf.clone()
|
||||
},
|
||||
self.cmd_buf.clone(),
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
|
@ -2741,17 +2577,11 @@ impl Component for Listing {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
|
||||
if let Some(modf) = self.component.modifier_command() {
|
||||
format!("{} {}", modf, self.cmd_buf)
|
||||
} else {
|
||||
self.cmd_buf.clone()
|
||||
},
|
||||
self.cmd_buf.clone(),
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('\x1b'))
|
||||
if !self.cmd_buf.is_empty() =>
|
||||
{
|
||||
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
|
||||
|
@ -2765,11 +2595,7 @@ impl Component for Listing {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
|
||||
if let Some(modf) = self.component.modifier_command() {
|
||||
format!("{} {}", modf, self.cmd_buf)
|
||||
} else {
|
||||
self.cmd_buf.clone()
|
||||
},
|
||||
self.cmd_buf.clone(),
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
|
@ -2839,7 +2665,7 @@ impl Component for Listing {
|
|||
});
|
||||
let mut config_map = context.settings.shortcuts.listing.key_values();
|
||||
if self.focus != ListingFocus::Menu {
|
||||
config_map.shift_remove("open_mailbox");
|
||||
config_map.remove("open_mailbox");
|
||||
}
|
||||
map.insert(Shortcuts::LISTING, config_map);
|
||||
|
||||
|
@ -2971,7 +2797,7 @@ impl Listing {
|
|||
account: 0,
|
||||
menu: MenuEntryCursor::Mailbox(0),
|
||||
},
|
||||
menu: Screen::<Virtual>::new(crate::conf::value(context, "mail.sidebar")),
|
||||
menu: Screen::<Virtual>::new(),
|
||||
menu_scrollbar_show_timer: context.main_loop_handler.job_executor.clone().create_timer(
|
||||
std::time::Duration::from_secs(0),
|
||||
std::time::Duration::from_millis(1200),
|
||||
|
@ -2988,10 +2814,6 @@ impl Listing {
|
|||
context[first_account_hash].listing.sidebar_divider
|
||||
),
|
||||
sidebar_divider_theme: conf::value(context, "mail.sidebar_divider"),
|
||||
mail_view_divider: *account_settings!(
|
||||
context[first_account_hash].listing.mail_view_divider
|
||||
),
|
||||
mail_view_divider_theme: conf::value(context, "mail.view.divider"),
|
||||
menu_visibility: !*account_settings!(
|
||||
context[first_account_hash].listing.hide_sidebar_on_launch
|
||||
),
|
||||
|
@ -3002,16 +2824,6 @@ impl Listing {
|
|||
cmd_buf: String::with_capacity(4),
|
||||
};
|
||||
ret.component.realize(ret.id().into(), context);
|
||||
{
|
||||
let _new_val = ret.cursor_pos.account;
|
||||
if let Some(idx) = context.accounts[_new_val]
|
||||
.default_mailbox()
|
||||
.and_then(|h| ret.accounts[_new_val].entry_by_hash(h))
|
||||
{
|
||||
ret.cursor_pos.menu = MenuEntryCursor::Mailbox(idx);
|
||||
ret.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(idx);
|
||||
}
|
||||
}
|
||||
ret.change_account(context);
|
||||
ret
|
||||
}
|
||||
|
@ -3134,65 +2946,7 @@ impl Listing {
|
|||
let must_highlight_account: bool = cursor.account == self.accounts[aidx].index;
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
let mail_sidebar_highlighted_value =
|
||||
crate::conf::value(context, "mail.sidebar_highlighted");
|
||||
let mail_sidebar_highlighted_account_name_value =
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_account_name");
|
||||
let mail_sidebar_account_name_value =
|
||||
crate::conf::value(context, "mail.sidebar_account_name");
|
||||
let mail_sidebar_highlighted_index_value =
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_index");
|
||||
let mail_sidebar_highlighted_unread_count_value =
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_unread_count");
|
||||
let mail_sidebar_highlighted_account_value =
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_account");
|
||||
let mail_sidebar_highlighted_account_index_value =
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_account_index");
|
||||
let mail_sidebar_highlighted_account_unread_count_value =
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_account_unread_count");
|
||||
let mail_sidebar_value = crate::conf::value(context, "mail.sidebar");
|
||||
let mail_sidebar_index_value = crate::conf::value(context, "mail.sidebar_index");
|
||||
let mail_sidebar_unread_count_value =
|
||||
crate::conf::value(context, "mail.sidebar_unread_count");
|
||||
let has_sibling_str: &str = account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.sidebar_mailbox_tree_has_sibling
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(" ");
|
||||
let no_sibling_str: &str = account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.sidebar_mailbox_tree_no_sibling
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(" ");
|
||||
|
||||
let has_sibling_leaf_str: &str = account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.sidebar_mailbox_tree_has_sibling_leaf
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(" ");
|
||||
|
||||
let no_sibling_leaf_str: &str = account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.sidebar_mailbox_tree_no_sibling_leaf
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(" ");
|
||||
let relative_menu_indices = *account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.relative_menu_indices
|
||||
);
|
||||
for (
|
||||
i,
|
||||
&MailboxMenuEntry {
|
||||
|
@ -3238,17 +2992,18 @@ impl Listing {
|
|||
|
||||
let account_attrs = if must_highlight_account {
|
||||
if cursor.menu == MenuEntryCursor::Status {
|
||||
let mut v = mail_sidebar_highlighted_value;
|
||||
let mut v = crate::conf::value(context, "mail.sidebar_highlighted");
|
||||
if !context.settings.terminal.use_color() {
|
||||
v.attrs |= Attr::REVERSE;
|
||||
}
|
||||
v
|
||||
} else {
|
||||
mail_sidebar_highlighted_account_name_value
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_account_name")
|
||||
}
|
||||
} else {
|
||||
mail_sidebar_account_name_value
|
||||
crate::conf::value(context, "mail.sidebar_account_name")
|
||||
};
|
||||
|
||||
// Print account name first
|
||||
self.menu.grid_mut().write_string(
|
||||
&self.accounts[aidx].name,
|
||||
|
@ -3257,7 +3012,6 @@ impl Listing {
|
|||
account_attrs.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
area = self.menu.area().skip_rows(account_y);
|
||||
|
||||
|
@ -3269,10 +3023,10 @@ impl Listing {
|
|||
account_attrs.attrs,
|
||||
area.skip_rows(1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
area = self.menu.area().skip_rows(account_y);
|
||||
|
||||
let lines_len = lines.len();
|
||||
let mut idx = 0;
|
||||
|
@ -3322,9 +3076,9 @@ impl Listing {
|
|||
_ => false,
|
||||
} {
|
||||
let mut ret = (
|
||||
mail_sidebar_highlighted_value,
|
||||
mail_sidebar_highlighted_index_value,
|
||||
mail_sidebar_highlighted_unread_count_value,
|
||||
crate::conf::value(context, "mail.sidebar_highlighted"),
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_index"),
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_unread_count"),
|
||||
);
|
||||
|
||||
if !context.settings.terminal.use_color() {
|
||||
|
@ -3335,19 +3089,21 @@ impl Listing {
|
|||
ret
|
||||
} else {
|
||||
(
|
||||
mail_sidebar_highlighted_account_value,
|
||||
mail_sidebar_highlighted_account_index_value,
|
||||
mail_sidebar_highlighted_account_unread_count_value,
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_account"),
|
||||
crate::conf::value(context, "mail.sidebar_highlighted_account_index"),
|
||||
crate::conf::value(
|
||||
context,
|
||||
"mail.sidebar_highlighted_account_unread_count",
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(
|
||||
mail_sidebar_value,
|
||||
mail_sidebar_index_value,
|
||||
mail_sidebar_unread_count_value,
|
||||
crate::conf::value(context, "mail.sidebar"),
|
||||
crate::conf::value(context, "mail.sidebar_index"),
|
||||
crate::conf::value(context, "mail.sidebar_unread_count"),
|
||||
)
|
||||
};
|
||||
self.menu.grid_mut().change_theme(area.nth_row(y + 1), att);
|
||||
|
||||
// Calculate how many columns the mailbox index tags should occupy with right
|
||||
// alignment, eg.
|
||||
|
@ -3366,8 +3122,48 @@ impl Listing {
|
|||
ctr
|
||||
};
|
||||
|
||||
let has_sibling_str: &str = account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.sidebar_mailbox_tree_has_sibling
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(" ");
|
||||
let no_sibling_str: &str = account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.sidebar_mailbox_tree_no_sibling
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(" ");
|
||||
|
||||
let has_sibling_leaf_str: &str = account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.sidebar_mailbox_tree_has_sibling_leaf
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(" ");
|
||||
|
||||
let no_sibling_leaf_str: &str = account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.sidebar_mailbox_tree_no_sibling_leaf
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(" ");
|
||||
|
||||
let (x, _) = self.menu.grid_mut().write_string(
|
||||
&if relative_menu_indices && must_highlight_account {
|
||||
&if *account_settings!(
|
||||
context[self.accounts[aidx].hash]
|
||||
.listing
|
||||
.relative_menu_indices
|
||||
) && must_highlight_account
|
||||
{
|
||||
format!(
|
||||
"{:>width$}",
|
||||
(l.inc - cursor.menu).abs(),
|
||||
|
@ -3381,7 +3177,6 @@ impl Listing {
|
|||
index_att.attrs,
|
||||
area.nth_row(y + 1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
area = self.menu.area().skip_rows(account_y);
|
||||
{
|
||||
|
@ -3415,7 +3210,6 @@ impl Listing {
|
|||
att.attrs,
|
||||
area.nth_row(y + 1).skip_cols(x),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.0
|
||||
+ x;
|
||||
|
@ -3431,55 +3225,44 @@ impl Listing {
|
|||
att.attrs,
|
||||
area.nth_row(y + 1).skip_cols(x),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.0
|
||||
+ x
|
||||
+ 1;
|
||||
+ x;
|
||||
area = self.menu.area().skip_rows(account_y);
|
||||
|
||||
// Unread message count
|
||||
let count_string: Cow<'static, str> = match (l.count, l.collapsed_count) {
|
||||
(None, None) if context.settings.terminal.ascii_drawing => "...".into(),
|
||||
(None, None) => "…".into(),
|
||||
(Some(0), None) => "".into(),
|
||||
(Some(0), Some(0)) | (None, Some(0)) => "v".into(),
|
||||
(Some(0), Some(coll)) => format!("({}) v", coll).into(),
|
||||
(Some(c), Some(0)) => format!("{} v", c).into(),
|
||||
(Some(c), Some(coll)) => format!("{} ({}) v", c, coll).into(),
|
||||
(Some(c), None) => format!("{}", c).into(),
|
||||
(None, Some(coll)) => format!("({}) v", coll).into(),
|
||||
let count_string = match (l.count, l.collapsed_count) {
|
||||
(None, None) => " ...".to_string(),
|
||||
(Some(0), None) => String::new(),
|
||||
(Some(0), Some(0)) | (None, Some(0)) => " v".to_string(),
|
||||
(Some(0), Some(coll)) => format!(" ({}) v", coll),
|
||||
(Some(c), Some(0)) => format!(" {} v", c),
|
||||
(Some(c), Some(coll)) => format!(" {} ({}) v", c, coll),
|
||||
(Some(c), None) => format!(" {}", c),
|
||||
(None, Some(coll)) => format!(" ({}) v", coll),
|
||||
};
|
||||
|
||||
let skip_cols = {
|
||||
let val = area.width().saturating_sub(count_string.len());
|
||||
let skip_cols = x.min(val);
|
||||
if skip_cols == val && matches!(self.show_menu_scrollbar, ShowMenuScrollbar::True) {
|
||||
skip_cols.saturating_sub(1)
|
||||
} else {
|
||||
skip_cols
|
||||
}
|
||||
};
|
||||
let (x, _) = self.menu.grid_mut().write_string(
|
||||
count_string.as_ref(),
|
||||
unread_count_att.fg,
|
||||
unread_count_att.bg,
|
||||
unread_count_att.attrs
|
||||
| if l.count.unwrap_or(0) > 0 {
|
||||
Attr::BOLD
|
||||
} else {
|
||||
Attr::DEFAULT
|
||||
},
|
||||
area.nth_row(y + 1).skip_cols(skip_cols),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
area = self.menu.area().skip_rows(account_y);
|
||||
for c in self
|
||||
let x = self
|
||||
.menu
|
||||
.grid_mut()
|
||||
.row_iter(area, (x + skip_cols)..area.width(), y + 1)
|
||||
{
|
||||
.write_string(
|
||||
&count_string,
|
||||
unread_count_att.fg,
|
||||
unread_count_att.bg,
|
||||
unread_count_att.attrs
|
||||
| if l.count.unwrap_or(0) > 0 {
|
||||
Attr::BOLD
|
||||
} else {
|
||||
Attr::DEFAULT
|
||||
},
|
||||
area.nth_row(y + 1)
|
||||
.skip_cols(x.min(area.width().saturating_sub(count_string.len()))),
|
||||
None,
|
||||
)
|
||||
.0
|
||||
+ x.min(area.width().saturating_sub(count_string.len()));
|
||||
area = self.menu.area().skip_rows(account_y);
|
||||
for c in self.menu.grid_mut().row_iter(area, x..area.width(), y + 1) {
|
||||
self.menu.grid_mut()[c]
|
||||
.set_fg(att.fg)
|
||||
.set_bg(att.bg)
|
||||
|
@ -3538,20 +3321,6 @@ impl Listing {
|
|||
index_style: previous_index_styles.get(&f.hash).copied(),
|
||||
})
|
||||
.collect::<_>();
|
||||
if let (
|
||||
ListingComponent::Offline(_),
|
||||
MenuEntryCursor::Mailbox(ref mut idx),
|
||||
Some(default),
|
||||
) = (
|
||||
&self.component,
|
||||
&mut self.cursor_pos.menu,
|
||||
context.accounts[self.cursor_pos.account]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[self.cursor_pos.account].entry_by_hash(h)),
|
||||
) {
|
||||
*idx = default;
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(default);
|
||||
}
|
||||
match self.cursor_pos.menu {
|
||||
MenuEntryCursor::Mailbox(idx) => {
|
||||
// Account might have no mailboxes yet if it's offline
|
||||
|
@ -3641,7 +3410,7 @@ impl Listing {
|
|||
let coordinates = self.component.coordinates();
|
||||
std::mem::replace(
|
||||
&mut self.component,
|
||||
Plain(PlainListing::new(self.id, coordinates, context)),
|
||||
Plain(PlainListing::new(self.id, coordinates)),
|
||||
)
|
||||
}
|
||||
IndexStyle::Threaded => {
|
||||
|
@ -3661,7 +3430,7 @@ impl Listing {
|
|||
let coordinates = self.component.coordinates();
|
||||
std::mem::replace(
|
||||
&mut self.component,
|
||||
Compact(CompactListing::new(self.id, coordinates, context)),
|
||||
Compact(CompactListing::new(self.id, coordinates)),
|
||||
)
|
||||
}
|
||||
IndexStyle::Conversations => {
|
||||
|
|
|
@ -138,14 +138,14 @@ pub struct CompactListing {
|
|||
sortcmd: bool,
|
||||
subsort: (SortField, SortOrder),
|
||||
/// Cache current view.
|
||||
data_columns: DataColumns<5>,
|
||||
data_columns: DataColumns<4>,
|
||||
rows_drawn: SegmentTree,
|
||||
rows: RowsState<(ThreadHash, EnvelopeHash)>,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
search_job: Option<(String, JoinHandle<Result<Vec<EnvelopeHash>>>)>,
|
||||
search_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
select_job: Option<(String, JoinHandle<Result<Vec<EnvelopeHash>>>)>,
|
||||
select_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
|
||||
filter_term: String,
|
||||
filtered_selection: Vec<ThreadHash>,
|
||||
filtered_order: HashMap<ThreadHash, usize>,
|
||||
|
@ -169,17 +169,14 @@ impl MailListingTrait for CompactListing {
|
|||
&mut self.rows.row_updates
|
||||
}
|
||||
|
||||
fn selection(&self) -> &HashMap<EnvelopeHash, bool> {
|
||||
&self.rows.selection
|
||||
}
|
||||
|
||||
fn selection_mut(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
&mut self.rows.selection
|
||||
}
|
||||
|
||||
fn get_focused_items(&self, _context: &Context) -> SmallVec<[EnvelopeHash; 8]> {
|
||||
let is_selection_empty = !self
|
||||
.selection()
|
||||
.rows
|
||||
.selection
|
||||
.values()
|
||||
.cloned()
|
||||
.any(std::convert::identity);
|
||||
|
@ -187,7 +184,8 @@ impl MailListingTrait for CompactListing {
|
|||
let sel_iter = if !is_selection_empty {
|
||||
cursor_iter = None;
|
||||
Some(
|
||||
self.selection()
|
||||
self.rows
|
||||
.selection
|
||||
.iter()
|
||||
.filter(|(_, v)| **v)
|
||||
.map(|(k, _)| *k),
|
||||
|
@ -229,21 +227,20 @@ impl MailListingTrait for CompactListing {
|
|||
|
||||
// Get mailbox as a reference.
|
||||
//
|
||||
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1, true) {
|
||||
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1) {
|
||||
Ok(()) => {}
|
||||
Err(_) => {
|
||||
self.length = 0;
|
||||
let message: String =
|
||||
context.accounts[&self.cursor_pos.0][&self.cursor_pos.1].status();
|
||||
if self.data_columns.columns[0].resize_with_context(message.len(), 1, context) {
|
||||
let area_col_0 = self.data_columns.columns[0].area();
|
||||
let area = self.data_columns.columns[0].area();
|
||||
self.data_columns.columns[0].grid_mut().write_string(
|
||||
message.as_str(),
|
||||
self.color_cache.theme_default.fg,
|
||||
self.color_cache.theme_default.bg,
|
||||
self.color_cache.theme_default.attrs,
|
||||
area_col_0,
|
||||
None,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
@ -302,20 +299,18 @@ impl MailListingTrait for CompactListing {
|
|||
self.sort = context.accounts[&self.cursor_pos.0].settings.account.order
|
||||
}
|
||||
self.length = 0;
|
||||
let mut min_width = (0, 0, 0, 0, 0);
|
||||
let mut min_width = (0, 0, 0, 0);
|
||||
#[allow(clippy::type_complexity)]
|
||||
let mut row_widths: (
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
) = (
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
);
|
||||
|
||||
let tags_lck = account.collection.tag_index.read().unwrap();
|
||||
|
@ -329,23 +324,7 @@ impl MailListingTrait for CompactListing {
|
|||
let my_address: Address = context.accounts[&self.cursor_pos.0]
|
||||
.settings
|
||||
.account
|
||||
.main_identity_address();
|
||||
let should_highlight_self = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self
|
||||
)
|
||||
.is_true();
|
||||
let highlight_self_colwidth: usize = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_HIGHLIGHT_SELF_FLAG)
|
||||
.grapheme_width();
|
||||
let mut itoa_buffer = itoa::Buffer::new();
|
||||
.make_display_name();
|
||||
'items_for_loop: for thread in items {
|
||||
let thread_node = &threads.thread_nodes()[&threads.thread_ref(thread).root()];
|
||||
let root_env_hash = if let Some(h) = thread_node.message().or_else(|| {
|
||||
|
@ -366,13 +345,13 @@ impl MailListingTrait for CompactListing {
|
|||
continue 'items_for_loop;
|
||||
};
|
||||
if !context.accounts[&self.cursor_pos.0].contains_key(root_env_hash) {
|
||||
//log::debug!("key = {}", root_env_hash);
|
||||
//log::debug!(
|
||||
// "name = {} {}",
|
||||
// account[&self.cursor_pos.1].name(),
|
||||
// context.accounts[&self.cursor_pos.0].name()
|
||||
//);
|
||||
//log::debug!("{:#?}", context.accounts);
|
||||
log::debug!("key = {}", root_env_hash);
|
||||
log::debug!(
|
||||
"name = {} {}",
|
||||
account[&self.cursor_pos.1].name(),
|
||||
context.accounts[&self.cursor_pos.0].name()
|
||||
);
|
||||
log::debug!("{:#?}", context.accounts);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -422,8 +401,11 @@ impl MailListingTrait for CompactListing {
|
|||
}
|
||||
}
|
||||
|
||||
highlight_self |= should_highlight_self
|
||||
&& (envelope.recipient_any(&my_address) || envelope.sender_any(&my_address));
|
||||
highlight_self |= envelope
|
||||
.to()
|
||||
.iter()
|
||||
.chain(envelope.cc().iter())
|
||||
.any(|a| a == &my_address);
|
||||
for addr in envelope.from().iter() {
|
||||
if from_address_set.contains(addr.address_spec_raw()) {
|
||||
continue;
|
||||
|
@ -432,6 +414,15 @@ impl MailListingTrait for CompactListing {
|
|||
from_address_list.push(addr.clone());
|
||||
}
|
||||
}
|
||||
if !mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self
|
||||
)
|
||||
.is_true()
|
||||
{
|
||||
highlight_self = false;
|
||||
}
|
||||
|
||||
let row_attr = row_attr!(
|
||||
self.color_cache,
|
||||
|
@ -453,13 +444,9 @@ impl MailListingTrait for CompactListing {
|
|||
highlight_self,
|
||||
thread,
|
||||
);
|
||||
row_widths.0.push(
|
||||
itoa_buffer
|
||||
.format(self.length)
|
||||
.len()
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
row_widths
|
||||
.0
|
||||
.push(digits_of_num!(self.length).try_into().unwrap_or(255));
|
||||
/* date */
|
||||
row_widths.1.push(
|
||||
entry_strings
|
||||
|
@ -476,25 +463,22 @@ impl MailListingTrait for CompactListing {
|
|||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
/* subject */
|
||||
row_widths.3.push(
|
||||
(entry_strings.flag.grapheme_width()
|
||||
+ usize::from(entry_strings.highlight_self) * highlight_self_colwidth)
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
row_widths.4.push(
|
||||
(entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width())
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
+ entry_strings.subject.grapheme_width()
|
||||
+ 1
|
||||
+ entry_strings.tags.grapheme_width())
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
min_width.1 = min_width.1.max(entry_strings.date.grapheme_width()); /* date */
|
||||
min_width.2 = min_width.2.max(entry_strings.from.grapheme_width()); /* from */
|
||||
min_width.3 = min_width.3.max(
|
||||
entry_strings.flag.grapheme_width()
|
||||
+ usize::from(entry_strings.highlight_self) * highlight_self_colwidth,
|
||||
);
|
||||
min_width.4 = min_width.4.max(
|
||||
entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width(),
|
||||
+ entry_strings.subject.grapheme_width()
|
||||
+ 1
|
||||
+ entry_strings.tags.grapheme_width(),
|
||||
); /* subject */
|
||||
self.rows.insert_thread(
|
||||
thread,
|
||||
|
@ -510,13 +494,12 @@ impl MailListingTrait for CompactListing {
|
|||
self.length += 1;
|
||||
}
|
||||
|
||||
min_width.0 = itoa_buffer.format(self.length.saturating_sub(1)).len();
|
||||
min_width.0 = self.length.saturating_sub(1).to_string().len();
|
||||
|
||||
self.data_columns.elasticities[0].set_rigid();
|
||||
self.data_columns.elasticities[1].set_rigid();
|
||||
self.data_columns.elasticities[2].set_grow(15, Some(35));
|
||||
self.data_columns.elasticities[3].set_rigid();
|
||||
self.data_columns.elasticities[4].set_rigid();
|
||||
self.data_columns
|
||||
.cursor_config
|
||||
.set_handle(true)
|
||||
|
@ -534,15 +517,12 @@ impl MailListingTrait for CompactListing {
|
|||
_ = self.data_columns.columns[1].resize_with_context(min_width.1, self.rows.len(), context);
|
||||
/* from column */
|
||||
_ = self.data_columns.columns[2].resize_with_context(min_width.2, self.rows.len(), context);
|
||||
// flags column
|
||||
/* subject column */
|
||||
_ = self.data_columns.columns[3].resize_with_context(min_width.3, self.rows.len(), context);
|
||||
// subject column
|
||||
_ = self.data_columns.columns[4].resize_with_context(min_width.4, self.rows.len(), context);
|
||||
self.data_columns.segment_tree[0] = row_widths.0.into();
|
||||
self.data_columns.segment_tree[1] = row_widths.1.into();
|
||||
self.data_columns.segment_tree[2] = row_widths.2.into();
|
||||
self.data_columns.segment_tree[3] = row_widths.3.into();
|
||||
self.data_columns.segment_tree[4] = row_widths.4.into();
|
||||
|
||||
self.rows_drawn = SegmentTree::from(
|
||||
std::iter::repeat(1)
|
||||
|
@ -554,14 +534,13 @@ impl MailListingTrait for CompactListing {
|
|||
if self.length == 0 && self.filter_term.is_empty() {
|
||||
let message: String = account[&self.cursor_pos.1].status();
|
||||
if self.data_columns.columns[0].resize_with_context(message.len(), 1, context) {
|
||||
let area_col_0 = self.data_columns.columns[0].area();
|
||||
let area = self.data_columns.columns[0].area();
|
||||
self.data_columns.columns[0].grid_mut().write_string(
|
||||
&message,
|
||||
self.color_cache.theme_default.fg,
|
||||
self.color_cache.theme_default.bg,
|
||||
self.color_cache.theme_default.attrs,
|
||||
area_col_0,
|
||||
None,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
@ -709,10 +688,6 @@ impl ListingTrait for CompactListing {
|
|||
}
|
||||
context.dirty_areas.push_back(new_area);
|
||||
}
|
||||
if *account_settings!(context[self.cursor_pos.0].listing.relative_list_indices) {
|
||||
self.draw_relative_numbers(grid, area, top_idx, context);
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
if !self.force_draw {
|
||||
return;
|
||||
}
|
||||
|
@ -732,9 +707,6 @@ impl ListingTrait for CompactListing {
|
|||
/* copy table columns */
|
||||
self.data_columns
|
||||
.draw(grid, top_idx, self.cursor_pos.2, grid.bounds_iter(area));
|
||||
if *account_settings!(context[self.cursor_pos.0].listing.relative_list_indices) {
|
||||
self.draw_relative_numbers(grid, area, top_idx, context);
|
||||
}
|
||||
/* apply each row colors separately */
|
||||
for i in top_idx..(top_idx + area.height()) {
|
||||
if let Some(row_attr) = self.rows.row_attr_cache.get(&i) {
|
||||
|
@ -764,19 +736,16 @@ impl ListingTrait for CompactListing {
|
|||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
fn filter(&mut self, filter_term: String, results: Vec<EnvelopeHash>, context: &Context) {
|
||||
if filter_term.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
fn filter(
|
||||
&mut self,
|
||||
filter_term: String,
|
||||
results: SmallVec<[EnvelopeHash; 512]>,
|
||||
context: &Context,
|
||||
) {
|
||||
self.length = 0;
|
||||
self.filtered_selection.clear();
|
||||
self.filtered_order.clear();
|
||||
self.filter_term = filter_term;
|
||||
self.rows.row_updates.clear();
|
||||
for v in self.selection_mut().values_mut() {
|
||||
*v = false;
|
||||
}
|
||||
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
let threads = account.collection.get_threads(self.cursor_pos.1);
|
||||
|
@ -823,10 +792,6 @@ impl ListingTrait for CompactListing {
|
|||
!matches!(self.focus, Focus::None)
|
||||
}
|
||||
|
||||
fn modifier_active(&self) -> bool {
|
||||
self.modifier_active
|
||||
}
|
||||
|
||||
fn set_modifier_active(&mut self, new_val: bool) {
|
||||
self.modifier_active = new_val;
|
||||
}
|
||||
|
@ -900,12 +865,7 @@ impl std::fmt::Display for CompactListing {
|
|||
}
|
||||
|
||||
impl CompactListing {
|
||||
pub fn new(
|
||||
parent: ComponentId,
|
||||
coordinates: (AccountHash, MailboxHash),
|
||||
context: &Context,
|
||||
) -> Box<Self> {
|
||||
let color_cache = ColorCache::new(context, IndexStyle::Compact);
|
||||
pub fn new(parent: ComponentId, coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
|
||||
Box::new(Self {
|
||||
cursor_pos: (AccountHash::default(), MailboxHash::default(), 0),
|
||||
new_cursor_pos: (coordinates.0, coordinates.1, 0),
|
||||
|
@ -919,12 +879,12 @@ impl CompactListing {
|
|||
filtered_selection: Vec::new(),
|
||||
filtered_order: HashMap::default(),
|
||||
focus: Focus::None,
|
||||
data_columns: DataColumns::new(color_cache.theme_default),
|
||||
data_columns: DataColumns::default(),
|
||||
rows_drawn: SegmentTree::default(),
|
||||
rows: RowsState::default(),
|
||||
dirty: true,
|
||||
force_draw: true,
|
||||
color_cache,
|
||||
color_cache: ColorCache::default(),
|
||||
movement: None,
|
||||
modifier_active: false,
|
||||
modifier_command: None,
|
||||
|
@ -1014,7 +974,8 @@ impl CompactListing {
|
|||
},
|
||||
flag: FlagString::new(
|
||||
flags,
|
||||
self.selection()
|
||||
self.rows
|
||||
.selection
|
||||
.get(&root_envelope.hash())
|
||||
.cloned()
|
||||
.unwrap_or(false),
|
||||
|
@ -1024,9 +985,8 @@ impl CompactListing {
|
|||
context,
|
||||
(self.cursor_pos.0, self.cursor_pos.1),
|
||||
),
|
||||
from: FromString(Address::display_name_slice(from, None)),
|
||||
from: FromString(Address::display_name_slice(from)),
|
||||
tags: TagString(tags_string, colors),
|
||||
unseen: thread.unseen() > 0,
|
||||
highlight_self,
|
||||
}
|
||||
}
|
||||
|
@ -1073,16 +1033,10 @@ impl CompactListing {
|
|||
let mut from_address_set: std::collections::HashSet<Vec<u8>> =
|
||||
std::collections::HashSet::new();
|
||||
let mut highlight_self: bool = false;
|
||||
let should_highlight_self = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self
|
||||
)
|
||||
.is_true();
|
||||
let my_address: Address = context.accounts[&self.cursor_pos.0]
|
||||
.settings
|
||||
.account
|
||||
.main_identity_address();
|
||||
.make_display_name();
|
||||
for (envelope, show_subject) in threads
|
||||
.thread_iter(thread_hash)
|
||||
.filter_map(|(_, h)| {
|
||||
|
@ -1107,8 +1061,11 @@ impl CompactListing {
|
|||
tags.insert(t);
|
||||
}
|
||||
}
|
||||
highlight_self |= should_highlight_self
|
||||
&& (envelope.recipient_any(&my_address) || envelope.sender_any(&my_address));
|
||||
highlight_self |= envelope
|
||||
.to()
|
||||
.iter()
|
||||
.chain(envelope.cc().iter())
|
||||
.any(|a| a == &my_address);
|
||||
for addr in envelope.from().iter() {
|
||||
if from_address_set.contains(addr.address_spec_raw()) {
|
||||
continue;
|
||||
|
@ -1117,8 +1074,17 @@ impl CompactListing {
|
|||
from_address_list.push(addr.clone());
|
||||
}
|
||||
}
|
||||
if !mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self
|
||||
)
|
||||
.is_true()
|
||||
{
|
||||
highlight_self = false;
|
||||
}
|
||||
|
||||
let mut entry_strings = self.make_entry_string(
|
||||
let strings = self.make_entry_string(
|
||||
&envelope,
|
||||
context,
|
||||
&tags_lck,
|
||||
|
@ -1129,22 +1095,161 @@ impl CompactListing {
|
|||
highlight_self,
|
||||
thread_hash,
|
||||
);
|
||||
entry_strings.highlight_self = should_highlight_self && {
|
||||
let my_address: Address = context.accounts[&self.cursor_pos.0]
|
||||
.settings
|
||||
.account
|
||||
.main_identity_address();
|
||||
envelope.recipient_any(&my_address) || envelope.sender_any(&my_address)
|
||||
};
|
||||
drop(envelope);
|
||||
let columns = &mut self.data_columns.columns;
|
||||
for n in 0..=4 {
|
||||
let area = columns[n].area().nth_row(idx);
|
||||
columns[n].grid_mut().clear_area(area, row_attr);
|
||||
let min_width = (
|
||||
columns[0].area().width(),
|
||||
columns[1].area().width(),
|
||||
columns[2].area().width(),
|
||||
columns[3].area().width(),
|
||||
);
|
||||
let (x, _) = {
|
||||
let area = columns[0].area().nth_row(idx);
|
||||
columns[0].grid_mut().write_string(
|
||||
&idx.to_string(),
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[0].area();
|
||||
columns[0].grid_mut().row_iter(area, x..min_width.0, idx)
|
||||
} {
|
||||
columns[0].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs)
|
||||
.set_ch(' ');
|
||||
}
|
||||
let (x, _) = {
|
||||
let area = columns[1].area().nth_row(idx);
|
||||
columns[1].grid_mut().write_string(
|
||||
&strings.date,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[1].area();
|
||||
columns[1].grid_mut().row_iter(area, x..min_width.1, idx)
|
||||
} {
|
||||
columns[1].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs)
|
||||
.set_ch(' ');
|
||||
}
|
||||
let (x, _) = {
|
||||
let area = columns[2].area().nth_row(idx);
|
||||
columns[2].grid_mut().write_string(
|
||||
&strings.from,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[2].area();
|
||||
columns[2].grid_mut().row_iter(area, x..min_width.2, idx)
|
||||
} {
|
||||
columns[2].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs)
|
||||
.set_ch(' ');
|
||||
}
|
||||
let (x, _) = {
|
||||
let area = columns[3].area().nth_row(idx);
|
||||
columns[3].grid_mut().write_string(
|
||||
&strings.flag,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
};
|
||||
let (x, _) = {
|
||||
let area = columns[3].area().nth_row(idx).skip_cols(x);
|
||||
columns[3].grid_mut().write_string(
|
||||
&strings.subject,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
};
|
||||
if let Some(c) = columns[3].grid_mut().get_mut(x, idx) {
|
||||
c.set_bg(row_attr.bg).set_attrs(row_attr.attrs).set_ch(' ');
|
||||
}
|
||||
let x = {
|
||||
let mut x = x + 1;
|
||||
for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) {
|
||||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let _x = {
|
||||
let area = columns[3].area().nth_row(idx).skip_cols(x + 1);
|
||||
columns[3]
|
||||
.grid_mut()
|
||||
.write_string(
|
||||
t,
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
.0
|
||||
+ x
|
||||
+ 1
|
||||
};
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, x..(x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_bg(color);
|
||||
}
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, _x..(_x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_bg(color).set_keep_bg(true);
|
||||
}
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, (x + 1)..(_x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c]
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
}
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, x..(x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_keep_bg(true);
|
||||
}
|
||||
x = _x + 2;
|
||||
}
|
||||
x
|
||||
};
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, x..min_width.3, idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c]
|
||||
.set_ch(' ')
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
*self.rows.entries.get_mut(idx).unwrap() = ((thread_hash, env_hash), strings);
|
||||
self.rows_drawn.update(idx, 1);
|
||||
|
||||
*self.rows.entries.get_mut(idx).unwrap() = ((thread_hash, env_hash), entry_strings);
|
||||
}
|
||||
|
||||
fn draw_rows(&mut self, context: &Context, start: usize, end: usize) {
|
||||
|
@ -1163,11 +1268,9 @@ impl CompactListing {
|
|||
self.data_columns.columns[1].area().width(),
|
||||
self.data_columns.columns[2].area().width(),
|
||||
self.data_columns.columns[3].area().width(),
|
||||
self.data_columns.columns[3].area().width(),
|
||||
);
|
||||
|
||||
let columns = &mut self.data_columns.columns;
|
||||
let mut itoa_buffer = itoa::Buffer::new();
|
||||
for (idx, ((_thread_hash, root_env_hash), strings)) in self
|
||||
.rows
|
||||
.entries
|
||||
|
@ -1191,13 +1294,12 @@ impl CompactListing {
|
|||
let (x, _) = {
|
||||
let area = columns[0].area().nth_row(idx);
|
||||
columns[0].grid_mut().write_string(
|
||||
itoa_buffer.format(idx),
|
||||
&idx.to_string(),
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
|
@ -1217,7 +1319,6 @@ impl CompactListing {
|
|||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
|
@ -1237,7 +1338,6 @@ impl CompactListing {
|
|||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
|
@ -1249,6 +1349,7 @@ impl CompactListing {
|
|||
.set_attrs(row_attr.attrs)
|
||||
.set_ch(' ');
|
||||
}
|
||||
#[cfg(feature = "regexp")]
|
||||
{
|
||||
for text_formatter in crate::conf::text_format_regexps(context, "listing.from") {
|
||||
let t = columns[2].grid_mut().insert_tag(text_formatter.tag);
|
||||
|
@ -1257,109 +1358,128 @@ impl CompactListing {
|
|||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut area_col_3 = columns[3].area().nth_row(idx);
|
||||
area_col_3 = area_col_3.skip_cols(columns[3].grid_mut().write_string(
|
||||
let (x, _) = {
|
||||
let area = columns[3].area().nth_row(idx);
|
||||
columns[3].grid_mut().write_string(
|
||||
&strings.flag,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_3,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
)
|
||||
};
|
||||
let x = {
|
||||
let mut area = columns[3].area().nth_row(idx).skip_cols(x);
|
||||
if strings.highlight_self {
|
||||
let (x, _) = columns[3].grid_mut().write_string(
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self_flag
|
||||
// [ref:hardcoded_color_value]: add highlight_self theme attr
|
||||
let x = columns[3]
|
||||
.grid_mut()
|
||||
.write_string(
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_HIGHLIGHT_SELF_FLAG),
|
||||
Color::BLUE,
|
||||
row_attr.bg,
|
||||
row_attr.attrs | Attr::FORCE_TEXT,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_HIGHLIGHT_SELF_FLAG),
|
||||
self.color_cache.highlight_self.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs | Attr::FORCE_TEXT,
|
||||
area_col_3,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for c in columns[3].grid().row_iter(area_col_3, 0..x, 0) {
|
||||
columns[3].grid_mut()[c].set_keep_fg(true);
|
||||
}
|
||||
area_col_3 = area_col_3.skip_cols(x + 1);
|
||||
}
|
||||
for c in columns[3].grid().row_iter(area_col_3, 0..min_width.3, 0) {
|
||||
columns[3].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut area_col_4 = columns[4].area().nth_row(idx);
|
||||
area_col_4 = area_col_4.skip_cols(columns[4].grid_mut().write_string(
|
||||
&strings.subject,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_4,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
{
|
||||
for text_formatter in
|
||||
crate::conf::text_format_regexps(context, "listing.subject")
|
||||
{
|
||||
let t = columns[4].grid_mut().insert_tag(text_formatter.tag);
|
||||
for (start, end) in
|
||||
text_formatter.regexp.find_iter(strings.subject.as_str())
|
||||
{
|
||||
columns[4].grid_mut().set_tag(t, (start, idx), (end, idx));
|
||||
.0;
|
||||
for row in columns[3].grid().bounds_iter(area.nth_row(0).take_cols(x)) {
|
||||
for c in row {
|
||||
columns[3].grid_mut()[c].set_keep_fg(true);
|
||||
}
|
||||
}
|
||||
area = area.skip_cols(x + 1);
|
||||
}
|
||||
area_col_4 = area_col_4.skip_cols(1);
|
||||
for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) {
|
||||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let (x, _) = columns[4].grid_mut().write_string(
|
||||
t,
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
area_col_4,
|
||||
columns[3]
|
||||
.grid_mut()
|
||||
.write_string(
|
||||
&strings.subject,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for c in columns[4].grid().row_iter(area_col_4, 0..(x + 1), 0) {
|
||||
columns[4].grid_mut()[c]
|
||||
.set_bg(color)
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
)
|
||||
.0
|
||||
+ x
|
||||
};
|
||||
#[cfg(feature = "regexp")]
|
||||
{
|
||||
for text_formatter in crate::conf::text_format_regexps(context, "listing.subject") {
|
||||
let t = columns[3].grid_mut().insert_tag(text_formatter.tag);
|
||||
for (start, end) in text_formatter.regexp.find_iter(strings.subject.as_str()) {
|
||||
columns[3].grid_mut().set_tag(t, (start, idx), (end, idx));
|
||||
}
|
||||
area_col_4 = area_col_4.skip_cols(x + 1);
|
||||
}
|
||||
for c in columns[4].grid().row_iter(area_col_4, 0..min_width.4, 0) {
|
||||
columns[4].grid_mut()[c]
|
||||
.set_ch(' ')
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
let mut x = x + 1;
|
||||
for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) {
|
||||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let _x = {
|
||||
let area = columns[3].area().nth_row(idx).skip_cols(x + 1);
|
||||
columns[3]
|
||||
.grid_mut()
|
||||
.write_string(
|
||||
t,
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
.0
|
||||
+ x
|
||||
+ 1
|
||||
};
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, x..(x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_bg(color);
|
||||
}
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, _x..(_x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_bg(color).set_keep_bg(true);
|
||||
}
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, (x + 1)..(_x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c]
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
}
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, x..(x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_keep_bg(true);
|
||||
}
|
||||
x = _x + 2;
|
||||
}
|
||||
}
|
||||
if self.length == 0 && self.filter_term.is_empty() {
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
let message: String = account[&self.cursor_pos.1].status();
|
||||
if self.data_columns.columns[0].resize_with_context(message.len(), 1, context) {
|
||||
let area_col_0 = self.data_columns.columns[0].area();
|
||||
let area = self.data_columns.columns[0].area();
|
||||
self.data_columns.columns[0].grid_mut().write_string(
|
||||
message.as_str(),
|
||||
self.color_cache.theme_default.fg,
|
||||
self.color_cache.theme_default.bg,
|
||||
self.color_cache.theme_default.attrs,
|
||||
area_col_0,
|
||||
None,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
@ -1369,7 +1489,7 @@ impl CompactListing {
|
|||
fn select(
|
||||
&mut self,
|
||||
search_term: &str,
|
||||
results: Result<Vec<EnvelopeHash>>,
|
||||
results: Result<SmallVec<[EnvelopeHash; 512]>>,
|
||||
context: &mut Context,
|
||||
) {
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
|
@ -1387,7 +1507,8 @@ impl CompactListing {
|
|||
let thread =
|
||||
threads.find_group(threads.thread_nodes[&env_thread_node_hash].group);
|
||||
if self.rows.all_threads.contains(&thread) {
|
||||
self.selection_mut()
|
||||
self.rows
|
||||
.selection
|
||||
.entry(env_hash)
|
||||
.and_modify(|entry| *entry = true);
|
||||
}
|
||||
|
@ -1411,54 +1532,6 @@ impl CompactListing {
|
|||
}
|
||||
}
|
||||
|
||||
fn draw_relative_numbers(
|
||||
&self,
|
||||
grid: &mut CellBuffer,
|
||||
area: Area,
|
||||
top_idx: usize,
|
||||
context: &Context,
|
||||
) {
|
||||
let width = self.data_columns.columns[0].area().width();
|
||||
let area = area.take_cols(width);
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
let threads = account.collection.get_threads(self.cursor_pos.1);
|
||||
for i in 0..area.height() {
|
||||
let idx = top_idx + i;
|
||||
if idx >= self.length {
|
||||
break;
|
||||
}
|
||||
let row_attr = if let Some(thread_hash) = self.get_thread_under_cursor(idx) {
|
||||
let thread = threads.thread_ref(thread_hash);
|
||||
row_attr!(
|
||||
self.color_cache,
|
||||
even: idx % 2 == 0,
|
||||
unseen: thread.unseen() > 0,
|
||||
highlighted: self.new_cursor_pos.2 == idx,
|
||||
selected: self.rows.is_thread_selected(thread_hash)
|
||||
)
|
||||
} else {
|
||||
row_attr!(self.color_cache, even: (top_idx + i) % 2 == 0, unseen: false, highlighted: true, selected: false)
|
||||
};
|
||||
|
||||
grid.clear_area(area.nth_row(i), row_attr);
|
||||
grid.write_string(
|
||||
&if self.new_cursor_pos.2.saturating_sub(top_idx) == i {
|
||||
self.new_cursor_pos.2.to_string()
|
||||
} else {
|
||||
(i as isize - (self.new_cursor_pos.2 - top_idx) as isize)
|
||||
.abs()
|
||||
.to_string()
|
||||
},
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area.nth_row(i),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_movement(&mut self, height: Option<usize>) {
|
||||
let rows = height.unwrap_or(1);
|
||||
if let Some(mvm) = self.movement.take() {
|
||||
|
@ -1534,7 +1607,6 @@ impl Component for CompactListing {
|
|||
self.color_cache.theme_default.bg,
|
||||
self.color_cache.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
Some(0),
|
||||
);
|
||||
|
||||
|
@ -1819,22 +1891,17 @@ impl Component for CompactListing {
|
|||
if !self.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["select_entry"]) =>
|
||||
{
|
||||
if let Some(thread_hash) = self.get_thread_under_cursor(self.new_cursor_pos.2) {
|
||||
if self.modifier_active && self.modifier_command.is_none() {
|
||||
self.modifier_command = Some(Modifier::default());
|
||||
} else if let Some(thread_hash) =
|
||||
self.get_thread_under_cursor(self.cursor_pos.2)
|
||||
{
|
||||
self.rows
|
||||
.update_selection_with_thread(thread_hash, |e| *e = !*e);
|
||||
self.set_dirty(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if !self.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["select_motion"]) =>
|
||||
{
|
||||
if self.modifier_active && self.modifier_command.is_none() {
|
||||
self.modifier_command = Some(Modifier::default());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Action(ref action) => {
|
||||
match action {
|
||||
Action::Sort(field, order) if !self.unfocused() => {
|
||||
|
@ -1962,11 +2029,7 @@ impl Component for CompactListing {
|
|||
let handle = context.accounts[&self.cursor_pos.0]
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn(
|
||||
"search".into(),
|
||||
job,
|
||||
context.accounts[&self.cursor_pos.0].is_async(),
|
||||
);
|
||||
.spawn_specialized("search".into(), job);
|
||||
self.search_job = Some((filter_term.to_string(), handle));
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -1991,11 +2054,7 @@ impl Component for CompactListing {
|
|||
let mut handle = context.accounts[&self.cursor_pos.0]
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn(
|
||||
"select-by-search".into(),
|
||||
job,
|
||||
context.accounts[&self.cursor_pos.0].is_async(),
|
||||
);
|
||||
.spawn_specialized("select_by_search".into(), job);
|
||||
if let Ok(Some(search_result)) = try_recv_timeout!(&mut handle.chan) {
|
||||
self.select(search_term, search_result, context);
|
||||
} else {
|
||||
|
|
|
@ -118,7 +118,7 @@ pub struct ConversationsListing {
|
|||
error: std::result::Result<(), String>,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
search_job: Option<(String, JoinHandle<Result<Vec<EnvelopeHash>>>)>,
|
||||
search_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
|
||||
filter_term: String,
|
||||
filtered_selection: Vec<ThreadHash>,
|
||||
filtered_order: HashMap<ThreadHash, usize>,
|
||||
|
@ -142,11 +142,7 @@ impl MailListingTrait for ConversationsListing {
|
|||
&mut self.rows.row_updates
|
||||
}
|
||||
|
||||
fn selection(&self) -> &HashMap<EnvelopeHash, bool> {
|
||||
&self.rows.selection
|
||||
}
|
||||
|
||||
fn selection_mut(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
&mut self.rows.selection
|
||||
}
|
||||
|
||||
|
@ -202,7 +198,7 @@ impl MailListingTrait for ConversationsListing {
|
|||
|
||||
// Get mailbox as a reference.
|
||||
//
|
||||
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1, true) {
|
||||
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1) {
|
||||
Ok(()) => {}
|
||||
Err(_) => {
|
||||
let message: String =
|
||||
|
@ -290,15 +286,15 @@ impl MailListingTrait for ConversationsListing {
|
|||
continue 'items_for_loop;
|
||||
};
|
||||
if !context.accounts[&self.cursor_pos.0].contains_key(root_env_hash) {
|
||||
//log::debug!("key = {}", root_env_hash);
|
||||
//log::debug!(
|
||||
// "name = {} {}",
|
||||
// account[&self.cursor_pos.1].name(),
|
||||
// context.accounts[&self.cursor_pos.0].name()
|
||||
//);
|
||||
//log::debug!("{:#?}", context.accounts);
|
||||
log::debug!("key = {}", root_env_hash);
|
||||
log::debug!(
|
||||
"name = {} {}",
|
||||
account[&self.cursor_pos.1].name(),
|
||||
context.accounts[&self.cursor_pos.0].name()
|
||||
);
|
||||
log::debug!("{:#?}", context.accounts);
|
||||
|
||||
continue 'items_for_loop;
|
||||
panic!();
|
||||
}
|
||||
let root_envelope: &EnvelopeRef = &context.accounts[&self.cursor_pos.0]
|
||||
.collection
|
||||
|
@ -464,7 +460,6 @@ impl ListingTrait for ConversationsListing {
|
|||
self.color_cache.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
context.dirty_areas.push_back(area);
|
||||
return;
|
||||
|
@ -521,7 +516,12 @@ impl ListingTrait for ConversationsListing {
|
|||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
fn filter(&mut self, filter_term: String, results: Vec<EnvelopeHash>, context: &Context) {
|
||||
fn filter(
|
||||
&mut self,
|
||||
filter_term: String,
|
||||
results: SmallVec<[EnvelopeHash; 512]>,
|
||||
context: &Context,
|
||||
) {
|
||||
if filter_term.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
@ -577,10 +577,6 @@ impl ListingTrait for ConversationsListing {
|
|||
!matches!(self.focus, Focus::None)
|
||||
}
|
||||
|
||||
fn modifier_active(&self) -> bool {
|
||||
self.modifier_active
|
||||
}
|
||||
|
||||
fn set_modifier_active(&mut self, new_val: bool) {
|
||||
self.modifier_active = new_val;
|
||||
}
|
||||
|
@ -773,9 +769,8 @@ impl ConversationsListing {
|
|||
context,
|
||||
(self.cursor_pos.0, self.cursor_pos.1),
|
||||
),
|
||||
from: FromString(Address::display_name_slice(from, None)),
|
||||
from: FromString(Address::display_name_slice(from)),
|
||||
tags: TagString(tags_string, colors),
|
||||
unseen: thread.unseen() > 0,
|
||||
highlight_self: false,
|
||||
}
|
||||
}
|
||||
|
@ -880,13 +875,12 @@ impl ConversationsListing {
|
|||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if !strings.flag.is_empty() {
|
||||
for c in grid.row_iter(area, x..(x + 1), 0) {
|
||||
for c in grid.row_iter(area, x..(x + 3), 0) {
|
||||
grid[c].set_bg(row_attr.bg);
|
||||
}
|
||||
x += 1;
|
||||
x += 3;
|
||||
}
|
||||
let subject_attr = row_attr!(
|
||||
subject,
|
||||
|
@ -903,7 +897,6 @@ impl ConversationsListing {
|
|||
subject_attr.attrs,
|
||||
area.skip_cols(x),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
x += x_;
|
||||
let mut subject_overflowed = subject_overflowed > 0;
|
||||
|
@ -920,7 +913,6 @@ impl ConversationsListing {
|
|||
self.color_cache.tag_default.attrs,
|
||||
area.skip_cols(1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if _y > 0 {
|
||||
subject_overflowed = true;
|
||||
|
@ -957,7 +949,6 @@ impl ConversationsListing {
|
|||
date_attr.attrs,
|
||||
area.skip(x, 1),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.0;
|
||||
for c in grid.row_iter(area, x..(x + 4), 1) {
|
||||
|
@ -980,7 +971,6 @@ impl ConversationsListing {
|
|||
from_attr.attrs,
|
||||
area.skip(x, 1),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.0;
|
||||
|
||||
|
@ -1053,7 +1043,6 @@ impl Component for ConversationsListing {
|
|||
self.color_cache.theme_default.bg,
|
||||
self.color_cache.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
Some(0),
|
||||
);
|
||||
for c in grid.row_iter(area, x..area.width(), y) {
|
||||
|
@ -1365,19 +1354,12 @@ impl Component for ConversationsListing {
|
|||
UIEvent::Input(ref key)
|
||||
if !self.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["select_entry"]) =>
|
||||
{
|
||||
if let Some(thread) = self.get_thread_under_cursor(self.new_cursor_pos.2) {
|
||||
self.rows.update_selection_with_thread(thread, |e| *e = !*e);
|
||||
self.set_dirty(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if !self.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["select_motion"]) =>
|
||||
{
|
||||
if self.modifier_active && self.modifier_command.is_none() {
|
||||
self.modifier_command = Some(Modifier::default());
|
||||
} else if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) {
|
||||
self.rows.update_selection_with_thread(thread, |e| *e = !*e);
|
||||
self.set_dirty(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -1490,11 +1472,7 @@ impl Component for ConversationsListing {
|
|||
let handle = context.accounts[&self.cursor_pos.0]
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn(
|
||||
"search".into(),
|
||||
job,
|
||||
context.accounts[&self.cursor_pos.0].is_async(),
|
||||
);
|
||||
.spawn_specialized("search".into(), job);
|
||||
self.search_job = Some((filter_term.to_string(), handle));
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -1511,7 +1489,7 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
_ => {}
|
||||
},
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('\x1b'))
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char(''))
|
||||
if !self.unfocused() && !&self.filter_term.is_empty() =>
|
||||
{
|
||||
self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1));
|
||||
|
|
|
@ -39,11 +39,7 @@ impl MailListingTrait for OfflineListing {
|
|||
&mut self._row_updates
|
||||
}
|
||||
|
||||
fn selection(&self) -> &HashMap<EnvelopeHash, bool> {
|
||||
&self._selection
|
||||
}
|
||||
|
||||
fn selection_mut(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
&mut self._selection
|
||||
}
|
||||
|
||||
|
@ -98,10 +94,6 @@ impl ListingTrait for OfflineListing {
|
|||
false
|
||||
}
|
||||
|
||||
fn modifier_active(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_modifier_active(&mut self, _: bool) {}
|
||||
|
||||
fn set_modifier_command(&mut self, _: Option<Modifier>) {}
|
||||
|
@ -156,7 +148,6 @@ impl Component for OfflineListing {
|
|||
error_message.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let (_, mut y_offset) = grid.write_string(
|
||||
|
@ -164,8 +155,7 @@ impl Component for OfflineListing {
|
|||
error_message.fg,
|
||||
error_message.bg,
|
||||
error_message.attrs,
|
||||
area,
|
||||
Some(x + 1),
|
||||
area.skip_cols(x + 1),
|
||||
Some(0),
|
||||
);
|
||||
y_offset += 1;
|
||||
|
@ -177,7 +167,6 @@ impl Component for OfflineListing {
|
|||
Attr::BOLD,
|
||||
area.skip_rows(y_offset),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
y_offset += 1;
|
||||
|
@ -189,7 +178,6 @@ impl Component for OfflineListing {
|
|||
text_unfocused.attrs,
|
||||
area.skip_rows(y_offset + i),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -200,7 +188,6 @@ impl Component for OfflineListing {
|
|||
conf::value(context, "highlight").attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let mut jobs: SmallVec<[_; 64]> = context.accounts[&self.cursor_pos.0]
|
||||
.active_jobs
|
||||
|
@ -215,7 +202,6 @@ impl Component for OfflineListing {
|
|||
text_unfocused.attrs,
|
||||
area.skip_rows(i + 1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -137,12 +137,12 @@ pub struct PlainListing {
|
|||
subsort: (SortField, SortOrder),
|
||||
rows: RowsState<(ThreadHash, EnvelopeHash)>,
|
||||
/// Cache current view.
|
||||
data_columns: DataColumns<5>,
|
||||
data_columns: DataColumns<4>,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
search_job: Option<(String, JoinHandle<Result<Vec<EnvelopeHash>>>)>,
|
||||
search_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
select_job: Option<(String, JoinHandle<Result<Vec<EnvelopeHash>>>)>,
|
||||
select_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
|
||||
filter_term: String,
|
||||
filtered_selection: Vec<EnvelopeHash>,
|
||||
filtered_order: HashMap<EnvelopeHash, usize>,
|
||||
|
@ -166,11 +166,7 @@ impl MailListingTrait for PlainListing {
|
|||
&mut self.rows.row_updates
|
||||
}
|
||||
|
||||
fn selection(&self) -> &HashMap<EnvelopeHash, bool> {
|
||||
&self.rows.selection
|
||||
}
|
||||
|
||||
fn selection_mut(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
&mut self.rows.selection
|
||||
}
|
||||
|
||||
|
@ -215,7 +211,7 @@ impl MailListingTrait for PlainListing {
|
|||
|
||||
// Get mailbox as a reference.
|
||||
//
|
||||
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1, true) {
|
||||
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1) {
|
||||
Ok(()) => {}
|
||||
Err(_) => {
|
||||
self.length = 0;
|
||||
|
@ -230,7 +226,6 @@ impl MailListingTrait for PlainListing {
|
|||
self.color_cache.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
@ -369,7 +364,7 @@ impl ListingTrait for PlainListing {
|
|||
even: idx % 2 == 0,
|
||||
unseen: !envelope.is_seen(),
|
||||
highlighted: self.cursor_pos.2 == idx,
|
||||
selected: self.rows.selection.get(&i).copied().unwrap_or(false)
|
||||
selected: self.rows.selection[&i]
|
||||
);
|
||||
|
||||
let x = self.data_columns.widths[0]
|
||||
|
@ -447,10 +442,6 @@ impl ListingTrait for PlainListing {
|
|||
}
|
||||
context.dirty_areas.push_back(new_area);
|
||||
}
|
||||
if *account_settings!(context[self.cursor_pos.0].listing.relative_list_indices) {
|
||||
self.draw_relative_numbers(grid, area, top_idx);
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
if !self.force_draw {
|
||||
return;
|
||||
}
|
||||
|
@ -470,9 +461,6 @@ impl ListingTrait for PlainListing {
|
|||
/* copy table columns */
|
||||
self.data_columns
|
||||
.draw(grid, top_idx, self.cursor_pos.2, grid.bounds_iter(area));
|
||||
if *account_settings!(context[self.cursor_pos.0].listing.relative_list_indices) {
|
||||
self.draw_relative_numbers(grid, area, top_idx);
|
||||
}
|
||||
/* apply each row colors separately */
|
||||
for i in top_idx..(top_idx + area.height()) {
|
||||
if let Some(row_attr) = self.rows.row_attr_cache.get(&i) {
|
||||
|
@ -502,7 +490,12 @@ impl ListingTrait for PlainListing {
|
|||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
fn filter(&mut self, filter_term: String, results: Vec<EnvelopeHash>, context: &Context) {
|
||||
fn filter(
|
||||
&mut self,
|
||||
filter_term: String,
|
||||
results: SmallVec<[EnvelopeHash; 512]>,
|
||||
context: &Context,
|
||||
) {
|
||||
if filter_term.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
@ -550,10 +543,6 @@ impl ListingTrait for PlainListing {
|
|||
!matches!(self.focus, Focus::None)
|
||||
}
|
||||
|
||||
fn modifier_active(&self) -> bool {
|
||||
self.modifier_active
|
||||
}
|
||||
|
||||
fn set_modifier_active(&mut self, new_val: bool) {
|
||||
self.modifier_active = new_val;
|
||||
}
|
||||
|
@ -626,12 +615,7 @@ impl std::fmt::Display for PlainListing {
|
|||
}
|
||||
|
||||
impl PlainListing {
|
||||
pub fn new(
|
||||
parent: ComponentId,
|
||||
coordinates: (AccountHash, MailboxHash),
|
||||
context: &Context,
|
||||
) -> Box<Self> {
|
||||
let color_cache = ColorCache::new(context, IndexStyle::Plain);
|
||||
pub fn new(parent: ComponentId, coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
|
||||
Box::new(Self {
|
||||
cursor_pos: (AccountHash::default(), MailboxHash::default(), 0),
|
||||
new_cursor_pos: (coordinates.0, coordinates.1, 0),
|
||||
|
@ -645,11 +629,11 @@ impl PlainListing {
|
|||
select_job: None,
|
||||
filtered_selection: Vec::new(),
|
||||
filtered_order: HashMap::default(),
|
||||
data_columns: DataColumns::new(color_cache.theme_default),
|
||||
data_columns: DataColumns::default(),
|
||||
dirty: true,
|
||||
force_draw: true,
|
||||
focus: Focus::None,
|
||||
color_cache,
|
||||
color_cache: ColorCache::default(),
|
||||
movement: None,
|
||||
modifier_active: false,
|
||||
modifier_command: None,
|
||||
|
@ -710,15 +694,15 @@ impl PlainListing {
|
|||
context,
|
||||
(self.cursor_pos.0, self.cursor_pos.1),
|
||||
),
|
||||
from: FromString(Address::display_name_slice(e.from(), None)),
|
||||
from: FromString(Address::display_name_slice(e.from())),
|
||||
tags: TagString(tags, colors),
|
||||
unseen: !e.is_seen(),
|
||||
highlight_self: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn redraw_list(&mut self, context: &Context, iter: Box<dyn Iterator<Item = EnvelopeHash>>) {
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
let mailbox = &account[&self.cursor_pos.1];
|
||||
let threads = account.collection.get_threads(self.cursor_pos.1);
|
||||
|
||||
self.rows.clear();
|
||||
|
@ -730,46 +714,22 @@ impl PlainListing {
|
|||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
) = (
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
);
|
||||
let should_highlight_self = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self
|
||||
)
|
||||
.is_true();
|
||||
let my_address: Address = context.accounts[&self.cursor_pos.0]
|
||||
.settings
|
||||
.account
|
||||
.main_identity_address();
|
||||
let highlight_self_colwidth: usize = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_HIGHLIGHT_SELF_FLAG)
|
||||
.grapheme_width();
|
||||
let mut itoa_buffer = itoa::Buffer::new();
|
||||
|
||||
for i in iter {
|
||||
if !context.accounts[&self.cursor_pos.0].contains_key(i)
|
||||
|| !threads.envelope_to_thread.contains_key(&i)
|
||||
{
|
||||
//let mailbox = &account[&self.cursor_pos.1];
|
||||
//log::debug!("key = {}", i);
|
||||
//log::debug!(
|
||||
// "name = {} {}",
|
||||
// mailbox.name(),
|
||||
// context.accounts[&self.cursor_pos.0].name()
|
||||
//);
|
||||
//log::debug!("{:#?}", context.accounts);
|
||||
if !context.accounts[&self.cursor_pos.0].contains_key(i) {
|
||||
log::debug!("key = {}", i);
|
||||
log::debug!(
|
||||
"name = {} {}",
|
||||
mailbox.name(),
|
||||
context.accounts[&self.cursor_pos.0].name()
|
||||
);
|
||||
log::debug!("{:#?}", context.accounts);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -795,16 +755,10 @@ impl PlainListing {
|
|||
);
|
||||
self.rows.row_attr_cache.insert(self.length, row_attr);
|
||||
|
||||
let mut entry_strings = self.make_entry_string(&envelope, context);
|
||||
entry_strings.highlight_self = should_highlight_self
|
||||
&& (envelope.recipient_any(&my_address) || envelope.sender_any(&my_address));
|
||||
row_widths.0.push(
|
||||
itoa_buffer
|
||||
.format(self.length)
|
||||
.len()
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
let entry_strings = self.make_entry_string(&envelope, context);
|
||||
row_widths
|
||||
.0
|
||||
.push(digits_of_num!(self.length).try_into().unwrap_or(255));
|
||||
row_widths.1.push(
|
||||
entry_strings
|
||||
.date
|
||||
|
@ -821,23 +775,19 @@ impl PlainListing {
|
|||
);
|
||||
row_widths.3.push(
|
||||
(entry_strings.flag.grapheme_width()
|
||||
+ usize::from(entry_strings.highlight_self) * highlight_self_colwidth)
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
); /* flags */
|
||||
row_widths.4.push(
|
||||
(entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width())
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
+ entry_strings.subject.grapheme_width()
|
||||
+ 1
|
||||
+ entry_strings.tags.grapheme_width())
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
min_width.1 = min_width.1.max(entry_strings.date.grapheme_width()); /* date */
|
||||
min_width.2 = min_width.2.max(entry_strings.from.grapheme_width()); /* from */
|
||||
min_width.3 = min_width.3.max(
|
||||
entry_strings.flag.grapheme_width()
|
||||
+ usize::from(entry_strings.highlight_self) * highlight_self_colwidth,
|
||||
); /* flags */
|
||||
min_width.4 = min_width.4.max(
|
||||
entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width(),
|
||||
+ entry_strings.subject.grapheme_width()
|
||||
+ 1
|
||||
+ entry_strings.tags.grapheme_width(),
|
||||
); /* tags + subject */
|
||||
self.rows.insert_thread(
|
||||
threads.envelope_to_thread[&i],
|
||||
|
@ -855,7 +805,6 @@ impl PlainListing {
|
|||
self.data_columns.elasticities[1].set_rigid();
|
||||
self.data_columns.elasticities[2].set_grow(15, Some(35));
|
||||
self.data_columns.elasticities[3].set_rigid();
|
||||
self.data_columns.elasticities[4].set_rigid();
|
||||
self.data_columns
|
||||
.cursor_config
|
||||
.set_handle(true)
|
||||
|
@ -873,15 +822,12 @@ impl PlainListing {
|
|||
_ = self.data_columns.columns[1].resize_with_context(min_width.1, self.rows.len(), context);
|
||||
/* from column */
|
||||
_ = self.data_columns.columns[2].resize_with_context(min_width.2, self.rows.len(), context);
|
||||
// flags column
|
||||
/* subject column */
|
||||
_ = self.data_columns.columns[3].resize_with_context(min_width.3, self.rows.len(), context);
|
||||
// subject column
|
||||
_ = self.data_columns.columns[4].resize_with_context(min_width.4, self.rows.len(), context);
|
||||
self.data_columns.segment_tree[0] = row_widths.0.into();
|
||||
self.data_columns.segment_tree[1] = row_widths.1.into();
|
||||
self.data_columns.segment_tree[2] = row_widths.2.into();
|
||||
self.data_columns.segment_tree[3] = row_widths.3.into();
|
||||
self.data_columns.segment_tree[4] = row_widths.4.into();
|
||||
|
||||
let iter = if self.filter_term.is_empty() {
|
||||
Box::new(self.local_collection.iter().cloned())
|
||||
|
@ -892,10 +838,8 @@ impl PlainListing {
|
|||
};
|
||||
|
||||
let columns = &mut self.data_columns.columns;
|
||||
let mut itoa_buffer = itoa::Buffer::new();
|
||||
for ((idx, i), (_, strings)) in iter.enumerate().zip(self.rows.entries.iter()) {
|
||||
if !context.accounts[&self.cursor_pos.0].contains_key(i) {
|
||||
//let mailbox = &account[&self.cursor_pos.1];
|
||||
//log::debug!("key = {}", i);
|
||||
//log::debug!(
|
||||
// "name = {} {}",
|
||||
|
@ -909,164 +853,137 @@ impl PlainListing {
|
|||
|
||||
let row_attr = self.rows.row_attr_cache[&idx];
|
||||
|
||||
{
|
||||
let mut area_col_0 = columns[0].area().nth_row(idx);
|
||||
if !*account_settings!(context[self.cursor_pos.0].listing.relative_list_indices) {
|
||||
area_col_0 = area_col_0.skip_cols(columns[0].grid_mut().write_string(
|
||||
itoa_buffer.format(idx),
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_0,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
for c in columns[0].grid().row_iter(area_col_0, 0..min_width.0, 0) {
|
||||
columns[0].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
}
|
||||
let (x, _) = {
|
||||
let area = columns[0].area().nth_row(idx);
|
||||
columns[0].grid_mut().write_string(
|
||||
&idx.to_string(),
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[0].area();
|
||||
columns[0].grid_mut().row_iter(area, x..min_width.0, idx)
|
||||
} {
|
||||
columns[0].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
{
|
||||
let mut area_col_1 = columns[1].area().nth_row(idx);
|
||||
area_col_1 = area_col_1.skip_cols(columns[1].grid_mut().write_string(
|
||||
let (x, _) = {
|
||||
let area = columns[1].area().nth_row(idx);
|
||||
columns[1].grid_mut().write_string(
|
||||
&strings.date,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_1,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
for c in columns[1].grid().row_iter(area_col_1, 0..min_width.1, 0) {
|
||||
columns[1].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[1].area();
|
||||
columns[1].grid_mut().row_iter(area, x..min_width.1, idx)
|
||||
} {
|
||||
columns[1].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
{
|
||||
let area_col_2 = columns[2].area().nth_row(idx);
|
||||
let (skip_cols, _) = columns[2].grid_mut().write_string(
|
||||
let (x, _) = {
|
||||
let area = columns[2].area().nth_row(idx);
|
||||
columns[2].grid_mut().write_string(
|
||||
&strings.from,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_2,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
{
|
||||
for text_formatter in crate::conf::text_format_regexps(context, "listing.from")
|
||||
{
|
||||
let t = columns[2].grid_mut().insert_tag(text_formatter.tag);
|
||||
for (start, end) in text_formatter.regexp.find_iter(strings.from.as_str()) {
|
||||
columns[2].grid_mut().set_tag(
|
||||
t,
|
||||
(start + skip_cols, idx),
|
||||
(end + skip_cols, idx),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in columns[2]
|
||||
.grid()
|
||||
.row_iter(area_col_2, skip_cols..min_width.2, 0)
|
||||
{
|
||||
columns[2].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[2].area();
|
||||
columns[2].grid_mut().row_iter(area, x..min_width.2, idx)
|
||||
} {
|
||||
columns[2].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs)
|
||||
.set_ch(' ');
|
||||
}
|
||||
{
|
||||
let mut area_col_3 = columns[3].area().nth_row(idx);
|
||||
area_col_3 = area_col_3.skip_cols(columns[3].grid_mut().write_string(
|
||||
let (x, _) = {
|
||||
let area = columns[3].area().nth_row(idx);
|
||||
columns[3].grid_mut().write_string(
|
||||
&strings.flag,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_3,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
if strings.highlight_self {
|
||||
let (x, _) = columns[3].grid_mut().write_string(
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_HIGHLIGHT_SELF_FLAG),
|
||||
self.color_cache.highlight_self.fg,
|
||||
)
|
||||
};
|
||||
let x = {
|
||||
let area = columns[3].area().nth_row(idx).skip_cols(x);
|
||||
columns[3]
|
||||
.grid_mut()
|
||||
.write_string(
|
||||
&strings.subject,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs | Attr::FORCE_TEXT,
|
||||
area_col_3,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for c in columns[3].grid().row_iter(area_col_3, 0..x, 0) {
|
||||
columns[3].grid_mut()[c].set_keep_fg(true);
|
||||
}
|
||||
area_col_3 = area_col_3.skip_cols(x + 1);
|
||||
)
|
||||
.0
|
||||
+ x
|
||||
};
|
||||
let mut x = x + 1;
|
||||
for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) {
|
||||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let _x = {
|
||||
let area = columns[3].area().nth_row(idx).skip_cols(x + 1);
|
||||
columns[3]
|
||||
.grid_mut()
|
||||
.write_string(
|
||||
t,
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
.0
|
||||
+ x
|
||||
+ 1
|
||||
};
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, x..(x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_bg(color);
|
||||
}
|
||||
for c in columns[3].grid().row_iter(area_col_3, 0..min_width.3, 0) {
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, _x..(_x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_bg(color).set_keep_bg(true);
|
||||
}
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, (x + 1)..(_x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut area_col_4 = columns[4].area().nth_row(idx);
|
||||
area_col_4 = area_col_4.skip_cols(columns[4].grid_mut().write_string(
|
||||
&strings.subject,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_4,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
{
|
||||
for text_formatter in
|
||||
crate::conf::text_format_regexps(context, "listing.subject")
|
||||
{
|
||||
let t = columns[4].grid_mut().insert_tag(text_formatter.tag);
|
||||
for (start, end) in
|
||||
text_formatter.regexp.find_iter(strings.subject.as_str())
|
||||
{
|
||||
columns[4].grid_mut().set_tag(t, (start, idx), (end, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
area_col_4 = area_col_4.skip_cols(1);
|
||||
for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) {
|
||||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let (x, _) = columns[4].grid_mut().write_string(
|
||||
t,
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
area_col_4.skip_cols(1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for c in columns[4].grid().row_iter(area_col_4, 0..(x + 1), 0) {
|
||||
columns[4].grid_mut()[c]
|
||||
.set_bg(color)
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
}
|
||||
area_col_4 = area_col_4.skip_cols(x + 1);
|
||||
}
|
||||
for c in columns[4].grid().row_iter(area_col_4, 0..min_width.4, 0) {
|
||||
columns[4].grid_mut()[c]
|
||||
.set_ch(' ')
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, x..(x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_keep_bg(true);
|
||||
}
|
||||
x = _x + 2;
|
||||
}
|
||||
}
|
||||
if self.length == 0 && self.filter_term.is_empty() {
|
||||
|
@ -1080,7 +997,6 @@ impl PlainListing {
|
|||
self.color_cache.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1110,34 +1026,44 @@ impl PlainListing {
|
|||
even: idx % 2 == 0,
|
||||
unseen: !envelope.is_seen(),
|
||||
highlighted: false,
|
||||
selected: self.rows.selection.get(&env_hash).copied().unwrap_or(false)
|
||||
selected: self.rows.selection[&env_hash]
|
||||
);
|
||||
self.rows.row_attr_cache.insert(idx, row_attr);
|
||||
|
||||
let strings = self.make_entry_string(&envelope, context);
|
||||
drop(envelope);
|
||||
let columns = &mut self.data_columns.columns;
|
||||
for n in 0..=4 {
|
||||
let area = columns[n].area().nth_row(idx);
|
||||
columns[n].grid_mut().clear_area(area, row_attr);
|
||||
{
|
||||
let area = columns[0].area().nth_row(idx);
|
||||
columns[0].grid_mut().clear_area(area, row_attr)
|
||||
};
|
||||
{
|
||||
let area = columns[1].area().nth_row(idx);
|
||||
columns[1].grid_mut().clear_area(area, row_attr);
|
||||
}
|
||||
{
|
||||
let area = columns[2].area().nth_row(idx);
|
||||
columns[2].grid_mut().clear_area(area, row_attr);
|
||||
}
|
||||
{
|
||||
let area = columns[3].area().nth_row(idx);
|
||||
columns[3].grid_mut().clear_area(area, row_attr);
|
||||
}
|
||||
|
||||
let mut itoa_buffer = itoa::Buffer::new();
|
||||
let (x, _) = {
|
||||
let area = columns[0].area().nth_row(idx);
|
||||
columns[0].grid_mut().write_string(
|
||||
itoa_buffer.format(idx),
|
||||
&idx.to_string(),
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[0].area().nth_row(idx);
|
||||
columns[0].grid_mut().row_iter(area, x..area.width(), 0)
|
||||
let area = columns[0].area();
|
||||
columns[0].grid_mut().row_iter(area, x..area.width(), idx)
|
||||
} {
|
||||
columns[0].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
|
@ -1152,12 +1078,11 @@ impl PlainListing {
|
|||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[1].area().nth_row(idx);
|
||||
columns[1].grid_mut().row_iter(area, x..area.width(), 0)
|
||||
let area = columns[1].area();
|
||||
columns[1].grid_mut().row_iter(area, x..area.width(), idx)
|
||||
} {
|
||||
columns[1].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
|
@ -1172,108 +1097,88 @@ impl PlainListing {
|
|||
row_attr.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[2].area().nth_row(idx);
|
||||
columns[2].grid_mut().row_iter(area, x..area.width(), 0)
|
||||
let area = columns[2].area();
|
||||
columns[2].grid_mut().row_iter(area, x..area.width(), idx)
|
||||
} {
|
||||
columns[2].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
{
|
||||
let mut area_col_3 = columns[3].area().nth_row(idx);
|
||||
area_col_3 = area_col_3.skip_cols(columns[3].grid_mut().write_string(
|
||||
let (x, _) = {
|
||||
let area = columns[3].area().nth_row(idx);
|
||||
columns[3].grid_mut().write_string(
|
||||
&strings.flag,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_3,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
if strings.highlight_self {
|
||||
let (x, _) = columns[3].grid_mut().write_string(
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_HIGHLIGHT_SELF_FLAG),
|
||||
self.color_cache.highlight_self.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs | Attr::FORCE_TEXT,
|
||||
area_col_3,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for c in columns[3].grid().row_iter(area_col_3, 0..x, 0) {
|
||||
columns[3].grid_mut()[c].set_keep_fg(true);
|
||||
}
|
||||
area_col_3 = area_col_3.skip_cols(x + 1);
|
||||
}
|
||||
for c in columns[3]
|
||||
.grid()
|
||||
.row_iter(area_col_3, 0..area_col_3.width(), 0)
|
||||
{
|
||||
columns[3].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut area_col_4 = columns[4].area().nth_row(idx);
|
||||
area_col_4 = area_col_4.skip_cols(columns[4].grid_mut().write_string(
|
||||
)
|
||||
};
|
||||
let (x, _) = {
|
||||
let area = columns[3].area().nth_row(idx).skip_cols(x);
|
||||
columns[3].grid_mut().write_string(
|
||||
&strings.subject,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_4,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
{
|
||||
for text_formatter in crate::conf::text_format_regexps(context, "listing.subject") {
|
||||
let t = columns[4].grid_mut().insert_tag(text_formatter.tag);
|
||||
for (start, end) in text_formatter.regexp.find_iter(strings.subject.as_str()) {
|
||||
columns[4].grid_mut().set_tag(t, (start, idx), (end, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
area_col_4 = area_col_4.skip_cols(1);
|
||||
)
|
||||
};
|
||||
let x = {
|
||||
let mut x = x + 1;
|
||||
for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) {
|
||||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let (x, _) = columns[4].grid_mut().write_string(
|
||||
t,
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
area_col_4.skip_cols(1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for c in columns[4].grid().row_iter(area_col_4, 0..(x + 1), 0) {
|
||||
columns[4].grid_mut()[c]
|
||||
.set_bg(color)
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
let (_x, _) = {
|
||||
let area = columns[3].area().nth_row(idx).skip_cols(x + 1);
|
||||
columns[3].grid_mut().write_string(
|
||||
t,
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
area,
|
||||
None,
|
||||
)
|
||||
};
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, x..(x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_bg(color);
|
||||
}
|
||||
area_col_4 = area_col_4.skip_cols(x + 1);
|
||||
}
|
||||
for c in columns[4]
|
||||
.grid()
|
||||
.row_iter(area_col_4, 0..area_col_4.width(), 0)
|
||||
{
|
||||
columns[4].grid_mut()[c]
|
||||
.set_ch(' ')
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, _x..(_x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_bg(color).set_keep_bg(true);
|
||||
}
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, (x + 1)..(_x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_keep_fg(true).set_keep_bg(true);
|
||||
}
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid_mut().row_iter(area, x..(x + 1), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c].set_keep_bg(true);
|
||||
}
|
||||
x = _x + 2;
|
||||
}
|
||||
x
|
||||
};
|
||||
for c in {
|
||||
let area = columns[3].area();
|
||||
columns[3].grid().row_iter(area, x..area.width(), idx)
|
||||
} {
|
||||
columns[3].grid_mut()[c]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
*self.rows.entries.get_mut(idx).unwrap() = ((thread_hash, env_hash), strings);
|
||||
}
|
||||
|
@ -1281,7 +1186,7 @@ impl PlainListing {
|
|||
fn select(
|
||||
&mut self,
|
||||
search_term: &str,
|
||||
results: Result<Vec<EnvelopeHash>>,
|
||||
results: Result<SmallVec<[EnvelopeHash; 512]>>,
|
||||
context: &mut Context,
|
||||
) {
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
|
@ -1324,50 +1229,6 @@ impl PlainListing {
|
|||
}
|
||||
}
|
||||
|
||||
fn draw_relative_numbers(&self, grid: &mut CellBuffer, area: Area, top_idx: usize) {
|
||||
let width = self.data_columns.columns[0].area().width();
|
||||
let area = area.take_cols(width);
|
||||
for i in 0..area.height() {
|
||||
if top_idx + i >= self.length {
|
||||
break;
|
||||
}
|
||||
let row_attr = if let Some(env_hash) = self.get_env_under_cursor(top_idx + i) {
|
||||
let unseen = self
|
||||
.rows
|
||||
.entries
|
||||
.get(top_idx + i)
|
||||
.map(|((_, _), strings)| strings.unseen)
|
||||
.unwrap_or(false);
|
||||
row_attr!(
|
||||
self.color_cache,
|
||||
even: (top_idx + i) % 2 == 0,
|
||||
unseen: unseen,
|
||||
highlighted: self.cursor_pos.2 == (top_idx + i),
|
||||
selected: self.rows.selection.get(&env_hash).copied().unwrap_or(false)
|
||||
)
|
||||
} else {
|
||||
row_attr!(self.color_cache, even: (top_idx + i) % 2 == 0, unseen: false, highlighted: true, selected: false)
|
||||
};
|
||||
|
||||
grid.clear_area(area.nth_row(i), row_attr);
|
||||
grid.write_string(
|
||||
&if self.new_cursor_pos.2.saturating_sub(top_idx) == i {
|
||||
self.new_cursor_pos.2.to_string()
|
||||
} else {
|
||||
(i as isize - (self.new_cursor_pos.2 - top_idx) as isize)
|
||||
.abs()
|
||||
.to_string()
|
||||
},
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area.nth_row(i),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_movement(&mut self, height: Option<usize>) {
|
||||
let rows = height.unwrap_or(1);
|
||||
if let Some(mvm) = self.movement.take() {
|
||||
|
@ -1443,7 +1304,6 @@ impl Component for PlainListing {
|
|||
self.color_cache.theme_default.bg,
|
||||
self.color_cache.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
Some(0),
|
||||
);
|
||||
|
||||
|
@ -1634,7 +1494,7 @@ impl Component for PlainListing {
|
|||
even: row % 2 == 0,
|
||||
unseen: !envelope.is_seen(),
|
||||
highlighted: false,
|
||||
selected: self.rows.selection.get(&env_hash).copied().unwrap_or(false)
|
||||
selected: self.rows.selection[&env_hash]
|
||||
);
|
||||
self.rows.row_attr_cache.insert(row, row_attr);
|
||||
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
|
||||
|
@ -1728,19 +1588,12 @@ impl Component for PlainListing {
|
|||
UIEvent::Input(ref key)
|
||||
if !self.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["select_entry"]) =>
|
||||
{
|
||||
if let Some(env_hash) = self.get_env_under_cursor(self.new_cursor_pos.2) {
|
||||
self.rows.update_selection_with_env(env_hash, |e| *e = !*e);
|
||||
self.set_dirty(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if !self.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["select_motion"]) =>
|
||||
{
|
||||
if self.modifier_active && self.modifier_command.is_none() {
|
||||
self.modifier_command = Some(Modifier::default());
|
||||
} else if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) {
|
||||
self.rows.update_selection_with_env(env_hash, |e| *e = !*e);
|
||||
self.set_dirty(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -1846,11 +1699,7 @@ impl Component for PlainListing {
|
|||
let handle = context.accounts[&self.cursor_pos.0]
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn(
|
||||
"search".into(),
|
||||
job,
|
||||
context.accounts[&self.cursor_pos.0].is_async(),
|
||||
);
|
||||
.spawn_specialized("search".into(), job);
|
||||
self.search_job = Some((filter_term.to_string(), handle));
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -1875,11 +1724,7 @@ impl Component for PlainListing {
|
|||
let mut handle = context.accounts[&self.cursor_pos.0]
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn(
|
||||
"select-by-search".into(),
|
||||
job,
|
||||
context.accounts[&self.cursor_pos.0].is_async(),
|
||||
);
|
||||
.spawn_specialized("select_by_search".into(), job);
|
||||
if let Ok(Some(search_result)) = try_recv_timeout!(&mut handle.chan) {
|
||||
self.select(search_term, search_result, context);
|
||||
} else {
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{convert::TryInto, iter::FromIterator};
|
||||
use std::{cmp, convert::TryInto, iter::FromIterator};
|
||||
|
||||
use melib::{Address, SortField, SortOrder, ThreadNode, Threads};
|
||||
|
||||
use super::*;
|
||||
use crate::{components::PageMovement, jobs::JoinHandle, segment_tree::SegmentTree};
|
||||
use crate::{components::PageMovement, jobs::JoinHandle};
|
||||
|
||||
macro_rules! row_attr {
|
||||
($color_cache:expr, even: $even:expr, unseen: $unseen:expr, highlighted: $highlighted:expr, selected: $selected:expr $(,)*) => {{
|
||||
|
@ -139,14 +139,13 @@ pub struct ThreadListing {
|
|||
color_cache: ColorCache,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
search_job: Option<(String, JoinHandle<Result<Vec<EnvelopeHash>>>)>,
|
||||
search_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
select_job: Option<(String, JoinHandle<Result<Vec<EnvelopeHash>>>)>,
|
||||
_select_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
|
||||
filter_term: String,
|
||||
filtered_selection: Vec<ThreadHash>,
|
||||
filtered_order: HashMap<ThreadHash, usize>,
|
||||
filtered_selection: Vec<EnvelopeHash>,
|
||||
_filtered_order: HashMap<EnvelopeHash, usize>,
|
||||
data_columns: DataColumns<5>,
|
||||
rows_drawn: SegmentTree,
|
||||
rows: RowsState<(ThreadHash, EnvelopeHash)>,
|
||||
seen_cache: IndexMap<EnvelopeHash, bool>,
|
||||
/// If we must redraw on next redraw event
|
||||
|
@ -168,17 +167,14 @@ impl MailListingTrait for ThreadListing {
|
|||
&mut self.rows.row_updates
|
||||
}
|
||||
|
||||
fn selection(&self) -> &HashMap<EnvelopeHash, bool> {
|
||||
&self.rows.selection
|
||||
}
|
||||
|
||||
fn selection_mut(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
|
||||
&mut self.rows.selection
|
||||
}
|
||||
|
||||
fn get_focused_items(&self, _context: &Context) -> SmallVec<[EnvelopeHash; 8]> {
|
||||
let is_selection_empty: bool = !self
|
||||
.selection()
|
||||
.rows
|
||||
.selection
|
||||
.values()
|
||||
.cloned()
|
||||
.any(std::convert::identity);
|
||||
|
@ -188,7 +184,13 @@ impl MailListingTrait for ThreadListing {
|
|||
.into_iter()
|
||||
.collect::<_>();
|
||||
}
|
||||
SmallVec::from_iter(self.selection().iter().filter(|(_, &v)| v).map(|(k, _)| *k))
|
||||
SmallVec::from_iter(
|
||||
self.rows
|
||||
.selection
|
||||
.iter()
|
||||
.filter(|(_, &v)| v)
|
||||
.map(|(k, _)| *k),
|
||||
)
|
||||
}
|
||||
|
||||
/// Fill the `self.content` `CellBuffer` with the contents of the account
|
||||
|
@ -209,7 +211,7 @@ impl MailListingTrait for ThreadListing {
|
|||
|
||||
// Get mailbox as a reference.
|
||||
//
|
||||
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1, true) {
|
||||
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
self.length = 0;
|
||||
|
@ -224,7 +226,6 @@ impl MailListingTrait for ThreadListing {
|
|||
self.color_cache.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -265,7 +266,6 @@ impl MailListingTrait for ThreadListing {
|
|||
self.color_cache.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -300,25 +300,6 @@ impl MailListingTrait for ThreadListing {
|
|||
.listing
|
||||
.threaded_repeat_identical_from_values
|
||||
);
|
||||
let should_highlight_self = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self
|
||||
)
|
||||
.is_true();
|
||||
let my_address: Address = context.accounts[&self.cursor_pos.0]
|
||||
.settings
|
||||
.account
|
||||
.main_identity_address();
|
||||
let highlight_self_colwidth: usize = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_HIGHLIGHT_SELF_FLAG)
|
||||
.grapheme_width();
|
||||
while let Some((indentation, thread_node_hash, has_sibling)) = iter.next() {
|
||||
let thread_node = &thread_nodes[&thread_node_hash];
|
||||
|
||||
|
@ -340,8 +321,6 @@ impl MailListingTrait for ThreadListing {
|
|||
prev_group = threads.find_group(thread_node.group);
|
||||
|
||||
let mut entry_strings = self.make_entry_string(&envelope, context);
|
||||
entry_strings.highlight_self = should_highlight_self
|
||||
&& (envelope.recipient_any(&my_address) || envelope.sender_any(&my_address));
|
||||
entry_strings.subject = SubjectString(Self::make_thread_entry(
|
||||
&envelope,
|
||||
indentation,
|
||||
|
@ -355,11 +334,7 @@ impl MailListingTrait for ThreadListing {
|
|||
entry_strings.from.clear();
|
||||
}
|
||||
hide_from = !threaded_repeat_identical_from_values
|
||||
&& matches!(
|
||||
iter.peek(),
|
||||
Some((_, tnh, _)) if thread_nodes[tnh].message().map(|next| account.collection.get_env(next).from() == envelope.from()
|
||||
&& threads.find_group(thread_nodes[tnh].group) == prev_group).unwrap_or(false)
|
||||
);
|
||||
&& matches!(iter.peek(), Some((_, tnh, _)) if thread_nodes[tnh].message().map(|next| account.collection.get_env(next).from() == envelope.from() && threads.find_group(thread_nodes[tnh].group) == prev_group).unwrap_or(false));
|
||||
row_widths.1.push(
|
||||
entry_strings
|
||||
.date
|
||||
|
@ -375,8 +350,9 @@ impl MailListingTrait for ThreadListing {
|
|||
.unwrap_or(255),
|
||||
); /* from */
|
||||
row_widths.3.push(
|
||||
(entry_strings.flag.grapheme_width()
|
||||
+ usize::from(entry_strings.highlight_self) * highlight_self_colwidth)
|
||||
entry_strings
|
||||
.flag
|
||||
.grapheme_width()
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
); /* flags */
|
||||
|
@ -387,13 +363,11 @@ impl MailListingTrait for ThreadListing {
|
|||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
min_width.1 = min_width.1.max(entry_strings.date.grapheme_width()); /* date */
|
||||
min_width.2 = min_width.2.max(entry_strings.from.grapheme_width()); /* from */
|
||||
min_width.3 = min_width.3.max(
|
||||
entry_strings.flag.grapheme_width()
|
||||
+ usize::from(entry_strings.highlight_self) * highlight_self_colwidth,
|
||||
); /* flags */
|
||||
min_width.4 = min_width.4.max(
|
||||
min_width.1 = cmp::max(min_width.1, entry_strings.date.grapheme_width()); /* date */
|
||||
min_width.2 = cmp::max(min_width.2, entry_strings.from.grapheme_width()); /* from */
|
||||
min_width.3 = cmp::max(min_width.3, entry_strings.flag.grapheme_width()); /* flags */
|
||||
min_width.4 = cmp::max(
|
||||
min_width.4,
|
||||
entry_strings.subject.grapheme_width()
|
||||
+ 1
|
||||
+ entry_strings.tags.grapheme_width(),
|
||||
|
@ -458,12 +432,6 @@ impl MailListingTrait for ThreadListing {
|
|||
_ = self.data_columns.columns[4].resize_with_context(min_width.4, self.rows.len(), context);
|
||||
self.data_columns.segment_tree[4] = row_widths.4.into();
|
||||
|
||||
self.rows_drawn = SegmentTree::from(
|
||||
std::iter::repeat(1)
|
||||
.take(self.rows.len())
|
||||
.collect::<SmallVec<_>>(),
|
||||
);
|
||||
debug_assert_eq!(self.rows_drawn.array.len(), self.rows.len());
|
||||
self.draw_rows(
|
||||
context,
|
||||
0,
|
||||
|
@ -514,10 +482,6 @@ impl ListingTrait for ThreadListing {
|
|||
self.focus = Focus::None;
|
||||
self.rows.clear();
|
||||
self.initialized = false;
|
||||
self.filtered_selection.clear();
|
||||
self.filtered_order.clear();
|
||||
self.filter_term.clear();
|
||||
self.data_columns.clear();
|
||||
}
|
||||
|
||||
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
|
@ -552,14 +516,16 @@ impl ListingTrait for ThreadListing {
|
|||
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
|
||||
|
||||
let top_idx = page_no * rows;
|
||||
let end_idx = self.length.saturating_sub(1).min(top_idx + rows - 1);
|
||||
self.draw_rows(context, top_idx, end_idx);
|
||||
|
||||
// If cursor position has changed, remove the highlight from the previous
|
||||
// position and apply it in the new one.
|
||||
if self.cursor_pos.2 != self.new_cursor_pos.2 && prev_page_no == page_no {
|
||||
let old_cursor_pos = self.cursor_pos;
|
||||
self.cursor_pos = self.new_cursor_pos;
|
||||
if *account_settings!(context[self.cursor_pos.0].listing.relative_list_indices) {
|
||||
self.draw_relative_numbers(grid, area, top_idx);
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
for &(idx, highlight) in &[(old_cursor_pos.2, false), (self.new_cursor_pos.2, true)] {
|
||||
if idx >= self.length {
|
||||
continue; //bounds check
|
||||
|
@ -575,10 +541,6 @@ impl ListingTrait for ThreadListing {
|
|||
}
|
||||
context.dirty_areas.push_back(new_area);
|
||||
}
|
||||
if *account_settings!(context[self.cursor_pos.0].listing.relative_list_indices) {
|
||||
self.draw_relative_numbers(grid, area, top_idx);
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
if !self.force_draw {
|
||||
return;
|
||||
}
|
||||
|
@ -595,6 +557,12 @@ impl ListingTrait for ThreadListing {
|
|||
grid.clear_area(area, self.color_cache.theme_default);
|
||||
}
|
||||
|
||||
self.draw_rows(
|
||||
context,
|
||||
top_idx,
|
||||
self.length.saturating_sub(1).min(top_idx + rows - 1),
|
||||
);
|
||||
|
||||
// Page_no has changed, so draw new page
|
||||
_ = self.data_columns.recalc_widths(area.size(), top_idx);
|
||||
// copy table columns
|
||||
|
@ -646,7 +614,7 @@ impl ListingTrait for ThreadListing {
|
|||
even: idx % 2 == 0,
|
||||
unseen: !envelope.is_seen(),
|
||||
highlighted: self.cursor_pos.2 == idx,
|
||||
selected: self.selection()[&env_hash],
|
||||
selected: self.rows.selection[&env_hash],
|
||||
);
|
||||
|
||||
let x = self.data_columns.widths[0]
|
||||
|
@ -671,55 +639,17 @@ impl ListingTrait for ThreadListing {
|
|||
}
|
||||
}
|
||||
|
||||
fn filter(&mut self, filter_term: String, results: Vec<EnvelopeHash>, context: &Context) {
|
||||
fn filter(
|
||||
&mut self,
|
||||
filter_term: String,
|
||||
_results: SmallVec<[EnvelopeHash; 512]>,
|
||||
context: &Context,
|
||||
) {
|
||||
if filter_term.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.length = 0;
|
||||
self.filtered_selection.clear();
|
||||
self.filtered_order.clear();
|
||||
self.filter_term = filter_term;
|
||||
self.rows.row_updates.clear();
|
||||
for v in self.selection_mut().values_mut() {
|
||||
*v = false;
|
||||
}
|
||||
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
let threads = account.collection.get_threads(self.cursor_pos.1);
|
||||
for env_hash in results {
|
||||
if !account.collection.contains_key(&env_hash) {
|
||||
continue;
|
||||
}
|
||||
let env_thread_node_hash = account.collection.get_env(env_hash).thread();
|
||||
if !threads.thread_nodes.contains_key(&env_thread_node_hash) {
|
||||
continue;
|
||||
}
|
||||
let thread = threads.find_group(threads.thread_nodes[&env_thread_node_hash].group);
|
||||
if self.filtered_order.contains_key(&thread) {
|
||||
continue;
|
||||
}
|
||||
if self.rows.all_threads.contains(&thread) {
|
||||
self.filtered_selection.push(thread);
|
||||
self.filtered_order
|
||||
.insert(thread, self.filtered_selection.len() - 1);
|
||||
}
|
||||
}
|
||||
if !self.filtered_selection.is_empty() {
|
||||
threads.group_inner_sort_by(
|
||||
&mut self.filtered_selection,
|
||||
self.sort,
|
||||
&context.accounts[&self.cursor_pos.0].collection.envelopes,
|
||||
);
|
||||
self.new_cursor_pos.2 = self.cursor_pos.2.min(self.filtered_selection.len() - 1);
|
||||
} else {
|
||||
_ = self.data_columns.columns[0].resize_with_context(0, 0, context);
|
||||
}
|
||||
self.redraw_threads_list(
|
||||
context,
|
||||
Box::new(self.filtered_selection.clone().into_iter())
|
||||
as Box<dyn Iterator<Item = ThreadHash>>,
|
||||
);
|
||||
let _account = &context.accounts[&self.cursor_pos.0];
|
||||
}
|
||||
|
||||
fn view_area(&self) -> Option<Area> {
|
||||
|
@ -730,10 +660,6 @@ impl ListingTrait for ThreadListing {
|
|||
!matches!(self.focus, Focus::None)
|
||||
}
|
||||
|
||||
fn modifier_active(&self) -> bool {
|
||||
self.modifier_active
|
||||
}
|
||||
|
||||
fn set_modifier_active(&mut self, new_val: bool) {
|
||||
self.modifier_active = new_val;
|
||||
}
|
||||
|
@ -811,23 +737,21 @@ impl ThreadListing {
|
|||
coordinates: (AccountHash, MailboxHash),
|
||||
context: &Context,
|
||||
) -> Box<Self> {
|
||||
let color_cache = ColorCache::new(context, IndexStyle::Threaded);
|
||||
Box::new(Self {
|
||||
cursor_pos: (coordinates.0, MailboxHash::default(), 0),
|
||||
new_cursor_pos: (coordinates.0, coordinates.1, 0),
|
||||
length: 0,
|
||||
sort: (Default::default(), Default::default()),
|
||||
subsort: (Default::default(), Default::default()),
|
||||
data_columns: DataColumns::new(color_cache.theme_default),
|
||||
color_cache,
|
||||
rows_drawn: SegmentTree::default(),
|
||||
color_cache: ColorCache::new(context, IndexStyle::Threaded),
|
||||
data_columns: DataColumns::default(),
|
||||
rows: RowsState::default(),
|
||||
seen_cache: IndexMap::default(),
|
||||
filter_term: String::new(),
|
||||
search_job: None,
|
||||
select_job: None,
|
||||
_select_job: None,
|
||||
filtered_selection: Vec::new(),
|
||||
filtered_order: HashMap::default(),
|
||||
_filtered_order: HashMap::default(),
|
||||
dirty: true,
|
||||
force_draw: true,
|
||||
focus: Focus::None,
|
||||
|
@ -942,16 +866,15 @@ impl ThreadListing {
|
|||
subject: SubjectString(subject),
|
||||
flag: FlagString::new(
|
||||
e.flags(),
|
||||
self.selection().get(&e.hash()).cloned().unwrap_or(false),
|
||||
self.rows.selection.get(&e.hash()).cloned().unwrap_or(false),
|
||||
/* snoozed */ false,
|
||||
!e.is_seen(),
|
||||
e.has_attachments(),
|
||||
context,
|
||||
(self.cursor_pos.0, self.cursor_pos.1),
|
||||
),
|
||||
from: FromString(Address::display_name_slice(e.from(), None)),
|
||||
from: FromString(Address::display_name_slice(e.from())),
|
||||
tags: TagString(tags, colors),
|
||||
unseen: !e.is_seen(),
|
||||
highlight_self: false,
|
||||
}
|
||||
}
|
||||
|
@ -961,12 +884,6 @@ impl ThreadListing {
|
|||
return;
|
||||
}
|
||||
debug_assert!(end >= start);
|
||||
if self.rows_drawn.get_max(start, end) == 0 {
|
||||
return;
|
||||
}
|
||||
for i in start..=end {
|
||||
self.rows_drawn.update(i, 0);
|
||||
}
|
||||
let min_width = (
|
||||
self.data_columns.columns[0].area().width(),
|
||||
self.data_columns.columns[1].area().width(),
|
||||
|
@ -974,8 +891,7 @@ impl ThreadListing {
|
|||
self.data_columns.columns[3].area().width(),
|
||||
self.data_columns.columns[4].area().width(),
|
||||
);
|
||||
let columns = &mut self.data_columns.columns;
|
||||
let mut itoa_buffer = itoa::Buffer::new();
|
||||
|
||||
for (idx, ((_thread_hash, env_hash), strings)) in self
|
||||
.rows
|
||||
.entries
|
||||
|
@ -991,164 +907,152 @@ impl ThreadListing {
|
|||
self.color_cache,
|
||||
even: idx % 2 == 0,
|
||||
unseen: !self.seen_cache[env_hash],
|
||||
highlighted: false,
|
||||
selected: false,
|
||||
highlighted: self.cursor_pos.2 == idx,
|
||||
selected: self.rows.selection[env_hash]
|
||||
);
|
||||
self.rows.row_attr_cache.insert(idx, row_attr);
|
||||
{
|
||||
let mut area_col_0 = columns[0].area().nth_row(idx);
|
||||
let area = self.data_columns.columns[0].area();
|
||||
if !*account_settings!(context[self.cursor_pos.0].listing.relative_list_indices) {
|
||||
area_col_0 = area_col_0.skip_cols(columns[0].grid_mut().write_string(
|
||||
itoa_buffer.format(idx),
|
||||
let (x, _) = self.data_columns.columns[0].grid_mut().write_string(
|
||||
&idx.to_string(),
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_0,
|
||||
area.nth_row(idx),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
for c in columns[0].grid().row_iter(area_col_0, 0..min_width.0, 0) {
|
||||
columns[0].grid_mut()[c]
|
||||
);
|
||||
for x in x..min_width.0 {
|
||||
self.data_columns.columns[0].grid_mut()[(x, idx)]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut area_col_1 = columns[1].area().nth_row(idx);
|
||||
area_col_1 = area_col_1.skip_cols(columns[1].grid_mut().write_string(
|
||||
let area = self.data_columns.columns[1].area();
|
||||
let (x, _) = self.data_columns.columns[1].grid_mut().write_string(
|
||||
&strings.date,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_1,
|
||||
area.nth_row(idx),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
for c in columns[1].grid().row_iter(area_col_1, 0..min_width.1, 0) {
|
||||
columns[1].grid_mut()[c]
|
||||
);
|
||||
for x in x..min_width.1 {
|
||||
self.data_columns.columns[1].grid_mut()[(x, idx)]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
}
|
||||
{
|
||||
let area_col_2 = columns[2].area().nth_row(idx);
|
||||
let (skip_cols, _) = columns[2].grid_mut().write_string(
|
||||
let area = self.data_columns.columns[2].area();
|
||||
let (x, _) = self.data_columns.columns[2].grid_mut().write_string(
|
||||
&strings.from,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_2,
|
||||
None,
|
||||
area.nth_row(idx),
|
||||
None,
|
||||
);
|
||||
#[cfg(feature = "regexp")]
|
||||
{
|
||||
for text_formatter in crate::conf::text_format_regexps(context, "listing.from")
|
||||
{
|
||||
let t = columns[2].grid_mut().insert_tag(text_formatter.tag);
|
||||
let t = self.data_columns.columns[2]
|
||||
.grid_mut()
|
||||
.insert_tag(text_formatter.tag);
|
||||
for (start, end) in text_formatter.regexp.find_iter(strings.from.as_str()) {
|
||||
columns[2].grid_mut().set_tag(
|
||||
self.data_columns.columns[2].grid_mut().set_tag(
|
||||
t,
|
||||
(start + skip_cols, idx),
|
||||
(end + skip_cols, idx),
|
||||
(start, idx),
|
||||
(end, idx),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in columns[2]
|
||||
.grid()
|
||||
.row_iter(area_col_2, skip_cols..min_width.2, 0)
|
||||
{
|
||||
columns[2].grid_mut()[c]
|
||||
for x in x..min_width.2 {
|
||||
self.data_columns.columns[2].grid_mut()[(x, idx)]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut area_col_3 = columns[3].area().nth_row(idx);
|
||||
area_col_3 = area_col_3.skip_cols(columns[3].grid_mut().write_string(
|
||||
let area = self.data_columns.columns[3].area();
|
||||
let (x, _) = self.data_columns.columns[3].grid_mut().write_string(
|
||||
&strings.flag,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_3,
|
||||
area.nth_row(idx),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
if strings.highlight_self {
|
||||
let (x, _) = columns[3].grid_mut().write_string(
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_HIGHLIGHT_SELF_FLAG),
|
||||
self.color_cache.highlight_self.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs | Attr::FORCE_TEXT,
|
||||
area_col_3,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for c in columns[3].grid().row_iter(area_col_3, 0..x, 0) {
|
||||
columns[3].grid_mut()[c].set_keep_fg(true);
|
||||
}
|
||||
area_col_3 = area_col_3.skip_cols(x + 1);
|
||||
}
|
||||
for c in columns[3].grid().row_iter(area_col_3, 0..min_width.3, 0) {
|
||||
columns[3].grid_mut()[c]
|
||||
);
|
||||
for x in x..min_width.3 {
|
||||
self.data_columns.columns[3].grid_mut()[(x, idx)]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut area_col_4 = columns[4].area().nth_row(idx);
|
||||
area_col_4 = area_col_4.skip_cols(columns[4].grid_mut().write_string(
|
||||
let area = self.data_columns.columns[4].area();
|
||||
let (x, _) = self.data_columns.columns[4].grid_mut().write_string(
|
||||
&strings.subject,
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area_col_4,
|
||||
area.nth_row(idx),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
);
|
||||
#[cfg(feature = "regexp")]
|
||||
{
|
||||
for text_formatter in
|
||||
crate::conf::text_format_regexps(context, "listing.subject")
|
||||
{
|
||||
let t = columns[4].grid_mut().insert_tag(text_formatter.tag);
|
||||
let t = self.data_columns.columns[4]
|
||||
.grid_mut()
|
||||
.insert_tag(text_formatter.tag);
|
||||
for (start, end) in
|
||||
text_formatter.regexp.find_iter(strings.subject.as_str())
|
||||
{
|
||||
columns[4].grid_mut().set_tag(t, (start, idx), (end, idx));
|
||||
self.data_columns.columns[4].grid_mut().set_tag(
|
||||
t,
|
||||
(start, idx),
|
||||
(end, idx),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
area_col_4 = area_col_4.skip_cols(1);
|
||||
for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) {
|
||||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let (x, _) = columns[4].grid_mut().write_string(
|
||||
t,
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
area_col_4.skip_cols(1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for c in columns[4].grid().row_iter(area_col_4, 0..(x + 1), 0) {
|
||||
columns[4].grid_mut()[c]
|
||||
.set_bg(color)
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
let x = {
|
||||
let mut x = x + 1;
|
||||
let area = self.data_columns.columns[4].area();
|
||||
for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) {
|
||||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let (_x, _) = self.data_columns.columns[4].grid_mut().write_string(
|
||||
t,
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
area.nth_row(idx).skip_cols(x + 1),
|
||||
None,
|
||||
);
|
||||
self.data_columns.columns[4].grid_mut()[(x, idx)].set_bg(color);
|
||||
if _x < min_width.4 {
|
||||
self.data_columns.columns[4].grid_mut()[(_x, idx)]
|
||||
.set_bg(color)
|
||||
.set_keep_bg(true);
|
||||
}
|
||||
for x in (x + 1).._x {
|
||||
self.data_columns.columns[4].grid_mut()[(x, idx)]
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
}
|
||||
self.data_columns.columns[4].grid_mut()[(x, idx)].set_keep_bg(true);
|
||||
x = _x + 1;
|
||||
}
|
||||
area_col_4 = area_col_4.skip_cols(x + 1);
|
||||
}
|
||||
for c in columns[4].grid().row_iter(area_col_4, 0..min_width.4, 0) {
|
||||
columns[4].grid_mut()[c]
|
||||
x
|
||||
};
|
||||
for x in x..min_width.4 {
|
||||
self.data_columns.columns[4].grid_mut()[(x, idx)]
|
||||
.set_ch(' ')
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
|
@ -1173,114 +1077,61 @@ impl ThreadListing {
|
|||
even: idx % 2 == 0,
|
||||
unseen: !envelope.is_seen(),
|
||||
highlighted: false,
|
||||
selected: self.selection()[&env_hash]
|
||||
selected: self.rows.selection[&env_hash]
|
||||
);
|
||||
self.seen_cache.insert(env_hash, envelope.is_seen());
|
||||
|
||||
let should_highlight_self = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.highlight_self
|
||||
)
|
||||
.is_true();
|
||||
let mut entry_strings = self.make_entry_string(&envelope, context);
|
||||
entry_strings.highlight_self = should_highlight_self && {
|
||||
let my_address: Address = context.accounts[&self.cursor_pos.0]
|
||||
.settings
|
||||
.account
|
||||
.main_identity_address();
|
||||
envelope.recipient_any(&my_address) || envelope.sender_any(&my_address)
|
||||
};
|
||||
// [ref:FIXME]: generate new tree indentation for this new row subject
|
||||
// entry_strings.subject = SubjectString(Self::make_thread_entry(
|
||||
// &envelope,
|
||||
// indentation,
|
||||
// thread_node_hash,
|
||||
// &threads,
|
||||
// &indentations,
|
||||
// has_sibling,
|
||||
// is_root,
|
||||
// ));
|
||||
let mut strings = self.make_entry_string(&envelope, context);
|
||||
drop(envelope);
|
||||
std::mem::swap(
|
||||
&mut self.rows.entries.get_mut(idx).unwrap().1.subject,
|
||||
&mut entry_strings.subject,
|
||||
&mut strings.subject,
|
||||
);
|
||||
let columns = &mut self.data_columns.columns;
|
||||
for n in 0..=4 {
|
||||
let area = columns[n].area().nth_row(idx);
|
||||
columns[n].grid_mut().clear_area(area, row_attr);
|
||||
}
|
||||
self.rows_drawn.update(idx, 1);
|
||||
|
||||
*self.rows.entries.get_mut(idx).unwrap() = ((thread_hash, env_hash), entry_strings);
|
||||
*self.rows.entries.get_mut(idx).unwrap() = ((thread_hash, env_hash), strings);
|
||||
}
|
||||
|
||||
fn select(
|
||||
&mut self,
|
||||
search_term: &str,
|
||||
results: Result<Vec<EnvelopeHash>>,
|
||||
context: &mut Context,
|
||||
) {
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
match results {
|
||||
Ok(results) => {
|
||||
let threads = account.collection.get_threads(self.cursor_pos.1);
|
||||
for env_hash in results {
|
||||
if !account.collection.contains_key(&env_hash) {
|
||||
continue;
|
||||
}
|
||||
let env_thread_node_hash = account.collection.get_env(env_hash).thread();
|
||||
if !threads.thread_nodes.contains_key(&env_thread_node_hash) {
|
||||
continue;
|
||||
}
|
||||
let thread =
|
||||
threads.find_group(threads.thread_nodes[&env_thread_node_hash].group);
|
||||
if self.rows.all_threads.contains(&thread) {
|
||||
self.selection_mut()
|
||||
.entry(env_hash)
|
||||
.and_modify(|entry| *entry = true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.cursor_pos.2 = 0;
|
||||
self.new_cursor_pos.2 = 0;
|
||||
let message = format!(
|
||||
"Encountered an error while searching for `{}`: {}.",
|
||||
search_term, &err
|
||||
);
|
||||
log::error!("{}", message);
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not perform search".into()),
|
||||
source: None,
|
||||
body: message.into(),
|
||||
kind: Some(crate::types::NotificationType::Error(err.kind)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_relative_numbers(&self, grid: &mut CellBuffer, area: Area, top_idx: usize) {
|
||||
fn draw_relative_numbers(&mut self, grid: &mut CellBuffer, area: Area, top_idx: usize) {
|
||||
let width = self.data_columns.columns[0].area().width();
|
||||
let area = area.take_cols(width);
|
||||
for i in 0..area.height() {
|
||||
if top_idx + i >= self.length {
|
||||
break;
|
||||
}
|
||||
let row_attr = if let Some(env_hash) = self.get_env_under_cursor(top_idx + i) {
|
||||
row_attr!(
|
||||
self.color_cache,
|
||||
even: (top_idx + i) % 2 == 0,
|
||||
unseen: !self.seen_cache[&env_hash],
|
||||
highlighted: self.cursor_pos.2 == (top_idx + i),
|
||||
selected: self.selection()[&env_hash]
|
||||
selected: self.rows.selection[&env_hash]
|
||||
)
|
||||
} else {
|
||||
row_attr!(self.color_cache, even: (top_idx + i) % 2 == 0, unseen: false, highlighted: true, selected: false)
|
||||
};
|
||||
|
||||
grid.clear_area(area.nth_row(i), row_attr);
|
||||
let idx_col_area = self.data_columns.columns[0].area();
|
||||
self.data_columns.columns[0]
|
||||
.grid_mut()
|
||||
.clear_area(idx_col_area, row_attr);
|
||||
|
||||
grid.clear_area(area.nth_row(i).take_cols(width), row_attr);
|
||||
self.data_columns.columns[0].grid_mut().write_string(
|
||||
&if self.new_cursor_pos.2.saturating_sub(top_idx) == i {
|
||||
self.new_cursor_pos.2.to_string()
|
||||
} else {
|
||||
(i as isize - (self.new_cursor_pos.2 - top_idx) as isize)
|
||||
.abs()
|
||||
.to_string()
|
||||
},
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
idx_col_area.nth_row(i),
|
||||
None,
|
||||
);
|
||||
|
||||
grid.write_string(
|
||||
&if self.new_cursor_pos.2.saturating_sub(top_idx) == i {
|
||||
self.new_cursor_pos.2.to_string()
|
||||
|
@ -1292,8 +1143,7 @@ impl ThreadListing {
|
|||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
area.nth_row(i),
|
||||
None,
|
||||
area.nth_row(i).take_cols(width),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
@ -1374,7 +1224,6 @@ impl Component for ThreadListing {
|
|||
self.color_cache.theme_default.bg,
|
||||
self.color_cache.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
Some(0),
|
||||
);
|
||||
|
||||
|
@ -1568,7 +1417,7 @@ impl Component for ThreadListing {
|
|||
even: row % 2 == 0,
|
||||
unseen: !envelope.is_seen(),
|
||||
highlighted: false,
|
||||
selected: self.selection()[&env_hash]
|
||||
selected: self.rows.selection[&env_hash]
|
||||
);
|
||||
self.rows.row_attr_cache.insert(row, row_attr);
|
||||
self.force_draw |= row >= top_idx && row < top_idx + rows;
|
||||
|
@ -1576,6 +1425,7 @@ impl Component for ThreadListing {
|
|||
if self.force_draw {
|
||||
/* Draw the entire list */
|
||||
self.draw_list(grid, area, context);
|
||||
self.force_draw = false;
|
||||
}
|
||||
} else {
|
||||
/* Draw the entire list */
|
||||
|
@ -1588,7 +1438,6 @@ impl Component for ThreadListing {
|
|||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
}
|
||||
self.force_draw = false;
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
|
@ -1677,7 +1526,7 @@ impl Component for ThreadListing {
|
|||
return false;
|
||||
}
|
||||
self.rows.rename_env(*old_hash, *new_hash);
|
||||
self.seen_cache.shift_remove(old_hash);
|
||||
self.seen_cache.remove(old_hash);
|
||||
self.seen_cache
|
||||
.insert(*new_hash, account.collection.get_env(*new_hash).is_seen());
|
||||
if let Some(&row) = self.rows.env_order.get(new_hash) {
|
||||
|
@ -1689,7 +1538,7 @@ impl Component for ThreadListing {
|
|||
UIEvent::EnvelopeRemove(ref env_hash, _) => {
|
||||
if self.rows.contains_env(*env_hash) {
|
||||
self.refresh_mailbox(context, false);
|
||||
self.seen_cache.shift_remove(env_hash);
|
||||
self.seen_cache.remove(env_hash);
|
||||
self.set_dirty(true);
|
||||
}
|
||||
}
|
||||
|
@ -1712,28 +1561,15 @@ impl Component for ThreadListing {
|
|||
UIEvent::Resize => {
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::Input(Key::Esc) if !self.unfocused() && !self.filter_term.is_empty() => {
|
||||
self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1));
|
||||
self.refresh_mailbox(context, false);
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if !self.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["select_entry"]) =>
|
||||
{
|
||||
if let Some(env_hash) = self.get_env_under_cursor(self.new_cursor_pos.2) {
|
||||
self.rows.update_selection_with_env(env_hash, |e| *e = !*e);
|
||||
self.set_dirty(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if !self.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["select_motion"]) =>
|
||||
{
|
||||
if self.modifier_active && self.modifier_command.is_none() {
|
||||
self.modifier_command = Some(Modifier::default());
|
||||
} else if let Some(env_hash) = self.get_env_under_cursor(self.new_cursor_pos.2) {
|
||||
self.rows.update_selection_with_env(env_hash, |e| *e = !*e);
|
||||
self.set_dirty(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -1760,46 +1596,9 @@ impl Component for ThreadListing {
|
|||
let handle = context.accounts[&self.new_cursor_pos.0]
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn(
|
||||
"search".into(),
|
||||
job,
|
||||
context.accounts[&self.new_cursor_pos.0].is_async(),
|
||||
);
|
||||
.spawn_specialized("search".into(), job);
|
||||
self.search_job = Some((filter_term.to_string(), handle));
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not perform search".into()),
|
||||
body: err.to_string().into(),
|
||||
kind: Some(crate::types::NotificationType::Error(err.kind)),
|
||||
source: Some(err),
|
||||
});
|
||||
}
|
||||
};
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
Action::Listing(Select(ref search_term)) if !self.unfocused() => {
|
||||
match context.accounts[&self.cursor_pos.0].search(
|
||||
search_term,
|
||||
self.sort,
|
||||
self.cursor_pos.1,
|
||||
) {
|
||||
Ok(job) => {
|
||||
let mut handle = context.accounts[&self.cursor_pos.0]
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn(
|
||||
"select-by-search".into(),
|
||||
job,
|
||||
context.accounts[&self.cursor_pos.0].is_async(),
|
||||
);
|
||||
if let Ok(Some(search_result)) = try_recv_timeout!(&mut handle.chan) {
|
||||
self.select(search_term, search_result, context);
|
||||
} else {
|
||||
self.select_job = Some((search_term.to_string(), handle));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not perform search".into()),
|
||||
|
@ -1829,29 +1628,14 @@ impl Component for ThreadListing {
|
|||
Ok(Some(Err(err))) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not perform search".into()),
|
||||
source: None,
|
||||
body: err.to_string().into(),
|
||||
kind: Some(crate::types::NotificationType::Error(err.kind)),
|
||||
source: Some(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
|
||||
if self
|
||||
.select_job
|
||||
.as_ref()
|
||||
.map(|(_, j)| j == job_id)
|
||||
.unwrap_or(false) =>
|
||||
{
|
||||
let (search_term, mut handle) = self.select_job.take().unwrap();
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => { /* search was canceled */ }
|
||||
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
|
||||
Ok(Some(results)) => self.select(&search_term, results, context),
|
||||
}
|
||||
self.set_dirty(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
|
|
|
@ -23,6 +23,7 @@ use std::{
|
|||
collections::{hash_map::DefaultHasher, BTreeMap},
|
||||
future::Future,
|
||||
hash::{Hash, Hasher},
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
|
@ -33,11 +34,10 @@ use melib::{
|
|||
},
|
||||
error::*,
|
||||
gpgme::*,
|
||||
log,
|
||||
parser::BytesExt,
|
||||
};
|
||||
|
||||
use super::AttachmentBoxFuture;
|
||||
|
||||
pub async fn decrypt(raw: Vec<u8>) -> Result<(melib_pgp::DecryptionMetadata, Vec<u8>)> {
|
||||
let mut ctx = Context::new()?;
|
||||
let cipher = ctx.new_data_mem(&raw)?;
|
||||
|
@ -81,128 +81,18 @@ async fn verify_inner(a: Attachment, cache: Arc<Mutex<BTreeMap<u64, Result<()>>>
|
|||
}
|
||||
|
||||
pub fn sign_filter(
|
||||
default_key: Option<String>,
|
||||
mut sign_keys: Vec<Key>,
|
||||
) -> Result<impl FnOnce(AttachmentBuilder) -> AttachmentBoxFuture + Send> {
|
||||
Ok(move |a: AttachmentBuilder| -> AttachmentBoxFuture {
|
||||
Box::pin(async move {
|
||||
if let Some(default_key) = default_key {
|
||||
let mut ctx = Context::new()?;
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
let keys = ctx.keylist(false, Some(default_key.clone()))?.await?;
|
||||
if keys.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Could not locate sign key with ID `{}`",
|
||||
default_key
|
||||
)));
|
||||
}
|
||||
sign_keys.extend(keys);
|
||||
}
|
||||
if sign_keys.is_empty() {
|
||||
return Err(Error::new(
|
||||
"No key was selected for signing; please select one.",
|
||||
));
|
||||
}
|
||||
let a: Attachment = a.into();
|
||||
let mut ctx = Context::new()?;
|
||||
let data = ctx.new_data_mem(&melib_pgp::convert_attachment_to_rfc_spec(
|
||||
a.into_raw().as_bytes(),
|
||||
))?;
|
||||
let sig_attachment = Attachment::new(
|
||||
ContentType::PGPSignature,
|
||||
Default::default(),
|
||||
ctx.sign(sign_keys, data)?.await?,
|
||||
);
|
||||
let a: AttachmentBuilder = a.into();
|
||||
let parts = vec![a, sig_attachment.into()];
|
||||
let boundary = ContentType::make_boundary(&parts);
|
||||
Ok(Attachment::new(
|
||||
ContentType::Multipart {
|
||||
boundary: boundary.into_bytes(),
|
||||
kind: MultipartType::Signed,
|
||||
parts: parts.into_iter().map(|a| a.into()).collect::<Vec<_>>(),
|
||||
parameters: vec![],
|
||||
},
|
||||
Default::default(),
|
||||
vec![],
|
||||
)
|
||||
.into())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encrypt_filter(
|
||||
encrypt_for_self: Option<melib::Address>,
|
||||
default_sign_key: Option<String>,
|
||||
mut sign_keys: Option<Vec<Key>>,
|
||||
default_encrypt_key: Option<String>,
|
||||
mut encrypt_keys: Vec<Key>,
|
||||
) -> Result<impl FnOnce(AttachmentBuilder) -> AttachmentBoxFuture + Send> {
|
||||
Ok(move |a: AttachmentBuilder| -> AttachmentBoxFuture {
|
||||
Box::pin(async move {
|
||||
if let Some(default_key) = default_sign_key {
|
||||
let mut ctx = Context::new()?;
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
let keys = ctx.keylist(true, Some(default_key.clone()))?.await?;
|
||||
if keys.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Could not locate sign key with ID `{}`",
|
||||
default_key
|
||||
)));
|
||||
}
|
||||
if let Some(ref mut sign_keys) = sign_keys {
|
||||
sign_keys.extend(keys);
|
||||
} else {
|
||||
sign_keys = Some(keys);
|
||||
}
|
||||
}
|
||||
if let Some(ref sign_keys) = sign_keys {
|
||||
if sign_keys.is_empty() {
|
||||
return Err(Error::new(
|
||||
"No key was selected for signing; please select one.",
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(default_key) = default_encrypt_key {
|
||||
let mut ctx = Context::new()?;
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
let keys = ctx.keylist(false, Some(default_key.clone()))?.await?;
|
||||
if keys.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Could not locate encryption key with ID `{}`",
|
||||
default_key
|
||||
)));
|
||||
}
|
||||
encrypt_keys.extend(keys);
|
||||
}
|
||||
if encrypt_keys.is_empty() {
|
||||
return Err(Error::new(
|
||||
"No key was selected for encryption; please select one.",
|
||||
));
|
||||
}
|
||||
if let Some(encrypt_for_self) = encrypt_for_self {
|
||||
let mut ctx = Context::new()?;
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
let keys = ctx
|
||||
.keylist(false, Some(encrypt_for_self.to_string()))?
|
||||
.await?;
|
||||
if keys.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Could not locate personal encryption key for address `{}`",
|
||||
encrypt_for_self
|
||||
)));
|
||||
}
|
||||
for key in keys {
|
||||
if !encrypt_keys.contains(&key) {
|
||||
encrypt_keys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
let a: Attachment = if let Some(sign_keys) = sign_keys {
|
||||
sign_keys: Vec<Key>,
|
||||
) -> Result<
|
||||
impl FnOnce(AttachmentBuilder) -> Pin<Box<dyn Future<Output = Result<AttachmentBuilder>> + Send>>
|
||||
+ Send,
|
||||
> {
|
||||
Ok(
|
||||
move |a: AttachmentBuilder| -> Pin<Box<dyn Future<Output = Result<AttachmentBuilder>>+Send>> {
|
||||
Box::pin(async move {
|
||||
let a: Attachment = a.into();
|
||||
let mut ctx = Context::new()?;
|
||||
let data = ctx.new_data_mem(&melib_pgp::convert_attachment_to_rfc_spec(
|
||||
a.into_raw().as_bytes(),
|
||||
a.into_raw().as_bytes(),
|
||||
))?;
|
||||
let sig_attachment = Attachment::new(
|
||||
ContentType::PGPSignature,
|
||||
|
@ -212,7 +102,7 @@ pub fn encrypt_filter(
|
|||
let a: AttachmentBuilder = a.into();
|
||||
let parts = vec![a, sig_attachment.into()];
|
||||
let boundary = ContentType::make_boundary(&parts);
|
||||
Attachment::new(
|
||||
Ok(Attachment::new(
|
||||
ContentType::Multipart {
|
||||
boundary: boundary.into_bytes(),
|
||||
kind: MultipartType::Signed,
|
||||
|
@ -222,42 +112,56 @@ pub fn encrypt_filter(
|
|||
Default::default(),
|
||||
vec![],
|
||||
)
|
||||
} else {
|
||||
a.into()
|
||||
};
|
||||
let mut ctx = Context::new()?;
|
||||
let data = ctx.new_data_mem(a.into_raw().as_bytes())?;
|
||||
.into())
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
let enc_attachment = {
|
||||
let mut a = Attachment::new(
|
||||
ContentType::OctetStream {
|
||||
name: None,
|
||||
pub fn encrypt_filter(
|
||||
sign_keys: Option<Vec<Key>>,
|
||||
encrypt_keys: Vec<Key>,
|
||||
) -> Result<
|
||||
impl FnOnce(AttachmentBuilder) -> Pin<Box<dyn Future<Output = Result<AttachmentBuilder>> + Send>>
|
||||
+ Send,
|
||||
> {
|
||||
Ok(
|
||||
move |a: AttachmentBuilder| -> Pin<Box<dyn Future<Output = Result<AttachmentBuilder>>+Send>> {
|
||||
Box::pin(async move {
|
||||
let a: Attachment = a.into();
|
||||
log::trace!("main attachment is {:?}", &a);
|
||||
let mut ctx = Context::new()?;
|
||||
let data = ctx.new_data_mem(
|
||||
a.into_raw().as_bytes()
|
||||
)?;
|
||||
|
||||
let sig_attachment = {
|
||||
let mut a = Attachment::new(
|
||||
ContentType::OctetStream { name: None, parameters: vec![] },
|
||||
Default::default(),
|
||||
ctx.encrypt(sign_keys, encrypt_keys, data)?.await?,
|
||||
);
|
||||
a.content_disposition = ContentDisposition::from(br#"attachment; filename="msg.asc""#);
|
||||
a
|
||||
};
|
||||
let mut a: AttachmentBuilder = AttachmentBuilder::new(b"Version: 1\n");
|
||||
|
||||
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(
|
||||
ContentType::Multipart {
|
||||
boundary: boundary.into_bytes(),
|
||||
kind: MultipartType::Encrypted,
|
||||
parts: parts.into_iter().map(|a| a.into()).collect::<Vec<_>>(),
|
||||
parameters: vec![],
|
||||
},
|
||||
Default::default(),
|
||||
ctx.encrypt(encrypt_keys, data)?.await?,
|
||||
);
|
||||
a.content_disposition =
|
||||
ContentDisposition::from(br#"attachment; filename="msg.asc""#);
|
||||
a
|
||||
};
|
||||
let mut a: AttachmentBuilder = AttachmentBuilder::new(b"Version: 1\n");
|
||||
|
||||
a.set_content_type_from_bytes(b"application/pgp-encrypted");
|
||||
a.set_content_disposition(ContentDisposition::from(b"attachment"));
|
||||
let parts = vec![a, enc_attachment.into()];
|
||||
let boundary = ContentType::make_boundary(&parts);
|
||||
Ok(Attachment::new(
|
||||
ContentType::Multipart {
|
||||
boundary: boundary.into_bytes(),
|
||||
kind: MultipartType::Encrypted,
|
||||
parts: parts.into_iter().map(|a| a.into()).collect::<Vec<_>>(),
|
||||
parameters: vec![],
|
||||
},
|
||||
Default::default(),
|
||||
vec![],
|
||||
)
|
||||
.into())
|
||||
})
|
||||
})
|
||||
vec![],
|
||||
)
|
||||
.into())
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -44,7 +44,15 @@ impl std::fmt::Display for AccountStatus {
|
|||
|
||||
impl AccountStatus {
|
||||
pub fn new(account_pos: usize, theme_default: ThemeAttribute) -> Self {
|
||||
let mut content = Screen::<Virtual>::new(theme_default);
|
||||
let default_cell = {
|
||||
let mut ret = Cell::with_char(' ');
|
||||
ret.set_fg(theme_default.fg)
|
||||
.set_bg(theme_default.bg)
|
||||
.set_attrs(theme_default.attrs);
|
||||
ret
|
||||
};
|
||||
let mut content = Screen::<Virtual>::new();
|
||||
content.grid_mut().default_cell = default_cell;
|
||||
content.grid_mut().set_growable(true);
|
||||
_ = content.resize(80, 20);
|
||||
|
||||
|
@ -71,7 +79,6 @@ impl AccountStatus {
|
|||
self.theme_default.attrs | Attr::UNDERLINE,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let area = self.content.area().skip(_x + 1, _y);
|
||||
let (_x, _y) = self.content.grid_mut().write_string(
|
||||
|
@ -81,7 +88,6 @@ impl AccountStatus {
|
|||
Attr::BOLD | Attr::UNDERLINE,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let mut line = 2;
|
||||
|
||||
|
@ -93,61 +99,51 @@ impl AccountStatus {
|
|||
Attr::BOLD,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
line += 2;
|
||||
|
||||
let mut total = a.active_jobs.len();
|
||||
if let Some((job_id, req)) = a
|
||||
.active_jobs
|
||||
.iter()
|
||||
.find(|(_, req)| matches!(req, JobRequest::Watch { .. }))
|
||||
{
|
||||
total -= 1;
|
||||
for (job_id, req) in a.active_jobs.iter() {
|
||||
let area = self.content.area().skip(1, line);
|
||||
self.content.grid_mut().write_string(
|
||||
let (x, y) = self.content.grid_mut().write_string(
|
||||
&format!("{} {}", req, job_id),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if let JobRequest::DeleteMailbox { mailbox_hash, .. }
|
||||
| JobRequest::SetMailboxPermissions { mailbox_hash, .. }
|
||||
| JobRequest::SetMailboxSubscription { mailbox_hash, .. }
|
||||
| JobRequest::Refresh { mailbox_hash, .. }
|
||||
| JobRequest::Fetch { mailbox_hash, .. } = req
|
||||
{
|
||||
let area = self.content.area().skip(x + 1, y + line);
|
||||
self.content.grid_mut().write_string(
|
||||
a.mailbox_entries[mailbox_hash].name(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
line += 1;
|
||||
}
|
||||
|
||||
if a.active_jobs.is_empty() || total != 0 {
|
||||
let area = self.content.area().skip(1, line);
|
||||
self.content.grid_mut().write_string(
|
||||
&if a.active_jobs.is_empty() && total == 0 {
|
||||
Cow::Borrowed("None.")
|
||||
} else if total == a.active_jobs.len() {
|
||||
Cow::Owned(format!("{} tasks", total))
|
||||
} else {
|
||||
Cow::Owned(format!("and other {} tasks", total))
|
||||
},
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
line += 1;
|
||||
}
|
||||
line += 2;
|
||||
|
||||
let area = self.content.area().skip(1, line);
|
||||
let (_x, _y) = self.content.grid_mut().write_string(
|
||||
"Tag support: ",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs | Attr::BOLD,
|
||||
Attr::BOLD,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let area = self.content.area().skip(_x + 1, line);
|
||||
let area = self.content.area().skip(_x + 1, _y + line);
|
||||
self.content.grid_mut().write_string(
|
||||
if a.backend_capabilities.supports_tags {
|
||||
"yes"
|
||||
|
@ -159,31 +155,6 @@ impl AccountStatus {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
line += 1;
|
||||
let area = self.content.area().skip(1, line);
|
||||
let (_x, _) = self.content.grid_mut().write_string(
|
||||
"Metadata: ",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs | Attr::BOLD,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
self.content.grid_mut().write_string(
|
||||
&a.backend_capabilities
|
||||
.metadata
|
||||
.as_ref()
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area.skip_cols(_x),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
line += 1;
|
||||
let area = self.content.area().skip(1, line);
|
||||
|
@ -194,7 +165,6 @@ impl AccountStatus {
|
|||
Attr::BOLD,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let area = self.content.area().skip(_x + 1, _y + line);
|
||||
self.content.grid_mut().write_string(
|
||||
|
@ -224,7 +194,6 @@ impl AccountStatus {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
line += 1;
|
||||
|
||||
|
@ -236,7 +205,6 @@ impl AccountStatus {
|
|||
Attr::BOLD,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for f in a
|
||||
.mailbox_entries
|
||||
|
@ -253,10 +221,36 @@ impl AccountStatus {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
line += 2;
|
||||
let area = self.content.area().skip(1, line);
|
||||
self.content.grid_mut().write_string(
|
||||
"Subscribed mailboxes:",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
Attr::BOLD,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
line += 2;
|
||||
for mailbox_node in a.list_mailboxes() {
|
||||
let f: &Mailbox = &a[&mailbox_node.hash].ref_mailbox;
|
||||
if f.is_subscribed() {
|
||||
let area = self.content.area().skip(1, line);
|
||||
self.content.grid_mut().write_string(
|
||||
f.path(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
line += 1;
|
||||
}
|
||||
}
|
||||
|
||||
line += 1;
|
||||
if let Some(ref extensions) = a.backend_capabilities.extensions {
|
||||
let area = self.content.area().skip(1, line);
|
||||
self.content.grid_mut().write_string(
|
||||
|
@ -266,7 +260,6 @@ impl AccountStatus {
|
|||
Attr::BOLD,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let max_name_width = std::cmp::max(
|
||||
"Server Extensions:".len(),
|
||||
|
@ -284,12 +277,9 @@ impl AccountStatus {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
line += 1;
|
||||
for (name, status) in extensions.iter() {
|
||||
use MailBackendExtensionStatus as Ext;
|
||||
|
||||
let area = self.content.area().skip(1, line);
|
||||
self.content.grid_mut().write_string(
|
||||
name.trim_at_boundary(30),
|
||||
|
@ -298,14 +288,19 @@ impl AccountStatus {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let (x, y) = {
|
||||
let (status, color) = match status {
|
||||
Ext::Unsupported { comment: _ } => ("not supported", Color::Red),
|
||||
Ext::Supported { comment: _ } => ("supported", Color::Green),
|
||||
Ext::Enabled { comment: _ } => ("enabled", Color::Green),
|
||||
MailBackendExtensionStatus::Unsupported { comment: _ } => {
|
||||
("not supported", Color::Red)
|
||||
}
|
||||
MailBackendExtensionStatus::Supported { comment: _ } => {
|
||||
("supported", Color::Green)
|
||||
}
|
||||
MailBackendExtensionStatus::Enabled { comment: _ } => {
|
||||
("enabled", Color::Green)
|
||||
}
|
||||
};
|
||||
let area = self.content.area().skip(max_name_width + 6, line);
|
||||
self.content.grid_mut().write_string(
|
||||
|
@ -315,58 +310,56 @@ impl AccountStatus {
|
|||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
if let Ext::Unsupported { comment: Some(s) }
|
||||
| Ext::Supported { comment: Some(s) }
|
||||
| Ext::Enabled { comment: Some(s) } = status
|
||||
{
|
||||
let area = self
|
||||
.content
|
||||
.area()
|
||||
.skip(max_name_width + 6, line)
|
||||
.skip(x, y);
|
||||
let (_x, _y) = self.content.grid_mut().write_string(
|
||||
" (",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let area = self
|
||||
.content
|
||||
.area()
|
||||
.skip(max_name_width + 6, line)
|
||||
.skip(x, y)
|
||||
.skip(_x, _y);
|
||||
let (__x, __y) = self.content.grid_mut().write_string(
|
||||
s,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let area = self
|
||||
.content
|
||||
.area()
|
||||
.skip(max_name_width + 6, line)
|
||||
.skip(x + _x + __x, y + _y + __y);
|
||||
self.content.grid_mut().write_string(
|
||||
")",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
match status {
|
||||
MailBackendExtensionStatus::Unsupported { comment }
|
||||
| MailBackendExtensionStatus::Supported { comment }
|
||||
| MailBackendExtensionStatus::Enabled { comment } => {
|
||||
if let Some(s) = comment {
|
||||
let area = self
|
||||
.content
|
||||
.area()
|
||||
.skip(max_name_width + 6, line)
|
||||
.skip(x, y);
|
||||
let (_x, _y) = self.content.grid_mut().write_string(
|
||||
" (",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
let area = self
|
||||
.content
|
||||
.area()
|
||||
.skip(max_name_width + 6, line)
|
||||
.skip(x, y)
|
||||
.skip(_x, _y);
|
||||
let (__x, __y) = self.content.grid_mut().write_string(
|
||||
s,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
let area = self
|
||||
.content
|
||||
.area()
|
||||
.skip(max_name_width + 6, line)
|
||||
.skip(x + _x + __x, y + _y + __y);
|
||||
self.content.grid_mut().write_string(
|
||||
")",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
line += 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,11 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
convert::TryFrom,
|
||||
fmt::Write as _,
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use melib::{
|
||||
email::attachment_types::ContentType, list_management, mailto::Mailto, parser::BytesExt,
|
||||
utils::datetime, Card, Draft, FlagOp, HeaderName, SpecialUsageMailbox,
|
||||
|
@ -125,14 +125,13 @@ impl MailView {
|
|||
{
|
||||
match account
|
||||
.operation(coordinates.2)
|
||||
.and_then(|op| op.as_bytes())
|
||||
.and_then(|mut op| op.as_bytes())
|
||||
{
|
||||
Ok(fut) => {
|
||||
let mut handle = account.main_loop_handler.job_executor.spawn(
|
||||
"fetch-envelope".into(),
|
||||
fut,
|
||||
account.is_async(),
|
||||
);
|
||||
let mut handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("fetch_envelopes".into(), fut);
|
||||
let job_id = handle.job_id;
|
||||
pending_action = if let MailViewState::Init {
|
||||
ref mut pending_action,
|
||||
|
@ -153,7 +152,6 @@ impl MailView {
|
|||
}
|
||||
} else {
|
||||
self.state = MailViewState::LoadingBody {
|
||||
main_loop_handler: self.main_loop_handler.clone(),
|
||||
handle,
|
||||
pending_action: pending_action.take(),
|
||||
};
|
||||
|
@ -198,27 +196,25 @@ impl MailView {
|
|||
ref env,
|
||||
ref env_view,
|
||||
..
|
||||
} => (bytes, env_view.body_text(), env),
|
||||
} => (
|
||||
bytes,
|
||||
EnvelopeView::attachment_displays_to_text(&env_view.display, false),
|
||||
env,
|
||||
),
|
||||
MailViewState::Error { .. } => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let composer = match action {
|
||||
PendingReplyAction::Reply => Box::new(Composer::reply_to_select(
|
||||
coordinates,
|
||||
reply_body.to_string(),
|
||||
context,
|
||||
)),
|
||||
PendingReplyAction::ReplyToAuthor => Box::new(Composer::reply_to_author(
|
||||
coordinates,
|
||||
reply_body.to_string(),
|
||||
context,
|
||||
)),
|
||||
PendingReplyAction::ReplyToAll => Box::new(Composer::reply_to_all(
|
||||
coordinates,
|
||||
reply_body.to_string(),
|
||||
context,
|
||||
)),
|
||||
PendingReplyAction::Reply => {
|
||||
Box::new(Composer::reply_to_select(coordinates, reply_body, context))
|
||||
}
|
||||
PendingReplyAction::ReplyToAuthor => {
|
||||
Box::new(Composer::reply_to_author(coordinates, reply_body, context))
|
||||
}
|
||||
PendingReplyAction::ReplyToAll => {
|
||||
Box::new(Composer::reply_to_all(coordinates, reply_body, context))
|
||||
}
|
||||
PendingReplyAction::ForwardAttachment => {
|
||||
Box::new(Composer::forward(coordinates, bytes, env, true, context))
|
||||
}
|
||||
|
@ -262,28 +258,10 @@ impl MailView {
|
|||
)));
|
||||
return;
|
||||
}
|
||||
// First retrieve user's identities, and remove them from the final address
|
||||
// list.
|
||||
let mut seen = {
|
||||
let extra = account.settings.account().extra_identity_addresses();
|
||||
let mut ret = IndexSet::with_capacity(extra.len() + 1);
|
||||
ret.extend(extra);
|
||||
ret.insert(account.settings.account().main_identity_address());
|
||||
ret
|
||||
};
|
||||
let envelope: EnvelopeRef = account.collection.get_env(coordinates.2);
|
||||
|
||||
let mut entries: IndexMap<Card, (Card, String)> = IndexMap::default();
|
||||
for addr in envelope
|
||||
.from()
|
||||
.iter()
|
||||
.chain(envelope.to().iter())
|
||||
.chain(envelope.cc().iter())
|
||||
{
|
||||
if seen.contains(addr) {
|
||||
continue;
|
||||
}
|
||||
seen.insert(addr.clone());
|
||||
for addr in envelope.from().iter().chain(envelope.to().iter()) {
|
||||
let mut new_card: Card = Card::new();
|
||||
new_card
|
||||
.set_email(addr.get_email())
|
||||
|
@ -360,9 +338,7 @@ impl Component for MailView {
|
|||
kind: Some(NotificationType::Error(err.kind)),
|
||||
});
|
||||
log::error!("Failed to open envelope: {err}");
|
||||
if err.is_recoverable() {
|
||||
self.init_futures(context);
|
||||
}
|
||||
self.init_futures(context);
|
||||
return;
|
||||
} else {
|
||||
grid.clear_area(area, self.theme_default);
|
||||
|
@ -414,7 +390,7 @@ impl Component for MailView {
|
|||
let account = &mut context.accounts[&coordinates.0];
|
||||
{
|
||||
for card in results.iter() {
|
||||
account.contacts.add_card(card.clone());
|
||||
account.address_book.add_card(card.clone());
|
||||
}
|
||||
}
|
||||
self.contact_selector = None;
|
||||
|
@ -439,9 +415,10 @@ impl Component for MailView {
|
|||
if self.active_jobs.contains(job_id) =>
|
||||
{
|
||||
match self.state {
|
||||
MailViewState::LoadingBody { ref mut handle, .. }
|
||||
if handle.job_id == *job_id =>
|
||||
{
|
||||
MailViewState::LoadingBody {
|
||||
ref mut handle,
|
||||
pending_action: _,
|
||||
} if handle.job_id == *job_id => {
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => { /* Job was canceled */ }
|
||||
Ok(None) => { /* something happened, perhaps a worker
|
||||
|
@ -556,11 +533,20 @@ impl Component for MailView {
|
|||
let _ = sender.send(operation?.as_bytes()?.await);
|
||||
Ok(())
|
||||
};
|
||||
let handle = context.main_loop_handler.job_executor.spawn(
|
||||
"fetch-envelope".into(),
|
||||
bytes_job,
|
||||
context.accounts[&account_hash].is_async(),
|
||||
);
|
||||
let handle = if context.accounts[&account_hash]
|
||||
.backend_capabilities
|
||||
.is_async
|
||||
{
|
||||
context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("fetch_envelope".into(), bytes_job)
|
||||
} else {
|
||||
context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_blocking("fetch_envelope".into(), bytes_job)
|
||||
};
|
||||
context.accounts[&account_hash].insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
|
@ -583,14 +569,15 @@ impl Component for MailView {
|
|||
}
|
||||
Err(err) => {
|
||||
let err_string = format!(
|
||||
"Failed to open envelope {:?}: {}",
|
||||
"Failed to open envelope {}: {}",
|
||||
context.accounts[&account_hash]
|
||||
.collection
|
||||
.envelopes
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&env_hash)
|
||||
.map(|env| env.message_id()),
|
||||
.map(|env| env.message_id_display())
|
||||
.unwrap_or_else(|| "Not found".into()),
|
||||
err
|
||||
);
|
||||
log::error!("{err_string}");
|
||||
|
@ -623,7 +610,7 @@ impl Component for MailView {
|
|||
self.start_contact_selector(context);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('\x1b'))
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
||||
if self.contact_selector.is_some() || self.forward_dialog.is_some() =>
|
||||
{
|
||||
if let Some(s) = self.contact_selector.take() {
|
||||
|
@ -689,7 +676,7 @@ impl Component for MailView {
|
|||
context.accounts[&coordinates.0]
|
||||
.settings
|
||||
.account()
|
||||
.main_identity_address()
|
||||
.make_display_name()
|
||||
.to_string(),
|
||||
);
|
||||
/* Manually drop stuff because borrowck doesn't do it
|
||||
|
|
|
@ -19,16 +19,11 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use melib::utils::{shellexpand::ShellExpandTrait, xdg::query_default_app};
|
||||
use melib::utils::xdg::query_default_app;
|
||||
|
||||
use super::*;
|
||||
#[cfg(feature = "gpgme")]
|
||||
use crate::jobs::IsAsync;
|
||||
use crate::ThreadEvent;
|
||||
|
||||
/// Envelope view, with sticky headers, a pager for the body, and
|
||||
|
@ -248,7 +243,7 @@ impl EnvelopeView {
|
|||
inner: Box::new(a.clone()),
|
||||
display: {
|
||||
let mut v = vec![];
|
||||
Self::attachment_to_display_helper(
|
||||
EnvelopeView::attachment_to_display_helper(
|
||||
&parts[0],
|
||||
main_loop_handler,
|
||||
active_jobs,
|
||||
|
@ -264,11 +259,9 @@ impl EnvelopeView {
|
|||
{
|
||||
if view_settings.auto_verify_signatures.is_true() {
|
||||
let verify_fut = crate::mail::pgp::verify(a.clone());
|
||||
let handle = main_loop_handler.job_executor.spawn(
|
||||
"gpg::verify".into(),
|
||||
verify_fut,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let handle = main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("gpg::verify_sig".into(), verify_fut);
|
||||
active_jobs.insert(handle.job_id);
|
||||
main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
|
||||
StatusEvent::NewJob(handle.job_id),
|
||||
|
@ -326,11 +319,9 @@ impl EnvelopeView {
|
|||
{
|
||||
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.spawn(
|
||||
"gpg::decrypt".into(),
|
||||
decrypt_fut,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let handle = main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("gpg::decrypt".into(), decrypt_fut);
|
||||
active_jobs.insert(handle.job_id);
|
||||
main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::StatusEvent(StatusEvent::NewJob(handle.job_id)),
|
||||
|
@ -379,6 +370,125 @@ impl EnvelopeView {
|
|||
self.display = display;
|
||||
self.attachment_tree = attachment_tree;
|
||||
self.attachment_paths = attachment_paths;
|
||||
self.regenerate_body_text();
|
||||
}
|
||||
|
||||
pub fn regenerate_body_text(&mut self) {
|
||||
self.body_text = Self::attachment_displays_to_text(&self.display, true);
|
||||
}
|
||||
|
||||
pub fn attachment_displays_to_text(
|
||||
displays: &[AttachmentDisplay],
|
||||
show_comments: bool,
|
||||
) -> String {
|
||||
let mut acc = String::new();
|
||||
for d in displays {
|
||||
use AttachmentDisplay::*;
|
||||
match d {
|
||||
Alternative {
|
||||
inner: _,
|
||||
shown_display,
|
||||
display,
|
||||
} => {
|
||||
acc.push_str(&Self::attachment_displays_to_text(
|
||||
&display[*shown_display..(*shown_display + 1)],
|
||||
show_comments,
|
||||
));
|
||||
}
|
||||
InlineText {
|
||||
inner: _,
|
||||
text,
|
||||
comment: Some(comment),
|
||||
} if show_comments => {
|
||||
acc.push_str(comment);
|
||||
if !acc.ends_with("\n\n") {
|
||||
acc.push_str("\n\n");
|
||||
}
|
||||
acc.push_str(text);
|
||||
}
|
||||
InlineText {
|
||||
inner: _,
|
||||
text,
|
||||
comment: _,
|
||||
} => acc.push_str(text),
|
||||
InlineOther { inner } => {
|
||||
if !acc.ends_with("\n\n") {
|
||||
acc.push_str("\n\n");
|
||||
}
|
||||
acc.push_str(&inner.to_string());
|
||||
if !acc.ends_with("\n\n") {
|
||||
acc.push_str("\n\n");
|
||||
}
|
||||
}
|
||||
Attachment { inner: _ } => {}
|
||||
SignedPending {
|
||||
inner: _,
|
||||
display,
|
||||
handle: _,
|
||||
job_id: _,
|
||||
} => {
|
||||
if show_comments {
|
||||
acc.push_str("Waiting for signature verification.\n\n");
|
||||
}
|
||||
acc.push_str(&Self::attachment_displays_to_text(display, show_comments));
|
||||
}
|
||||
SignedUnverified { inner: _, display } => {
|
||||
if show_comments {
|
||||
acc.push_str("Unverified signature.\n\n");
|
||||
}
|
||||
acc.push_str(&Self::attachment_displays_to_text(display, show_comments))
|
||||
}
|
||||
SignedFailed {
|
||||
inner: _,
|
||||
display,
|
||||
error,
|
||||
} => {
|
||||
if show_comments {
|
||||
let _ = writeln!(acc, "Failed to verify signature: {}.\n", error);
|
||||
}
|
||||
acc.push_str(&Self::attachment_displays_to_text(display, show_comments));
|
||||
}
|
||||
SignedVerified {
|
||||
inner: _,
|
||||
display,
|
||||
description,
|
||||
} => {
|
||||
if show_comments {
|
||||
if description.is_empty() {
|
||||
acc.push_str("Verified signature.\n\n");
|
||||
} else {
|
||||
acc.push_str(description);
|
||||
acc.push_str("\n\n");
|
||||
}
|
||||
}
|
||||
acc.push_str(&Self::attachment_displays_to_text(display, show_comments));
|
||||
}
|
||||
EncryptedPending { .. } => acc.push_str("Waiting for decryption result."),
|
||||
EncryptedFailed { inner: _, error } => {
|
||||
let _ = write!(acc, "Decryption failed: {}.", &error);
|
||||
}
|
||||
EncryptedSuccess {
|
||||
inner: _,
|
||||
plaintext: _,
|
||||
plaintext_display,
|
||||
description,
|
||||
} => {
|
||||
if show_comments {
|
||||
if description.is_empty() {
|
||||
acc.push_str("Successfully decrypted.\n\n");
|
||||
} else {
|
||||
acc.push_str(description);
|
||||
acc.push_str("\n\n");
|
||||
}
|
||||
}
|
||||
acc.push_str(&Self::attachment_displays_to_text(
|
||||
plaintext_display,
|
||||
show_comments,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
fn attachment_displays_to_tree(
|
||||
|
@ -601,10 +711,6 @@ impl EnvelopeView {
|
|||
))));
|
||||
None
|
||||
}
|
||||
|
||||
pub fn body_text(&self) -> &str {
|
||||
&self.body_text
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for EnvelopeView {
|
||||
|
@ -639,38 +745,27 @@ impl Component for EnvelopeView {
|
|||
if sticky || skip_header_ctr == 0 {
|
||||
if y <= area.height() {
|
||||
grid.clear_area(
|
||||
area.skip_rows(y).take_rows(1),
|
||||
area.skip_rows(y),
|
||||
hdr_area_theme,
|
||||
);
|
||||
let (_x, _y) =
|
||||
grid.write_string(
|
||||
&format!("{}:", $header),
|
||||
hdr_name_theme.fg,
|
||||
hdr_name_theme.bg,
|
||||
hdr_name_theme.attrs,
|
||||
area.skip_rows(y),
|
||||
None,
|
||||
Some(0)
|
||||
);
|
||||
let (__x, mut __y) =
|
||||
&format!("{}:", $header),
|
||||
hdr_name_theme.fg,
|
||||
hdr_name_theme.bg,
|
||||
hdr_name_theme.attrs,
|
||||
area.skip_rows(y),
|
||||
Some(0),
|
||||
);
|
||||
let (__x, __y) =
|
||||
grid.write_string(
|
||||
&$string,
|
||||
hdr_theme.fg,
|
||||
hdr_theme.bg,
|
||||
hdr_theme.attrs,
|
||||
area.skip_rows(y + _y),
|
||||
Some(_x + 1),
|
||||
Some(2)
|
||||
);
|
||||
if __y > 0 {
|
||||
if __y > 3 && !self.view_settings.expand_headers {
|
||||
__y = 3;
|
||||
}
|
||||
grid.clear_area(
|
||||
area.skip_rows(y + _y + 1).take_rows(__y).take_cols(2),
|
||||
hdr_area_theme,
|
||||
);
|
||||
}
|
||||
&$string,
|
||||
hdr_theme.fg,
|
||||
hdr_theme.bg,
|
||||
hdr_theme.attrs,
|
||||
area.skip(_x+1, y+ _y),
|
||||
Some(0),
|
||||
);
|
||||
y += _y +__y + 1;
|
||||
}
|
||||
} else {
|
||||
|
@ -685,10 +780,9 @@ impl Component for EnvelopeView {
|
|||
if let Some(pos) = s.as_bytes().iter().position(|b| *b == b'+' || *b == b'-') {
|
||||
let offset = &s[pos..];
|
||||
diff.0 = offset.starts_with('+');
|
||||
if let (Some(hr_offset), Some(min_offset)) = (
|
||||
offset.get(1..3).and_then(|slice| slice.parse::<i64>().ok()),
|
||||
offset.get(3..5).and_then(|slice| slice.parse::<i64>().ok()),
|
||||
) {
|
||||
if let (Ok(hr_offset), Ok(min_offset)) =
|
||||
(offset[1..3].parse::<i64>(), offset[3..5].parse::<i64>())
|
||||
{
|
||||
diff.1 .0 = hr_offset;
|
||||
diff.1 .1 = min_offset;
|
||||
}
|
||||
|
@ -734,19 +828,21 @@ impl Component for EnvelopeView {
|
|||
(HeaderName::SUBJECT, envelope.subject()),
|
||||
(
|
||||
HeaderName::MESSAGE_ID,
|
||||
envelope.message_id().display_brackets().to_string()
|
||||
format!("<{}>", envelope.message_id_raw())
|
||||
)
|
||||
);
|
||||
if self.view_settings.expand_headers {
|
||||
if let Some(val) = envelope.in_reply_to() {
|
||||
if let Some(val) = envelope.in_reply_to_display() {
|
||||
print_header!(
|
||||
(
|
||||
HeaderName::IN_REPLY_TO,
|
||||
melib::MessageID::display_slice(val.refs(), Some(" "))
|
||||
),
|
||||
(HeaderName::IN_REPLY_TO, val),
|
||||
(
|
||||
HeaderName::REFERENCES,
|
||||
melib::MessageID::display_slice(envelope.references(), Some(" "))
|
||||
envelope
|
||||
.references()
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -770,34 +866,26 @@ impl Component for EnvelopeView {
|
|||
if let Some(id) = id {
|
||||
if sticky || skip_header_ctr == 0 {
|
||||
grid.clear_area(area.nth_row(y), hdr_area_theme);
|
||||
let (_x, _y) = grid.write_string(
|
||||
let (_x, _) = grid.write_string(
|
||||
"List-ID: ",
|
||||
hdr_name_theme.fg,
|
||||
hdr_name_theme.bg,
|
||||
hdr_name_theme.attrs,
|
||||
area.nth_row(y),
|
||||
None,
|
||||
Some(0),
|
||||
);
|
||||
if _y != 0 {
|
||||
x = _x;
|
||||
} else {
|
||||
x += _x;
|
||||
}
|
||||
y += _y;
|
||||
x = _x;
|
||||
let (_x, _y) = grid.write_string(
|
||||
id,
|
||||
hdr_theme.fg,
|
||||
hdr_theme.bg,
|
||||
hdr_theme.attrs,
|
||||
area.nth_row(y),
|
||||
Some(x),
|
||||
Some(0),
|
||||
area.nth_row(y).skip_cols(_x),
|
||||
None,
|
||||
);
|
||||
x += _x;
|
||||
if _y != 0 {
|
||||
x = _x;
|
||||
} else {
|
||||
x += _x;
|
||||
x = 0;
|
||||
}
|
||||
y += _y;
|
||||
}
|
||||
|
@ -810,15 +898,10 @@ impl Component for EnvelopeView {
|
|||
hdr_name_theme.fg,
|
||||
hdr_name_theme.bg,
|
||||
hdr_name_theme.attrs,
|
||||
area.skip(0, y),
|
||||
Some(x),
|
||||
area.skip(x, y),
|
||||
Some(0),
|
||||
);
|
||||
if _y != 0 {
|
||||
x = _x;
|
||||
} else {
|
||||
x += _x;
|
||||
}
|
||||
x += _x;
|
||||
y += _y;
|
||||
}
|
||||
if archive.is_some() {
|
||||
|
@ -827,15 +910,10 @@ impl Component for EnvelopeView {
|
|||
hdr_theme.fg,
|
||||
hdr_theme.bg,
|
||||
hdr_theme.attrs,
|
||||
area.skip(0, y),
|
||||
Some(x),
|
||||
area.skip(x, y),
|
||||
Some(0),
|
||||
);
|
||||
if _y != 0 {
|
||||
x = _x;
|
||||
} else {
|
||||
x += _x;
|
||||
}
|
||||
x += _x;
|
||||
y += _y;
|
||||
}
|
||||
if post.is_some() {
|
||||
|
@ -844,15 +922,10 @@ impl Component for EnvelopeView {
|
|||
hdr_theme.fg,
|
||||
hdr_theme.bg,
|
||||
hdr_theme.attrs,
|
||||
area.skip(0, y),
|
||||
Some(x),
|
||||
area.skip(x, y),
|
||||
Some(0),
|
||||
);
|
||||
if _y != 0 {
|
||||
x = _x;
|
||||
} else {
|
||||
x += _x;
|
||||
}
|
||||
x += _x;
|
||||
y += _y;
|
||||
}
|
||||
if unsubscribe.is_some() {
|
||||
|
@ -861,15 +934,10 @@ impl Component for EnvelopeView {
|
|||
hdr_theme.fg,
|
||||
hdr_theme.bg,
|
||||
hdr_theme.attrs,
|
||||
area.skip(0, y),
|
||||
Some(x),
|
||||
area.skip(x, y),
|
||||
Some(0),
|
||||
);
|
||||
if _y != 0 {
|
||||
x = _x;
|
||||
} else {
|
||||
x += _x;
|
||||
}
|
||||
x += _x;
|
||||
y += _y;
|
||||
}
|
||||
if archive.is_some() || post.is_some() || unsubscribe.is_some() {
|
||||
|
@ -888,12 +956,19 @@ impl Component for EnvelopeView {
|
|||
}
|
||||
}
|
||||
}
|
||||
for c in grid.row_iter(area, (x + 1)..area.width(), y) {
|
||||
grid[c]
|
||||
.set_ch(' ')
|
||||
.set_fg(hdr_area_theme.fg)
|
||||
.set_bg(hdr_area_theme.bg);
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.force_draw_headers = false;
|
||||
grid.clear_area(area.skip_rows(y), self.view_settings.theme_default);
|
||||
|
||||
grid.clear_area(area.nth_row(y), hdr_area_theme);
|
||||
context.dirty_areas.push_back(area.take_rows(y + 3));
|
||||
if !self.view_settings.sticky_headers {
|
||||
let height_p = self.pager.size().1;
|
||||
|
@ -915,7 +990,7 @@ impl Component for EnvelopeView {
|
|||
}
|
||||
};
|
||||
|
||||
if self.filters.is_empty() {
|
||||
if self.filters.is_empty() || self.body_text.is_empty() {
|
||||
let body = self.mail.body();
|
||||
if body.is_html() {
|
||||
let attachment = if let Some(sub) = match body.content_type {
|
||||
|
@ -930,7 +1005,7 @@ impl Component for EnvelopeView {
|
|||
} else {
|
||||
&body
|
||||
};
|
||||
if let Ok(filter) = ViewFilter::new_html(attachment, &self.view_settings, context) {
|
||||
if let Ok(filter) = ViewFilter::new_html(attachment, context) {
|
||||
self.filters.push(filter);
|
||||
}
|
||||
} else if self.view_settings.auto_choose_multipart_alternative
|
||||
|
@ -952,71 +1027,47 @@ impl Component for EnvelopeView {
|
|||
.iter()
|
||||
.find(|a| a.is_html())
|
||||
.unwrap_or(&body),
|
||||
&self.view_settings,
|
||||
context,
|
||||
) {
|
||||
self.filters.push(filter);
|
||||
} else if let Ok(filter) =
|
||||
ViewFilter::new_attachment(&body, &self.view_settings, context)
|
||||
{
|
||||
self.filters.push(filter);
|
||||
}
|
||||
} else if let Ok(filter) =
|
||||
ViewFilter::new_attachment(&body, &self.view_settings, context)
|
||||
{
|
||||
} else if let Ok(filter) = ViewFilter::new_attachment(&body, context) {
|
||||
self.filters.push(filter);
|
||||
}
|
||||
self.body_text = String::from_utf8_lossy(
|
||||
&body.decode(Option::<Charset>::from(&self.force_charset).into()),
|
||||
)
|
||||
.to_string();
|
||||
}
|
||||
if !self.initialised {
|
||||
self.initialised = true;
|
||||
let mut text = if !self.filters.is_empty() {
|
||||
let mut text = String::new();
|
||||
self.body_text.clear();
|
||||
if let Some(last) = self.filters.last() {
|
||||
let mut stack = vec![last];
|
||||
while let Some(ViewFilter {
|
||||
filter_invocation,
|
||||
body_text,
|
||||
notice,
|
||||
..
|
||||
}) = stack.pop()
|
||||
{
|
||||
text.push_str(
|
||||
¬ice
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| {
|
||||
if filter_invocation.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!("Text filtered by `{filter_invocation}`"))
|
||||
}
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
if !text.is_empty() {
|
||||
text.push('\n');
|
||||
}
|
||||
if !self.body_text.is_empty() {
|
||||
self.body_text.push('\n');
|
||||
}
|
||||
match body_text {
|
||||
ViewFilterContent::Filtered { inner } => {
|
||||
let payload =
|
||||
self.options.convert(&mut self.links, &self.body, inner);
|
||||
text.push_str(&payload);
|
||||
self.body_text.push_str(&payload);
|
||||
}
|
||||
ViewFilterContent::Error { inner } => text.push_str(&inner.to_string()),
|
||||
ViewFilterContent::Running { .. } => {
|
||||
text.push_str("Filter job running in background.")
|
||||
}
|
||||
ViewFilterContent::InlineAttachments { parts } => {
|
||||
stack.extend(parts.iter().rev());
|
||||
}
|
||||
}
|
||||
let mut text = if let Some(ViewFilter {
|
||||
filter_invocation,
|
||||
body_text,
|
||||
notice,
|
||||
..
|
||||
}) = self.filters.last()
|
||||
{
|
||||
let mut text = if self.filters.len() == 1 {
|
||||
if filter_invocation.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("Text piped through `{filter_invocation}`\n\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notice
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| {
|
||||
if filter_invocation.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!("Text piped through `{filter_invocation}`\n\n"))
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
text.push_str(&self.options.convert(&mut self.links, &self.body, body_text));
|
||||
text
|
||||
} else {
|
||||
self.options
|
||||
|
@ -1026,9 +1077,6 @@ impl Component for EnvelopeView {
|
|||
text.push_str("\n\n");
|
||||
}
|
||||
text.push_str(&self.attachment_tree);
|
||||
while text.ends_with('\n') {
|
||||
text.pop();
|
||||
}
|
||||
let cursor_pos = self.pager.cursor_pos();
|
||||
self.view_settings.body_theme = crate::conf::value(context, "mail.view.body");
|
||||
self.pager = Pager::from_string(
|
||||
|
@ -1069,7 +1117,6 @@ impl Component for EnvelopeView {
|
|||
self.view_settings.theme_default.attrs,
|
||||
l.skip_cols_from_end(8),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1077,168 +1124,6 @@ impl Component for EnvelopeView {
|
|||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if matches!(event, UIEvent::StatusEvent(StatusEvent::JobFinished(_))) {
|
||||
match *event {
|
||||
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
|
||||
if self.active_jobs.contains(job_id) =>
|
||||
{
|
||||
let mut caught = false;
|
||||
for d in self.display.iter_mut() {
|
||||
let succeeded: bool;
|
||||
match d {
|
||||
AttachmentDisplay::SignedPending {
|
||||
ref mut inner,
|
||||
handle,
|
||||
display,
|
||||
job_id: our_job_id,
|
||||
} if *our_job_id == *job_id => {
|
||||
caught = true;
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => {
|
||||
/* Job was canceled */
|
||||
succeeded = false;
|
||||
log::warn!("Could not verify signature: Job was canceled",);
|
||||
}
|
||||
Ok(None) => {
|
||||
/* something happened,
|
||||
* perhaps a worker thread
|
||||
* panicked */
|
||||
succeeded = false;
|
||||
log::warn!(
|
||||
"Could not verify signature: check logs for any errors",
|
||||
);
|
||||
}
|
||||
Ok(Some(Ok(()))) => {
|
||||
succeeded = true;
|
||||
*d = AttachmentDisplay::SignedVerified {
|
||||
inner: std::mem::replace(
|
||||
inner,
|
||||
Box::new(AttachmentBuilder::new(&[]).build()),
|
||||
),
|
||||
display: std::mem::take(display),
|
||||
description: String::new(),
|
||||
};
|
||||
}
|
||||
Ok(Some(Err(error))) => {
|
||||
succeeded = false;
|
||||
log::error!("Could not verify signature: {}", error);
|
||||
*d = AttachmentDisplay::SignedFailed {
|
||||
inner: std::mem::replace(
|
||||
inner,
|
||||
Box::new(AttachmentBuilder::new(&[]).build()),
|
||||
),
|
||||
display: std::mem::take(display),
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
AttachmentDisplay::EncryptedPending {
|
||||
ref mut inner,
|
||||
handle,
|
||||
} if handle.job_id == *job_id => {
|
||||
caught = true;
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => {
|
||||
/* Job was canceled */
|
||||
|
||||
succeeded = false;
|
||||
log::warn!(
|
||||
"Could not decrypt encrypted message: Job was canceled",
|
||||
);
|
||||
}
|
||||
Ok(None) => {
|
||||
/* something happened,
|
||||
* perhaps a worker thread
|
||||
* panicked */
|
||||
succeeded = false;
|
||||
log::warn!(
|
||||
"Could not decrypt encrypted message: check logs for \
|
||||
any errors",
|
||||
);
|
||||
}
|
||||
Ok(Some(Ok((metadata, decrypted_bytes)))) => {
|
||||
succeeded = true;
|
||||
let plaintext = Box::new(
|
||||
AttachmentBuilder::new(&decrypted_bytes).build(),
|
||||
);
|
||||
let mut plaintext_display = vec![];
|
||||
Self::attachment_to_display_helper(
|
||||
&plaintext,
|
||||
&self.main_loop_handler,
|
||||
&mut self.active_jobs,
|
||||
&mut plaintext_display,
|
||||
&self.view_settings,
|
||||
(&self.force_charset).into(),
|
||||
);
|
||||
*d = AttachmentDisplay::EncryptedSuccess {
|
||||
inner: std::mem::replace(
|
||||
inner,
|
||||
Box::new(AttachmentBuilder::new(&[]).build()),
|
||||
),
|
||||
plaintext,
|
||||
plaintext_display,
|
||||
description: format!("{:?}", metadata),
|
||||
};
|
||||
}
|
||||
Ok(Some(Err(error))) => {
|
||||
succeeded = false;
|
||||
log::error!(
|
||||
"Could not decrypt encrypted message: {}",
|
||||
error
|
||||
);
|
||||
*d = AttachmentDisplay::EncryptedFailed {
|
||||
inner: std::mem::replace(
|
||||
inner,
|
||||
Box::new(AttachmentBuilder::new(&[]).build()),
|
||||
),
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.set_job_success(*job_id, succeeded);
|
||||
}
|
||||
if caught {
|
||||
self.links.clear();
|
||||
self.initialised = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
|
||||
self.active_jobs.remove(job_id);
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
|
||||
if self.filters.iter().any(|f| f.contains_job_id(*job_id)) =>
|
||||
{
|
||||
let mut stack = self.filters.iter_mut().collect::<VecDeque<&mut _>>();
|
||||
while let Some(filter) = stack.pop_front() {
|
||||
if let Some(cb) = filter.event_handler {
|
||||
if cb(
|
||||
filter,
|
||||
&mut UIEvent::StatusEvent(StatusEvent::JobFinished(*job_id)),
|
||||
context,
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let ViewFilterContent::InlineAttachments { ref mut parts, .. } =
|
||||
filter.body_text
|
||||
{
|
||||
stack.extend(parts.iter_mut());
|
||||
}
|
||||
}
|
||||
self.links.clear();
|
||||
self.initialised = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
match (&mut self.force_charset, &event) {
|
||||
(ForceCharset::Dialog(selector), UIEvent::FinishedUIDialog(id, results))
|
||||
if *id == selector.id() =>
|
||||
|
@ -1260,13 +1145,6 @@ impl Component for EnvelopeView {
|
|||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(ForceCharset::Dialog(selector), UIEvent::ComponentUnrealize(id))
|
||||
if *id == selector.id() =>
|
||||
{
|
||||
self.force_charset = ForceCharset::None;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(ForceCharset::Dialog(selector), _) => {
|
||||
if selector.process_event(event, context) {
|
||||
return true;
|
||||
|
@ -1328,9 +1206,7 @@ impl Component for EnvelopeView {
|
|||
UIEvent::Resize | UIEvent::VisibilityChange(true) => {
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('\x1B'))
|
||||
if !self.cmd_buf.is_empty() =>
|
||||
{
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => {
|
||||
self.cmd_buf.clear();
|
||||
context
|
||||
.replies
|
||||
|
@ -1408,10 +1284,10 @@ impl Component for EnvelopeView {
|
|||
}
|
||||
UIEvent::Action(View(ViewAction::ExportMail(ref path))) => {
|
||||
// Save entire message as eml
|
||||
let mut path = std::path::Path::new(path).to_path_buf().expand();
|
||||
let mut path = std::path::Path::new(path).to_path_buf();
|
||||
|
||||
if path.is_dir() {
|
||||
path.push(format!("{}.eml", self.mail.message_id()));
|
||||
path.push(format!("{}.eml", self.mail.message_id_raw()));
|
||||
}
|
||||
if path.is_relative() {
|
||||
path = context.current_dir().join(&path);
|
||||
|
@ -1442,7 +1318,7 @@ impl Component for EnvelopeView {
|
|||
return true;
|
||||
}
|
||||
UIEvent::Action(View(ViewAction::SaveAttachment(a_i, ref path))) => {
|
||||
let mut path = std::path::Path::new(path).to_path_buf().expand();
|
||||
let mut path = std::path::Path::new(path).to_path_buf();
|
||||
|
||||
if let Some(u) = self.open_attachment(a_i, context) {
|
||||
if path.is_dir() {
|
||||
|
@ -1482,7 +1358,7 @@ impl Component for EnvelopeView {
|
|||
} else if a_i == 0 {
|
||||
// Save entire message as eml
|
||||
if path.is_dir() {
|
||||
path.push(format!("{}.eml", self.mail.message_id()));
|
||||
path.push(format!("{}.eml", self.mail.message_id_raw()));
|
||||
}
|
||||
if path.is_relative() {
|
||||
path = context.current_dir().join(&path);
|
||||
|
@ -1521,68 +1397,6 @@ impl Component for EnvelopeView {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Action(View(ViewAction::PipeAttachment(a_i, ref bin, ref args))) => {
|
||||
use std::borrow::Cow;
|
||||
|
||||
let bytes =
|
||||
if let Some(u) = self.open_attachment(a_i, context) {
|
||||
Cow::Owned(u.decode(Default::default()))
|
||||
} else if a_i == 0 {
|
||||
Cow::Borrowed(&self.mail.bytes)
|
||||
} else {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!("Attachment `{}` not found.", a_i)),
|
||||
));
|
||||
return true;
|
||||
};
|
||||
// Kill input thread so that spawned command can be sole receiver of stdin
|
||||
{
|
||||
context.input_kill();
|
||||
}
|
||||
let pipe_command = format!("{} {}", bin, args.as_slice().join(" "));
|
||||
log::trace!("Executing: {}", &pipe_command);
|
||||
match Command::new(bin)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.map_err(Error::from)
|
||||
.and_then(|mut child| {
|
||||
let Some(mut stdin) = child.stdin.take() else {
|
||||
let _ = child.wait();
|
||||
return Err(Error::new(format!(
|
||||
"Could not open standard input of {bin}"
|
||||
))
|
||||
.set_kind(ErrorKind::External));
|
||||
};
|
||||
stdin.write_all(&bytes).chain_err_summary(|| {
|
||||
format!("Could not write to standard input of {bin}")
|
||||
})?;
|
||||
|
||||
Ok(child)
|
||||
}) {
|
||||
Ok(mut child) => {
|
||||
let _ = child.wait();
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("Failed to execute {}: {}", pipe_command, err).into(),
|
||||
),
|
||||
source: None,
|
||||
body: err.to_string().into(),
|
||||
kind: Some(NotificationType::Error(melib::error::ErrorKind::External)),
|
||||
});
|
||||
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
|
||||
context.restore_input();
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["open_attachment"])
|
||||
&& !self.cmd_buf.is_empty() =>
|
||||
|
@ -1617,9 +1431,7 @@ impl Component for EnvelopeView {
|
|||
| ContentType::Text { .. }
|
||||
| ContentType::PGPSignature
|
||||
| ContentType::CMSSignature => {
|
||||
if let Ok(filter) =
|
||||
ViewFilter::new_attachment(attachment, &self.view_settings, context)
|
||||
{
|
||||
if let Ok(filter) = ViewFilter::new_attachment(attachment, context) {
|
||||
self.filters.push(filter);
|
||||
}
|
||||
self.initialised = false;
|
||||
|
@ -1825,6 +1637,103 @@ impl Component for EnvelopeView {
|
|||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
|
||||
if self.active_jobs.contains(job_id) =>
|
||||
{
|
||||
let mut caught = false;
|
||||
for d in self.display.iter_mut() {
|
||||
match d {
|
||||
AttachmentDisplay::SignedPending {
|
||||
ref mut inner,
|
||||
handle,
|
||||
display,
|
||||
job_id: our_job_id,
|
||||
} if *our_job_id == *job_id => {
|
||||
caught = true;
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => { /* Job was canceled */ }
|
||||
Ok(None) => { /* something happened,
|
||||
* perhaps a worker thread
|
||||
* panicked */
|
||||
}
|
||||
Ok(Some(Ok(()))) => {
|
||||
*d = AttachmentDisplay::SignedVerified {
|
||||
inner: std::mem::replace(
|
||||
inner,
|
||||
Box::new(AttachmentBuilder::new(&[]).build()),
|
||||
),
|
||||
display: std::mem::take(display),
|
||||
description: String::new(),
|
||||
};
|
||||
}
|
||||
Ok(Some(Err(error))) => {
|
||||
*d = AttachmentDisplay::SignedFailed {
|
||||
inner: std::mem::replace(
|
||||
inner,
|
||||
Box::new(AttachmentBuilder::new(&[]).build()),
|
||||
),
|
||||
display: std::mem::take(display),
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
AttachmentDisplay::EncryptedPending {
|
||||
ref mut inner,
|
||||
handle,
|
||||
} if handle.job_id == *job_id => {
|
||||
caught = true;
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => { /* Job was canceled */ }
|
||||
Ok(None) => { /* something happened,
|
||||
* perhaps a worker thread
|
||||
* panicked */
|
||||
}
|
||||
Ok(Some(Ok((metadata, decrypted_bytes)))) => {
|
||||
let plaintext =
|
||||
Box::new(AttachmentBuilder::new(&decrypted_bytes).build());
|
||||
let mut plaintext_display = vec![];
|
||||
Self::attachment_to_display_helper(
|
||||
&plaintext,
|
||||
&self.main_loop_handler,
|
||||
&mut self.active_jobs,
|
||||
&mut plaintext_display,
|
||||
&self.view_settings,
|
||||
(&self.force_charset).into(),
|
||||
);
|
||||
*d = AttachmentDisplay::EncryptedSuccess {
|
||||
inner: std::mem::replace(
|
||||
inner,
|
||||
Box::new(AttachmentBuilder::new(&[]).build()),
|
||||
),
|
||||
plaintext,
|
||||
plaintext_display,
|
||||
description: format!("{:?}", metadata),
|
||||
};
|
||||
}
|
||||
Ok(Some(Err(error))) => {
|
||||
*d = AttachmentDisplay::EncryptedFailed {
|
||||
inner: std::mem::replace(
|
||||
inner,
|
||||
Box::new(AttachmentBuilder::new(&[]).build()),
|
||||
),
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if caught {
|
||||
self.links.clear();
|
||||
self.regenerate_body_text();
|
||||
self.initialised = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
|
||||
self.active_jobs.remove(job_id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
|
@ -1847,7 +1756,7 @@ impl Component for EnvelopeView {
|
|||
// our_map.remove("return_to_normal_view");
|
||||
//}
|
||||
if !self.options.contains(ViewOptions::URL) {
|
||||
our_map.shift_remove("go_to_url");
|
||||
our_map.remove("go_to_url");
|
||||
}
|
||||
map.insert(Shortcuts::ENVELOPE_VIEW, our_map);
|
||||
|
||||
|
|
|
@ -31,80 +31,25 @@ type ProcessEventFn = fn(&mut ViewFilter, &mut UIEvent, &mut Context) -> bool;
|
|||
use melib::{
|
||||
attachment_types::{ContentType, MultipartType, Text},
|
||||
error::*,
|
||||
log,
|
||||
parser::BytesExt,
|
||||
text::Truncate,
|
||||
utils::xdg::query_default_app,
|
||||
Attachment, AttachmentBuilder, Result,
|
||||
Attachment, Result,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
components::*,
|
||||
desktop_exec_to_command,
|
||||
jobs::{IsAsync, JobId, JoinHandle},
|
||||
mail::view::ViewSettings,
|
||||
terminal::{Area, CellBuffer},
|
||||
try_recv_timeout, Context, ErrorKind, File, StatusEvent, UIEvent,
|
||||
Context, ErrorKind, File, StatusEvent, UIEvent,
|
||||
};
|
||||
|
||||
type FilterResult = std::result::Result<(Attachment, Vec<u8>), (Error, Vec<u8>)>;
|
||||
type OnSuccessNoticeCb = Arc<dyn (Fn() -> Cow<'static, str>) + Send + Sync>;
|
||||
|
||||
pub enum ViewFilterContent {
|
||||
Running {
|
||||
job_id: JobId,
|
||||
on_success_notice_cb: OnSuccessNoticeCb,
|
||||
job_handle: JoinHandle<FilterResult>,
|
||||
view_settings: ViewSettings,
|
||||
},
|
||||
Error {
|
||||
inner: Error,
|
||||
},
|
||||
Filtered {
|
||||
inner: String,
|
||||
},
|
||||
InlineAttachments {
|
||||
parts: Vec<ViewFilter>,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ViewFilterContent {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use ViewFilterContent::*;
|
||||
match self {
|
||||
Running {
|
||||
ref job_id,
|
||||
on_success_notice_cb: _,
|
||||
job_handle: _,
|
||||
view_settings: _,
|
||||
} => fmt
|
||||
.debug_struct(stringify!(ViewFilterContent::Running))
|
||||
.field("job_id", &job_id)
|
||||
.finish(),
|
||||
Error { ref inner } => fmt
|
||||
.debug_struct(stringify!(ViewFilterContent::Error))
|
||||
.field("error", inner)
|
||||
.finish(),
|
||||
Filtered { ref inner } => fmt
|
||||
.debug_struct(stringify!(ViewFilterContent::Filtered))
|
||||
.field("body_text", &inner.trim_at_boundary(18))
|
||||
.field("body_text_len", &inner.len())
|
||||
.finish(),
|
||||
InlineAttachments { ref parts } => fmt
|
||||
.debug_struct(stringify!(ViewFilterContent::InlineAttachments))
|
||||
.field("parts_no", &parts.len())
|
||||
.field("parts", &parts)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ViewFilter {
|
||||
pub filter_invocation: String,
|
||||
pub content_type: ContentType,
|
||||
pub notice: Option<Cow<'static, str>>,
|
||||
pub body_text: ViewFilterContent,
|
||||
pub body_text: String,
|
||||
pub unfiltered: Vec<u8>,
|
||||
pub event_handler: Option<ProcessEventFn>,
|
||||
pub id: ComponentId,
|
||||
|
@ -112,11 +57,12 @@ pub struct ViewFilter {
|
|||
|
||||
impl std::fmt::Debug for ViewFilter {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct(melib::identify!(ViewFilter))
|
||||
fmt.debug_struct(stringify!(ViewFilter))
|
||||
.field("filter_invocation", &self.filter_invocation)
|
||||
.field("content_type", &self.content_type)
|
||||
.field("notice", &self.notice)
|
||||
.field("body_text", &self.body_text)
|
||||
.field("body_text", &self.body_text.trim_at_boundary(18))
|
||||
.field("body_text_len", &self.body_text.len())
|
||||
.field("event_handler", &self.event_handler.is_some())
|
||||
.field("id", &self.id)
|
||||
.finish()
|
||||
|
@ -130,11 +76,7 @@ impl std::fmt::Display for ViewFilter {
|
|||
}
|
||||
|
||||
impl ViewFilter {
|
||||
pub fn new_html(
|
||||
body: &Attachment,
|
||||
view_settings: &ViewSettings,
|
||||
context: &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)
|
||||
|
@ -229,110 +171,53 @@ impl ViewFilter {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
let settings = &context.settings;
|
||||
let (filter_invocation, cmd, args): (
|
||||
Cow<'static, str>,
|
||||
&'static str,
|
||||
SmallVec<[Cow<'static, str>; 8]>,
|
||||
) = if let Some(filter_invocation) = settings.pager.html_filter.as_ref() {
|
||||
(
|
||||
filter_invocation.to_string().into(),
|
||||
"sh",
|
||||
smallvec::smallvec!["-c".into(), filter_invocation.to_string().into()],
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"w3m -I utf-8 -T text/html".into(),
|
||||
"w3m",
|
||||
smallvec::smallvec!["-I".into(), "utf-8".into(), "-T".into(), "text/html".into()],
|
||||
)
|
||||
};
|
||||
let bytes: Vec<u8> = att.decode(Default::default());
|
||||
|
||||
let filter_invocation2 = filter_invocation.to_string();
|
||||
let bytes2 = bytes.clone();
|
||||
let job = async move {
|
||||
let filter_invocation = filter_invocation2;
|
||||
let bytes = bytes2;
|
||||
let borrowed_args = args
|
||||
.iter()
|
||||
.map(|a| a.as_ref())
|
||||
.collect::<SmallVec<[&str; 8]>>();
|
||||
match run(cmd, &borrowed_args, &bytes) {
|
||||
Err(err) => Err((
|
||||
Error::new(format!(
|
||||
let settings = &context.settings;
|
||||
if let Some(filter_invocation) = settings.pager.html_filter.as_ref() {
|
||||
match run("sh", &["-c", filter_invocation], &bytes) {
|
||||
Err(err) => {
|
||||
return Err(Error::new(format!(
|
||||
"Failed to start html filter process `{}`",
|
||||
filter_invocation,
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::External),
|
||||
bytes,
|
||||
)),
|
||||
.set_kind(ErrorKind::External));
|
||||
}
|
||||
Ok(body_text) => {
|
||||
let mut att = AttachmentBuilder::default();
|
||||
att.set_raw(body_text.into_bytes()).set_body_to_raw();
|
||||
Ok((att.build(), bytes))
|
||||
let notice =
|
||||
Some(format!("Text piped through `{}`.\n\n", filter_invocation).into());
|
||||
return Ok(Self {
|
||||
filter_invocation: filter_invocation.clone(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice,
|
||||
body_text,
|
||||
unfiltered: bytes,
|
||||
event_handler: Some(Self::html_process_event),
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
let filter_invocation2 = filter_invocation.to_string();
|
||||
let open_html_shortcut = settings.shortcuts.envelope_view.open_html.clone();
|
||||
let on_success_notice_cb = Arc::new(move || {
|
||||
format!(
|
||||
"Text piped through `{}` Press `{}` to open in web browser.",
|
||||
filter_invocation2, open_html_shortcut
|
||||
)
|
||||
.into()
|
||||
});
|
||||
let mut job_handle = context.main_loop_handler.job_executor.spawn(
|
||||
filter_invocation.to_string().into(),
|
||||
job,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let mut retval = Self {
|
||||
filter_invocation: filter_invocation.to_string(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
unfiltered: bytes,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
event_handler: Some(Self::job_process_event),
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
|
||||
retval.body_text = ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb: on_success_notice_cb.clone(),
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
};
|
||||
retval.event_handler = Some(Self::html_process_event);
|
||||
Self::process_job_result(
|
||||
&mut retval,
|
||||
Ok(Some(job_result)),
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
context,
|
||||
);
|
||||
return Ok(retval);
|
||||
}
|
||||
Ok(Self {
|
||||
body_text: ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb,
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
},
|
||||
..retval
|
||||
})
|
||||
if let Ok(body_text) = run("w3m", &["-I", "utf-8", "-T", "text/html"], &bytes) {
|
||||
return Ok(Self {
|
||||
filter_invocation: "w3m -I utf-8 -T text/html".into(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: Some("Text piped through `w3m -I utf-8 -T text/html`.\n\n".into()),
|
||||
body_text,
|
||||
unfiltered: bytes,
|
||||
event_handler: Some(Self::html_process_event),
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
}
|
||||
|
||||
Err(
|
||||
Error::new("Failed to find any application to use as html filter")
|
||||
.set_kind(ErrorKind::Configuration),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_attachment(
|
||||
att: &Attachment,
|
||||
view_settings: &ViewSettings,
|
||||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
pub fn new_attachment(att: &Attachment, context: &mut Context) -> Result<Self> {
|
||||
if matches!(
|
||||
att.content_type,
|
||||
ContentType::Other { .. } | ContentType::OctetStream { .. }
|
||||
|
@ -352,33 +237,28 @@ impl ViewFilter {
|
|||
if let Some(Ok(v)) = parts
|
||||
.iter()
|
||||
.find(|p| p.is_text() && !p.body().trim().is_empty())
|
||||
.map(|p| Self::new_attachment(p, view_settings, context))
|
||||
.map(|p| Self::new_attachment(p, context))
|
||||
{
|
||||
return Ok(v);
|
||||
}
|
||||
} else if let ContentType::Multipart {
|
||||
kind: MultipartType::Related | MultipartType::Mixed,
|
||||
kind: MultipartType::Related,
|
||||
ref parts,
|
||||
..
|
||||
} = att.content_type
|
||||
{
|
||||
return Ok(Self {
|
||||
filter_invocation: String::new(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::InlineAttachments {
|
||||
parts: parts
|
||||
.iter()
|
||||
.filter_map(|p| Self::new_attachment(p, view_settings, context).ok())
|
||||
.collect::<Vec<Self>>(),
|
||||
},
|
||||
unfiltered: att.decode(Default::default()),
|
||||
event_handler: None,
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
if let Some(v @ Ok(_)) = parts.iter().find_map(|p| {
|
||||
if let v @ Ok(_) = Self::new_attachment(p, context) {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
if att.is_html() {
|
||||
return Self::new_html(att, view_settings, context);
|
||||
return Self::new_html(att, context);
|
||||
}
|
||||
if matches!(
|
||||
att.content_type,
|
||||
|
@ -391,300 +271,42 @@ impl ViewFilter {
|
|||
filter_invocation: String::new(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
body_text: String::new(),
|
||||
unfiltered: vec![],
|
||||
event_handler: None,
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
} else if let ContentType::Multipart {
|
||||
kind: MultipartType::Signed,
|
||||
ref parts,
|
||||
ref boundary,
|
||||
ref parameters,
|
||||
} = att.content_type
|
||||
{
|
||||
if !view_settings.auto_verify_signatures.is_true() {
|
||||
let att = Attachment {
|
||||
content_type: ContentType::Multipart {
|
||||
kind: MultipartType::Mixed,
|
||||
parts: parts.clone(),
|
||||
parameters: parameters.clone(),
|
||||
boundary: boundary.clone(),
|
||||
},
|
||||
..att.clone()
|
||||
};
|
||||
return Ok(Self {
|
||||
notice: Some("Unverified signature.".into()),
|
||||
..Self::new_attachment(&att, view_settings, context)?
|
||||
});
|
||||
}
|
||||
#[cfg(not(feature = "gpgme"))]
|
||||
{
|
||||
let content = att.raw();
|
||||
let bytes = content.trim().to_vec();
|
||||
return Ok(Self {
|
||||
filter_invocation: String::new(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: Some(
|
||||
"Cannot verify signature: meli must be compiled with libgpgme support."
|
||||
.into(),
|
||||
),
|
||||
body_text: ViewFilterContent::InlineAttachments {
|
||||
parts: parts
|
||||
.iter()
|
||||
.filter_map(|p| Self::new_attachment(p, view_settings, context).ok())
|
||||
.collect::<Vec<Self>>(),
|
||||
},
|
||||
unfiltered: bytes,
|
||||
event_handler: None,
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
{
|
||||
for a in parts {
|
||||
if a.content_type == "application/pgp-signature" {
|
||||
let content = att.raw();
|
||||
let bytes = content.trim().to_vec();
|
||||
let verify_fut = {
|
||||
let a = Attachment {
|
||||
content_type: ContentType::Multipart {
|
||||
kind: MultipartType::Mixed,
|
||||
parts: parts.clone(),
|
||||
parameters: parameters.clone(),
|
||||
boundary: boundary.clone(),
|
||||
},
|
||||
..att.clone()
|
||||
};
|
||||
let att = att.clone();
|
||||
async move {
|
||||
crate::mail::pgp::verify(att)
|
||||
.await
|
||||
.map_err(|err| (err, bytes.clone()))
|
||||
.map(|_| (a, bytes))
|
||||
}
|
||||
};
|
||||
let mut job_handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::verify".into(),
|
||||
verify_fut,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let on_success_notice_cb = Arc::new(|| "Verified signature.".into());
|
||||
let mut retval = Self {
|
||||
filter_invocation: "gpg::verify".into(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
unfiltered: att.raw().to_vec(),
|
||||
event_handler: Some(Self::job_process_event),
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
|
||||
retval.body_text = ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb: on_success_notice_cb.clone(),
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
};
|
||||
retval.event_handler = None;
|
||||
Self::process_job_result(
|
||||
&mut retval,
|
||||
Ok(Some(job_result)),
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
context,
|
||||
);
|
||||
return Ok(retval);
|
||||
}
|
||||
return Ok(Self {
|
||||
body_text: ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb,
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
},
|
||||
..retval
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let ContentType::Multipart {
|
||||
kind: MultipartType::Encrypted,
|
||||
}
|
||||
if let ContentType::Multipart {
|
||||
kind: MultipartType::Mixed,
|
||||
ref parts,
|
||||
..
|
||||
} = att.content_type
|
||||
{
|
||||
#[cfg(not(feature = "gpgme"))]
|
||||
{
|
||||
let msg = "Cannot decrypt: meli must be compiled with libgpgme support.";
|
||||
if let Some(Ok(mut res)) =
|
||||
parts.iter().find_map(|part| {
|
||||
match Self::new_attachment(part, view_settings, context) {
|
||||
v @ Ok(_) => Some(v),
|
||||
Err(_) => None,
|
||||
}
|
||||
if let Some(Ok(res)) =
|
||||
parts
|
||||
.iter()
|
||||
.find_map(|part| match Self::new_attachment(part, context) {
|
||||
v @ Ok(_) => Some(v),
|
||||
Err(_) => None,
|
||||
})
|
||||
{
|
||||
match res.notice {
|
||||
Some(ref mut notice) => {
|
||||
let notice = std::mem::take(notice);
|
||||
let mut notice = notice.into_owned();
|
||||
notice.push_str("\n");
|
||||
notice.push_str(msg);
|
||||
|
||||
res.notice = Some(notice.into());
|
||||
}
|
||||
None => {
|
||||
res.notice = Some(msg.into());
|
||||
}
|
||||
}
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
{
|
||||
for a in parts {
|
||||
if a.content_type == "application/octet-stream" {
|
||||
let content = a.raw();
|
||||
let bytes = content.trim().to_vec();
|
||||
let decrypt_fut = async {
|
||||
let (_metadata, bytes) = crate::mail::pgp::decrypt(
|
||||
melib::email::pgp::convert_attachment_to_rfc_spec(&bytes),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| (err, bytes))?;
|
||||
Ok((AttachmentBuilder::new(&bytes).build(), bytes))
|
||||
};
|
||||
let mut job_handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::decrypt".into(),
|
||||
decrypt_fut,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let on_success_notice_cb = Arc::new(|| "Decrypted content.".into());
|
||||
let mut retval = Self {
|
||||
filter_invocation: "gpg::decrypt".into(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
unfiltered: a.raw().to_vec(),
|
||||
event_handler: Some(Self::job_process_event),
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
|
||||
retval.body_text = ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb: on_success_notice_cb.clone(),
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
};
|
||||
retval.event_handler = None;
|
||||
Self::process_job_result(
|
||||
&mut retval,
|
||||
Ok(Some(job_result)),
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
context,
|
||||
);
|
||||
return Ok(retval);
|
||||
}
|
||||
return Ok(Self {
|
||||
body_text: ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb,
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
},
|
||||
..retval
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
if let ContentType::Text {
|
||||
kind: Text::Plain, ..
|
||||
} = att.content_type
|
||||
{
|
||||
let content = att.text(Text::Plain);
|
||||
if content
|
||||
.trim_start()
|
||||
.starts_with("-----BEGIN PGP MESSAGE-----")
|
||||
&& content.trim_end().ends_with("-----END PGP MESSAGE-----")
|
||||
{
|
||||
let bytes = content.trim().to_string().into_bytes();
|
||||
let decrypt_fut = async {
|
||||
let (_metadata, bytes) = crate::mail::pgp::decrypt(
|
||||
melib::email::pgp::convert_attachment_to_rfc_spec(&bytes),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| (err, bytes))?;
|
||||
Ok((AttachmentBuilder::new(&bytes).build(), bytes))
|
||||
};
|
||||
let mut job_handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::decrypt".into(),
|
||||
decrypt_fut,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let on_success_notice_cb = Arc::new(|| "Decrypted content.".into());
|
||||
let mut retval = Self {
|
||||
filter_invocation: "gpg::decrypt".into(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
unfiltered: content.into_bytes(),
|
||||
event_handler: Some(Self::job_process_event),
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
|
||||
retval.body_text = ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb: on_success_notice_cb.clone(),
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
};
|
||||
retval.event_handler = None;
|
||||
Self::process_job_result(
|
||||
&mut retval,
|
||||
Ok(Some(job_result)),
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
context,
|
||||
);
|
||||
return Ok(retval);
|
||||
}
|
||||
return Ok(Self {
|
||||
body_text: ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb,
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
},
|
||||
..retval
|
||||
});
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
let notice = Some("Viewing attachment.\n\n".into());
|
||||
Ok(Self {
|
||||
filter_invocation: String::new(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: att.text(Text::Plain),
|
||||
},
|
||||
notice,
|
||||
body_text: att.text(),
|
||||
unfiltered: att.decode(Default::default()),
|
||||
event_handler: None,
|
||||
id: ComponentId::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn html_process_event(self_: &mut Self, 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
|
||||
|
@ -702,7 +324,7 @@ impl ViewFilter {
|
|||
command
|
||||
};
|
||||
if let Some(command) = command {
|
||||
let res = 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,
|
||||
|
@ -750,112 +372,6 @@ impl ViewFilter {
|
|||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn contains_job_id(&self, match_job_id: JobId) -> bool {
|
||||
if let ViewFilterContent::Running { ref job_id, .. } = self.body_text {
|
||||
return *job_id == match_job_id;
|
||||
}
|
||||
if let ViewFilterContent::InlineAttachments { ref parts, .. } = self.body_text {
|
||||
return parts.iter().any(|p| p.contains_job_id(match_job_id));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn job_process_event(self_: &mut Self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
log::trace!(
|
||||
"job_process_event: self_ = {:?}, event = {:?}",
|
||||
self_,
|
||||
event
|
||||
);
|
||||
if matches!(event, UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) if self_.contains_job_id(*job_id))
|
||||
{
|
||||
if matches!(self_.body_text, ViewFilterContent::Running { .. }) {
|
||||
if let ViewFilterContent::Running {
|
||||
job_id: _,
|
||||
mut job_handle,
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
} = std::mem::replace(
|
||||
&mut self_.body_text,
|
||||
ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
) {
|
||||
log::trace!("job_process_event: inside if let ");
|
||||
let job_result = job_handle.chan.try_recv();
|
||||
Self::process_job_result(
|
||||
self_,
|
||||
job_result,
|
||||
on_success_notice_cb,
|
||||
&view_settings,
|
||||
context,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if let ViewFilterContent::InlineAttachments { ref mut parts, .. } = self_.body_text {
|
||||
return parts
|
||||
.iter_mut()
|
||||
.any(|p| Self::job_process_event(p, event, context));
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn process_job_result(
|
||||
self_: &mut Self,
|
||||
result: std::result::Result<Option<FilterResult>, ::futures::channel::oneshot::Canceled>,
|
||||
on_success_notice_cb: OnSuccessNoticeCb,
|
||||
view_settings: &ViewSettings,
|
||||
context: &Context,
|
||||
) {
|
||||
match result {
|
||||
Err(err) => {
|
||||
self_.event_handler = None;
|
||||
/* Job was cancelled */
|
||||
self_.body_text = ViewFilterContent::Error {
|
||||
inner: Error::new("Job was cancelled.").set_source(Some(Arc::new(err))),
|
||||
};
|
||||
self_.notice = Some(format!("{} cancelled", self_.filter_invocation).into());
|
||||
}
|
||||
Ok(None) => {
|
||||
self_.event_handler = None;
|
||||
// something happened, perhaps a worker thread panicked
|
||||
self_.body_text = ViewFilterContent::Error {
|
||||
inner: Error::new(
|
||||
"Unknown error. Maybe some process panicked in the background?",
|
||||
),
|
||||
};
|
||||
self_.notice = Some(format!("{} failed", self_.filter_invocation).into());
|
||||
}
|
||||
Ok(Some(Ok((att, bytes)))) => {
|
||||
self_.event_handler = None;
|
||||
log::trace!("job_process_event: OK ");
|
||||
match Self::new_attachment(&att, view_settings, context) {
|
||||
Ok(mut new_self) => {
|
||||
if self_.content_type.is_text_html() {
|
||||
new_self.event_handler = Some(Self::html_process_event);
|
||||
}
|
||||
new_self.unfiltered = bytes;
|
||||
new_self.notice = Some(on_success_notice_cb());
|
||||
*self_ = new_self;
|
||||
}
|
||||
Err(err) => {
|
||||
self_.body_text = ViewFilterContent::Error { inner: err };
|
||||
self_.notice = Some(
|
||||
format!("decoding result of {} failed", self_.filter_invocation).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(Err((error, bytes)))) => {
|
||||
self_.event_handler = None;
|
||||
self_.body_text = ViewFilterContent::Error { inner: error };
|
||||
self_.unfiltered = bytes;
|
||||
self_.notice = Some(format!("{} failed", self_.filter_invocation).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ViewFilter {
|
||||
|
@ -864,9 +380,6 @@ impl Component for ViewFilter {
|
|||
if let Some(ref mut f) = self.event_handler {
|
||||
return f(self, event, context);
|
||||
}
|
||||
if let ViewFilterContent::InlineAttachments { ref mut parts, .. } = self.body_text {
|
||||
return parts.iter_mut().any(|p| p.process_event(event, context));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
|
|
238
meli/src/mail/view/html.rs
Normal file
238
meli/src/mail/view/html.rs
Normal file
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use melib::utils::xdg::query_default_app;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct HtmlView {
|
||||
pager: Pager,
|
||||
bytes: Vec<u8>,
|
||||
coordinates: Option<(AccountHash, MailboxHash, EnvelopeHash)>,
|
||||
id: ComponentId,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for HtmlView {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct(stringify!(HtmlView))
|
||||
.field("pager", &self.pager)
|
||||
.field("bytes", &self.bytes.len())
|
||||
.field("coordinates", &self.coordinates)
|
||||
.field("id", &self.id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlView {
|
||||
pub fn new(body: &Attachment, context: &mut Context) -> Self {
|
||||
let id = ComponentId::default();
|
||||
let bytes: Vec<u8> = body.decode(Default::default());
|
||||
|
||||
let settings = &context.settings;
|
||||
let mut display_text = if let Some(filter_invocation) = settings.pager.html_filter.as_ref()
|
||||
{
|
||||
let command_obj = Command::new("sh")
|
||||
.args(["-c", filter_invocation])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn();
|
||||
match command_obj {
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("Failed to start html filter process: {}", filter_invocation,)
|
||||
.into(),
|
||||
),
|
||||
source: None,
|
||||
body: err.to_string().into(),
|
||||
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
|
||||
});
|
||||
String::from_utf8_lossy(&bytes).to_string()
|
||||
}
|
||||
Ok(mut html_filter) => {
|
||||
html_filter
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&bytes)
|
||||
.expect("Failed to write to html filter stdin");
|
||||
let mut display_text = format!(
|
||||
"Text piped through `{}`. Press `v` to open in web browser. \n\n",
|
||||
filter_invocation
|
||||
);
|
||||
display_text.push_str(&String::from_utf8_lossy(
|
||||
&html_filter.wait_with_output().unwrap().stdout,
|
||||
));
|
||||
display_text
|
||||
}
|
||||
}
|
||||
} else if let Ok(mut html_filter) = Command::new("w3m")
|
||||
.args(["-I", "utf-8", "-T", "text/html"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
html_filter
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&bytes)
|
||||
.expect("Failed to write to html filter stdin");
|
||||
let mut display_text =
|
||||
String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n");
|
||||
display_text.push_str(&String::from_utf8_lossy(
|
||||
&html_filter.wait_with_output().unwrap().stdout,
|
||||
));
|
||||
|
||||
display_text
|
||||
} else {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Failed to find any application to use as html filter".into()),
|
||||
source: None,
|
||||
body: "".into(),
|
||||
kind: Some(NotificationType::Error(melib::error::ErrorKind::None)),
|
||||
});
|
||||
String::from_utf8_lossy(&bytes).to_string()
|
||||
};
|
||||
if body.count_attachments() > 1 {
|
||||
display_text =
|
||||
body.attachments()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(display_text, |mut s, (idx, a)| {
|
||||
let _ = writeln!(s, "[{}] {}\n\n", idx, a);
|
||||
s
|
||||
});
|
||||
}
|
||||
let colors = crate::conf::value(context, "mail.view.body");
|
||||
let pager = Pager::from_string(display_text, None, None, None, colors);
|
||||
HtmlView {
|
||||
pager,
|
||||
bytes,
|
||||
id,
|
||||
coordinates: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_coordinates(&mut self, new_value: Option<(AccountHash, MailboxHash, EnvelopeHash)>) {
|
||||
self.coordinates = new_value;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HtmlView {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "view")
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for HtmlView {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
self.pager.draw(grid, area, context);
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if self.pager.process_event(event, context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let UIEvent::Input(Key::Char('v')) = event {
|
||||
let command = if let Some(coordinates) = self.coordinates {
|
||||
mailbox_settings!(context[coordinates.0][&coordinates.1].pager.html_open)
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| query_default_app("text/html").ok())
|
||||
} else {
|
||||
query_default_app("text/html").ok()
|
||||
};
|
||||
let command = if cfg!(target_os = "macos") {
|
||||
command.or_else(|| Some("open".into()))
|
||||
} else if cfg!(target_os = "linux") {
|
||||
command.or_else(|| Some("xdg-open".into()))
|
||||
} else {
|
||||
command
|
||||
};
|
||||
if let Some(command) = command {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(
|
||||
command.to_string(),
|
||||
)));
|
||||
match File::create_temp_file(&self.bytes, None, None, Some("html"), true).and_then(
|
||||
|p| {
|
||||
let exec_cmd = super::desktop_exec_to_command(
|
||||
&command,
|
||||
p.path().display().to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
Ok((
|
||||
p,
|
||||
Command::new("sh")
|
||||
.args(["-c", &exec_cmd])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?,
|
||||
))
|
||||
},
|
||||
) {
|
||||
Ok((p, child)) => {
|
||||
context.temp_files.push(p);
|
||||
context.children.push(child);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!("Failed to start {err}",)),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
|
||||
"Couldn't find a default application for html files.".to_string(),
|
||||
)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
self.pager.shortcuts(context)
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
self.pager.is_dirty()
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
self.pager.set_dirty(value);
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
self.id
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue