Compare commits

...

125 Commits

Author SHA1 Message Date
Manos Pitsidianakis 4bdfb3a31b
melib/connections.rs: disable Nagle's algorithm by default
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 5m36s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 10m41s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-18 12:16:47 +03:00
Manos Pitsidianakis 671d35e21e
melib: update mailin-embedded dependency to 0.8.2
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m32s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 11m4s Details
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 5m27s Details
Closes: #391

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-16 08:49:00 +02:00
Manos Pitsidianakis a4ebe3b7d4
conf.rs: Add ErrorKind::Platform
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 5m18s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m21s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 10m32s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:32 +02:00
Manos Pitsidianakis 57e3e643a1
conversations.rs: remove excessive right padding in flags
Flags had too many spaces on its right side padding. This commit removes
it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:32 +02:00
Manos Pitsidianakis a8c7582fa3
melib/imap: fix ENVELOPE parsing in untagged responses
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:31 +02:00
Manos Pitsidianakis a9c3b151f1
listing.rs: impl highlight_self in all index styles
Add highlight_self to all listing styles (compact, conversations, plain,
thread).

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:31 +02:00
Manos Pitsidianakis 1abce964c7
melib: add Envelope::recipient_any method
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:30 +02:00
Manos Pitsidianakis 735b44f286
Add 'highlight_self' theme attribute
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 11:59:31 +02:00
Manos Pitsidianakis 50ff16c44f
themes: add LIGHT, DARK constant theme keys
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 11:59:31 +02:00
Manos Pitsidianakis 9ca34a6864
Update MSRV to 1.70.0
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-07 11:49:19 +03:00
Manos Pitsidianakis 8fff740176
Update yanked zerocopy dependency
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m17s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Failing after 7m2s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 16m12s Details
Build release binary / Build on ${{ matrix.build }} (meli-linux-amd64, linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (push) Successful in 9m24s Details
Build .deb package / Package for debian on ${{ matrix.arch }} (amd64, linux-amd64, linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (push) Successful in 10m44s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-06 19:15:58 +03:00
Manos Pitsidianakis 8eaf03554f
Bump version to 0.8.5
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m51s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 11m27s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m17s Details
Build .deb package / Package for debian on ${{ matrix.arch }} (amd64, linux-amd64, linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (push) Successful in 10m42s Details
Build release binary / Build on ${{ matrix.build }} (meli-linux-amd64, linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (push) Successful in 7m8s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-06 18:37:30 +03:00
Manos Pitsidianakis 8ec6f22090
Use ShellExpandTrait::expand in more user-provided paths
ShellExpandTrait::expand was not used consistently, leading to only some
functionalities supporting things like tilde expansion.

Fixes #387

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-06 18:33:43 +03:00
Manos Pitsidianakis b5ddc397df
terminal: remove unwrap() from get_events() loop
When exiting the app, the received value might be None.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-06 10:49:41 +03:00
Manos Pitsidianakis 46e40856ba
dialogs: fix UIConfirmationDialog highlight printing
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m38s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m11s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-05 16:56:33 +03:00
Manos Pitsidianakis 35408b1689
pager.rs: run pager filter asynchronously
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 5m2s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 10m28s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:32 +03:00
Manos Pitsidianakis 5d915baa81
terminal/embedded: use Screen::resize instead of CellBuffer::resize
CellBuffer::resize does not update generation info and should only be
used from within Screen::resize

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:32 +03:00
Manos Pitsidianakis 684fae3ed8
terminal: copy old content to new buf when resizing
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:32 +03:00
Manos Pitsidianakis ab04189887
clippy: fix new warnings for 1.78.0
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:32 +03:00
Manos Pitsidianakis 36b7c00b97
clippy: Put doc text type names and co. in backtics
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:31 +03:00
Manos Pitsidianakis 3a5306e9dd
View manpages in pager inside meli
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:31 +03:00
Manos Pitsidianakis 89c7972e12
command/error.rs: add suggestions to BadValue variant
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:31 +03:00
Manos Pitsidianakis 8f3dee9b22
args.rs: extract mod manpages to standalone file
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-02 13:47:42 +03:00
Manos Pitsidianakis 660022ce23
docs: add mailaddr.7 manpage
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m26s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m17s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-02 13:21:47 +03:00
Manos Pitsidianakis 29cc1bce5b
Remove obsolete file melib/src/text/tables.rs.gz
Fixes #382

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-02 13:20:01 +03:00
Manos Pitsidianakis bc1b65316d
conversations.rs: fix constant redrawing
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m15s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m36s Details
self.force_draw was not reset back to false after drawing, so it was
constantly being redrawn until meli becomes unresponsive.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-26 09:41:13 +03:00
Manos Pitsidianakis 11a0586d56
Remove num_cpus dependency
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m0s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 11m50s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 11m9s Details
Functionality already exists in standard library with std:🧵:available_parallelism()

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-20 16:17:01 +03:00
Manos Pitsidianakis f70496f14c
Add codemeta.json
https://codemeta.github.io/

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-18 15:08:57 +03:00
Manos Pitsidianakis 8a16cf6db4
listing/thread: fix wrong column index crash
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 6m4s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 10m58s Details
columns[0] was jused in every for loop instead of columns[n], which
would make the debug_assert_eq(area generation, column generation) panic

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-17 13:17:34 +03:00
Manos Pitsidianakis 11f3077b06
args: add more possible values for manpage names
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-14 21:47:02 +03:00
Manos Pitsidianakis dedee908d1
Update `notify` dep from 4.0.17 to 6.1.1
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m26s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 12m2s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m32s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-11 21:48:30 +03:00
Manos Pitsidianakis 255e93764a
Update `linkify` dep from 0.8.1 to 0.10.0
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-11 21:48:25 +03:00
Manos Pitsidianakis c5e9e67604
docs: add historical-manpages dir
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m11s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 11m50s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m33s Details
Add some old manpages that may be of interest to users:

- maildir (5)
- mbox (5)
- mbox (5qmail)
- qmail-maildir (5)

Under meli/docs/historical-manpages/

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-11 21:19:15 +03:00
Manos Pitsidianakis ae96038fbf
Make unicode-segmentation a hard dependency
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m48s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m42s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 11m5s Details
meli/melib are UTF8 software, so we should have proper Unicode support.

A compile-time env var is added, `UNICODE_REGENERATE_TABLES` to force
network access and rebuild the cached unicode tables.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-11 21:15:47 +03:00
Manos Pitsidianakis 07072e2e3f
melib/thread: prevent panic if envelope is deleted
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-06 21:08:37 +03:00
Manos Pitsidianakis aa5737a004
compose: prevent drawing pager on embedded mode
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-06 21:07:40 +03:00
Manos Pitsidianakis 48cb9ee204
Fix compilation for macos
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m25s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m2s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-05 16:27:25 +03:00
Guillaume Ranquet c53a32de4c
thread: re-enables horizontal thread view
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m46s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m32s Details
Re-implemnts horizontal thread view.
Default is still vertical, but pressing toggle_layout now has an effect.

Signed-off-by: Guillaume Ranquet <granquet@baylibre.com>
2024-04-05 16:00:58 +03:00
Manos Pitsidianakis a69c674c07
Fix new 1.77 clippy lints
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m20s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 12m33s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-24 16:04:51 +02:00
Manos Pitsidianakis 6a66afe93e
view: make add contact dialog scrollable on overflow
If contact entries in the add contact dialog are too many to fit in the
dialog area, show a scrollbar and allow the user to navigate it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-24 15:21:05 +02:00
Manos Pitsidianakis 974502c6ff
melib/addressbook: impl Hash for Card
Implement hashing for Card.

This fixes the appearance of duplicate entries in the add contacts
selector in an envelope view when an address appears more than one time
in the envelope headers.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-24 15:14:20 +02:00
Manos Pitsidianakis 3e9144657b
meli: store children process metadata
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Failing after 7m2s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Failing after 10m42s Details
Store children process metadata. Pid and command lines can then be shown
in the UI and in logs.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-23 10:43:58 +02:00
Manos Pitsidianakis 35a9f33aab
listing: extract common FlagString logic
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-21 21:04:22 +02:00
Manos Pitsidianakis 475609fe92
listing: make {prev,next}_entry shortcut behavior consistent
prev_entry, next_entry shortcuts (default bindings: Ctrl+p and Ctrl+n)
were not behaving consistently in all different listing index styles. In
particular in some conditions the switch entry shortcuts worked at most
once because the cursor position was not updated properly. This commit
fixes that.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-21 13:35:41 +02:00
Manos Pitsidianakis 38bca8f8bc
docs/meli.conf.5: mention use_oauth2=true for gmail oauth2
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m52s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 12m42s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-17 14:07:07 +02:00
Manos Pitsidianakis ec01a4412a
melib/imap: turn some sync connections to unsync
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 23:47:30 +02:00
Manos Pitsidianakis 4e941a9e8b
accounts: add default_mailbox setting
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m28s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m30s Details
Add a default mailbox setting:

> The mailbox that is the default to open / view for this account. Must be
> a valid mailbox name.
>
> If not specified, the default is [`Self::root_mailbox`].

Closes: #350
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 18:16:05 +02:00
Manos Pitsidianakis 742f038f74
accounts: move sent_mailbox to settings
In the next commits we will add a `default_mailbox` field. Instead of
poluting the `Account` struct with more setting fields, consolidate them
on its `settings`.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 18:15:51 +02:00
Manos Pitsidianakis 484712b0c3
accounts: check for unrecoverable errors in is_online
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 18:15:50 +02:00
Manos Pitsidianakis 264782d228
Various unimportant minor style/doc fixups
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 18:15:50 +02:00
Manos Pitsidianakis 41e965b8a3
meli/accounts: split mbox/job stuff in submodules
accounts.rs is getting rather long (almost 3K lines) so split standalone
stuff in submodules.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 12:48:57 +02:00
Manos Pitsidianakis f31b5c4000
melib/connections: don't print raw bytes as escaped unicode
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-15 13:20:07 +02:00
Manos Pitsidianakis 8014af2563
imap/protocol_parser: reduce debug prints
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-15 13:19:26 +02:00
Manos Pitsidianakis 4ce616aeca
CI: fix lints.yaml rustup install step
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m23s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 10m39s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 10m3s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-15 11:42:58 +02:00
Manos Pitsidianakis a3aaec382a
melib/conf: remove unused imports
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 11m24s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m38s Details
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Failing after 17s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-14 19:18:23 +02:00
Manos Pitsidianakis b8b24282a0
Update all instances of old domains with meli-email.org
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Failing after 11m42s Details
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Failing after 19s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m12s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-10 21:38:12 +02:00
Manos Pitsidianakis e481880321
Various manpage touchups and URL updates
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-10 21:34:46 +02:00
Geert Stappers a88b8c5ea0 debian/changelog warning fix
Added
- actual change log entries
- a space in front of hyphen hyphen
- empty lines

Signed-off-by: Geert Stappers <stappers@stappers.it>
2024-03-10 16:43:51 +02:00
Manos Pitsidianakis b820bd6d9c
melib/imap: remove unused imap_trace! and fix comp
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m29s Details
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 4m25s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-10 13:08:58 +02:00
Manos Pitsidianakis 3b93fa8e7c
state.rs: don't draw messages above embedded terminal
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m21s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m13s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-08 16:56:47 +02:00
Manos Pitsidianakis 634bd1917a
melib/imap: convert log prints to traces
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m17s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m23s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-08 16:56:33 +02:00
Manos Pitsidianakis b5fd3f57a7
listing.rs: make self.view an Option
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m52s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m21s Details
Prevent accessing a ThreadView if it has not been initialized by making
an uninitialized ThreadView impossible.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-08 16:56:01 +02:00
Manos Pitsidianakis 1fcb1d59b8
build.rs: remove rerun when build.rs changes
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-08 16:55:28 +02:00
Manos Pitsidianakis e2cdebe89c
Add option to highlight self in mailing list threads
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m34s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m24s Details
Use under `listing` options such as:

globally
========

  [listing]
  highlight_self = true

per-account
===========

  [accounts.work]
  root_mailbox = '[Gmail]'
  format = "imap"
  subscribed_mailboxes = ["*"]
  listing.index_style = "compact"
  listing.highlight_self = true

per-mailbox
===========

  [accounts.work.mailboxes]
  "INBOX/Lists/project-devel" = { listing.highlight_self=true }

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-06 17:39:28 +02:00
Manos Pitsidianakis 3884c0da1f
docs/meli.conf.5: small typographic fixups
- Add macro for literal string values to enable showing unicode
 literal characters
- Fix bool/boolean inconsistency
- Fix "true" / true inconsistency
- Add macro for horizontal rule in subsections
- Add terminal subsection about unicode modifier / combining marks for
  emojis

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-06 17:39:28 +02:00
Manos Pitsidianakis 26928e3ae9
terminal: fix compilation for macos
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m24s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m15s Details
Fixes: 70fc2b455c ("Update nix dependency to 0.27")
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-03 22:57:09 +02:00
Manos Pitsidianakis 070930e671
meli/sqlite3: Fix auto index build when missing
An error was returned from the db_path function, preventing the issuing
of the reindex command in the background.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-03 22:57:09 +02:00
Manos Pitsidianakis c7aee72525
melib: add clippy::doc_markdown
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-03 22:57:09 +02:00
Manos Pitsidianakis 30a3205e4f
meli: Add clippy::doc_markdown
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-03 11:38:57 +02:00
Manos Pitsidianakis 9af284b8db
listing: Don't hide unread count for mailboxes that are partly truncated
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m4s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m31s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-01 17:14:05 +02:00
Manos Pitsidianakis 62aee4644b
Add subcommand to print log file location
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 11m44s Details
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 5m50s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-29 12:04:49 +02:00
Manos Pitsidianakis 5af2e1ee66
Add subcommand to print config file location
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-29 11:53:30 +02:00
Manos Pitsidianakis 4e7b665672
sqlite caching refactor
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m54s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m32s Details
General refactoring to make blocking operations use special blocking
thread workers, SQL operations to use transactions, and setting up WAL
journal mode mode to minimize locking.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-24 19:16:42 +02:00
Manos Pitsidianakis fd64fe0bf8
README.md: update codeberg.org URL
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-21 19:54:50 +02:00
Manos Pitsidianakis 51e3f163d4
melib/jmap: Use Url instead of String in deserializing
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m55s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 18m21s Details
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m7s Details
Catch invalid URLs at the parsing stage.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-13 14:13:53 +02:00
Manos Pitsidianakis 417b24cd84
meli: print invalid command on error
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m19s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m29s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m1s Details
Instead of printing just "invalid command", print the command as well.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-13 14:13:52 +02:00
Manos Pitsidianakis 873a67d0fb
Replace erroneous use of set_err_kind with set_kind
set_err_kind() is a method of the IntoError trait, not an Error method;
it is meant to be used for any error type that can be converted into
Error. Since melib::Error implements Into<melib::Error> tautologically,
this was not a compilation error. Nevertheless, the correct thing to do
is use the type method directly to set ErrorKind.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis c332c2f5ff
Fix new clippy lints (mostly clippy::blocks_in_conditions)
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis 1048ce6824
melib/utils: add hostname() utility function
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis 70fc2b455c
Update nix dependency to 0.27
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis 8de8addd11
melib/datetime: add cfg for musl builds
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis 1fe3619208
conf: Make conf validation recognize AccountSettings extra keys
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m19s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m39s Details
AccountSettings extra keys like `vcard_folder` were not taken into
account when validating a config.

This commit introduces an AccountSettings::validate_config() method that
checks for the presence and validity of this key value.

Fixes #349

#349

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-04 14:52:06 +02:00
Manos Pitsidianakis 0b468d88ad
addressbook/vcard: improve Error messages
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-04 14:52:06 +02:00
Manos Pitsidianakis 1eca34b398
Set lowest priority to shortcut command UIEvents
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 12m2s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 19m14s Details
Commit (a37d5fc1 conf/shortcuts: implement
a key to command mapping) introduced shortcuts that expand to user
defined commands. To allow already existing shortcuts to take
precedence, the check for the user-defined shortcuts should be the last
one in the evaluation order.

Example problem scenario:
- Press new_mail shortcut (e.g. `m`)
- Code in listing.rs searches if it matches any of the commands, and
  regardless if it matches or not, stops the evaluation and returns.
- New mail composer never shows up.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-21 12:03:40 +02:00
Manos Pitsidianakis 5afc078587
Update README.md, DEVELOPMENT.md and create BUILD.md
README.md is quite lengthy so split extraneous info to other `.md`
files.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-20 12:09:34 +02:00
Guillaume Ranquet a37d5fc1d1 conf/shortcuts: implement a key to command mapping
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m15s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 20m58s Details
Permits users to map keys in their configuration file to an array of meli commands

e.g:
[shortcuts.listing]
commands = [ { command = [ "tag remove trash", "flag unset trash" ], shortcut = "D" },
             { command = [ "tag add trash", "flag set trash" ], shortcut = "d" } ]

Signed-off-by: Guillaume Ranquet <granquet@baylibre.com>
2024-01-18 15:53:35 +01:00
Manos Pitsidianakis 60f26f9dae
melib: Fix some old pre-intradoc rustdoc links
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 11m3s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 18m7s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-11 09:22:28 +02:00
Ethra e80ea9c9de
Changed default manpage install path
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 12m15s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 19m32s Details
2024-01-11 05:08:58 +03:00
Manos Pitsidianakis 64e60cb0ee
listing: fix select modifier regression
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m51s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m56s Details
Commit 61a0c3c27f ("listing: do not clear
selection after action") broke select/jump modifiers (e.g. prefixing a
jump with a number).

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-08 12:32:50 +02:00
Manos Pitsidianakis 81d1c0536b
scripts: add mandoc_lint.sh
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-06 16:22:16 +02:00
Manos Pitsidianakis cd448924ed
listing: add clear-selection command
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m36s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m27s Details
Add a command that performs what Escape does: clears the selection.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-06 15:35:56 +02:00
Manos Pitsidianakis 61a0c3c27f
listing: do not clear selection after action
Clear selection only when Escape is pressed, not after action is
completed. The user might want to perform further actions on the
selection.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-06 15:20:00 +02:00
Manos Pitsidianakis 7952006870
melib/percent_encoding: remove doctests, add tests module
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m37s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m34s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-04 10:41:00 +02:00
Manos Pitsidianakis ddab3179c2
melib/wcwidth: move tests to tests module
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-04 10:40:31 +02:00
Manos Pitsidianakis 7861fb0402
Fix typos found with `typos` tool
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-03 11:08:55 +02:00
Manos Pitsidianakis 148f0433d9
meli: implement flag set/unset action in UI
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 10m12s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 16m30s Details
Also document it in manpages meli.1 and meli.7

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 21:28:21 +02:00
Manos Pitsidianakis 8185f2cf7d
meli: add deny clippy lints and fix them
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 10m56s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 17m46s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 15:59:13 +02:00
Manos Pitsidianakis 0270db0123
melib: From<&[u8]> -> From<B: AsRef<[u8]>>
This change allows byte literals to be used with the from trait method.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 15:16:47 +02:00
Manos Pitsidianakis 8ddd673dd8
melib/imap/untagged: update all mailboxes
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 10m41s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 17m22s Details
When receiving an envelope event (deleted, or changed flags), update all
mailboxes that contain that envelope hash; not just the currently
selected mailbox.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 14:36:29 +02:00
Manos Pitsidianakis e3351d2755
melib/imap: fix set unseen updating all mboxes
When manually setting an envelope as not seen, all mailboxes had their
unseen count increased. This commit updates only those that include the
envelope in the first place.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 14:34:50 +02:00
Manos Pitsidianakis 31401fa35c
melib/backends: add LazyCountSet::contains method
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 14:34:05 +02:00
Manos Pitsidianakis 33408146a1
Fix feature permutation mis-compilations found with cargo-hack
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m54s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m46s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m28s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 11:38:42 +02:00
Manos Pitsidianakis 8a95febb78
CI: set debuginfo=0 in test/lint builds
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m55s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 20m39s Details
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 7m47s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 11:03:29 +02:00
Manos Pitsidianakis 73d5b24e98
melib/tests: merge integration tests in one crate
Saves about 0.5 seconds from compilation and runtime.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 10:44:31 +02:00
Manos Pitsidianakis 0da97dd8c1
mail/listing: check row_updates in is_dirty()
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m0s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m3s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 12m10s Details
If there are row_updates, it means we need to redraw. But in the draw()
call, we check is_dirty() to decide whether to proceed drawing. Add
row_updates not being empty into the dirty conditions.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 20:02:58 +02:00
Manos Pitsidianakis 933bf157ae
melib/email/parser: ack \ as an atom
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m31s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m5s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m24s Details
I think this is not spec compliant but the MIME spec (rfc6068 - The
'mailto' URI Scheme) uses it for "valid" addresses.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:56:52 +02:00
Manos Pitsidianakis f685726eac
melib/email/parser: add backtrace field to ParsingError
Add backtrace field to ParsingError when the build is for testing or
documentation.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:56:52 +02:00
Manos Pitsidianakis ab1b946fd9
melib/error: don't print details if it's an empty string.
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:56:52 +02:00
Manos Pitsidianakis ce4ba06ce9
command: add a flag set/unset command
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m12s Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m39s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 15m13s Details
e.g. "flag unset draft"

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:51:45 +02:00
Manos Pitsidianakis bebb473d1b
melib/mbox: derive extra traits for enums
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:51:45 +02:00
Manos Pitsidianakis f0866a3965
meli: make config error more user-friendly
If `send_mail` is incorrect, display a long-ish list of valid examples.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:51:45 +02:00
Manos Pitsidianakis f63774fa6d
Fix new clippy lints (1.75)
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:51:44 +02:00
Manos Pitsidianakis 808aa4942d
melib: rename text_processing to text for the whole brevity thing
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-26 16:47:42 +02:00
Manos Pitsidianakis 08518e1ca8
terminal: remove obsolete position.rs module
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m15s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m15s Details
The functions in terminal::position were pretty much obsolete after
commit

0e3a0c4b70 Add safe UI widget area drawing API

So this commit does a little cleanup and removes the module.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 19:49:23 +02:00
Manos Pitsidianakis 34a2d52e7e
Fix rustdoc::redundant_explicit_links
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m28s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m37s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 19:14:00 +02:00
Manos Pitsidianakis 4026e25428
melib/notmuch: add some doc comments
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 19:13:15 +02:00
Manos Pitsidianakis ca7d7bb95d
melib/notmuch: use message freeze/thaw for flag changes
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 19:12:45 +02:00
Manos Pitsidianakis ebe1b3da7e
melib/notmuch: wrap *mut struct fields in NonNull<_>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 18:50:23 +02:00
Manos Pitsidianakis 506ae9f594
melib/error: Add ErrorKind::LinkedLibrary variant
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 18:43:12 +02:00
Manos Pitsidianakis b6f769b2f4
mail/listing: add field names to row_attr! bool values
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 18:35:09 +02:00
Manos Pitsidianakis 3691cd2962
accounts.rs: send EnvelopeUpdate event after self.collection.update_flags()
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 18:20:14 +02:00
Manos Pitsidianakis 97eb636375
Makefile: add dpkg --print-architecture to deb filename
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-13 15:43:00 +02:00
Manos Pitsidianakis b3079715f6
melib/smtp: disable flakey test_smtp()
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m49s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m54s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-13 09:45:17 +02:00
Manos Pitsidianakis 86bbf1ea57
melib/notmuch: refresh NotmuchMailbox counts when setting flags
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m59s Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 17m21s Details
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-12 20:03:14 +02:00
Manos Pitsidianakis 1b0bdd0a9a
melib/notmuch: split queries and mailbox into submodules
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-12 20:03:14 +02:00
188 changed files with 13616 additions and 8563 deletions

View File

@ -5,7 +5,7 @@ env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short
@ -56,7 +56,8 @@ jobs:
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
source "${HOME}/.cargo/env"
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
rustup toolchain install --profile minimal --component clippy,rustfmt ${{ matrix.rust }} --target ${{ matrix.target }}
rustup toolchain install --profile minimal --component clippy,rustfmt --target ${{ matrix.target }} -- "${{ matrix.rust }}"
rustup default ${{ matrix.rust }}
fi
- name: Configure cargo data directory
# After this point, all cargo registry and crate data is stored in

View File

@ -5,7 +5,7 @@ env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short

View File

@ -5,7 +5,7 @@ env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short
@ -90,11 +90,14 @@ jobs:
if: success() || failure()
run: cargo test --all --no-fail-fast --all-features --no-run --locked
- name: cargo test
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
run: |
cargo nextest run --all --no-fail-fast --all-features --future-incompat-report -E 'not (test(smtp::test::test_smtp))'
#cargo test --all --no-fail-fast --all-features -- --nocapture --quiet
- name: rustdoc build
if: success() || failure()
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
run: |
make build-rustdoc
- name: rustdoc tests
if: success() || failure()
run: |
make test-docs

76
BUILD.md 100644
View File

@ -0,0 +1,76 @@
# Build `meli`
For a quick start, build and install locally:
```sh
PREFIX=~/.local make install
```
Available subcommands for `make` are listed with `make help`.
The Makefile *should* be POSIX portable and not require a specific `make` version.
`meli` requires rust version 1.70.0 or later and rust's package manager, Cargo.
Information on how to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`.
Run `make install` to install the binary and man pages.
This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=${HOME}/.local install`.
You can build and run `meli` with one command: `cargo run --release`.
## Build features
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
- `gpgme` enables GPG support via `libgpgme` (on by default)
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (on by default)
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` (on by default).
- `regexp` provides experimental support for theming some e-mail fields based
on regular expressions.
It uses the `pcre2` library.
Since it's actual use in the code is very limited, it is not recommended to use this (off by default).
- `static` and `*-static` bundle C libraries in dependencies so that you don't need them installed in your system (on by default).
Though not a feature, the presence of the environment variable `UNICODE_REGENERATE_TABLES` in compile-time of the `melib` crate will force the regeneration of unicode tables.
Otherwise the tables are included with the source code, and there's no real reason to regenerate them unless you intend to modify the code or update to a new Unicode version.
## Build Debian package (*deb*)
Building with Debian's packaged cargo might require the installation of these two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
A `*.deb` package can be built with `make deb-dist`
## Using notmuch
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system.
In Debian-like systems, install the `libnotmuch5` packages.
`meli` detects the library's presence on runtime.
If it is not detected, you can use the `library_file_path` setting on your notmuch account to specify the absolute path of the library.
## Using GPG
To use the optional gpg feature, you must have `libgpgme` installed in your system.
In Debian-like systems, install the `libgpgme11` package.
`meli` detects the library's presence on runtime.
## Development
Development builds can be built and/or run with
```
cargo build
cargo run
```
There is a debug/tracing log feature that can be enabled by using the flag `--feature debug-tracing` after uncommenting the features in `Cargo.toml`.
The logs are printed in stderr when the env var `MELI_DEBUG_STDERR` is defined, thus you can run `meli` with a redirection (i.e `2> log`).
To trace network and protocol communications you can enable the following features:
- `imap-trace`
- `jmap-trace`
- `nntp-trace`
- `smtp-trace`

File diff suppressed because it is too large Load Diff

595
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,23 @@
# Development
Code style follows the `rustfmt.toml` file.
## Trace logs
Enable trace logs to `stderr` with:
```sh
export MELI_DEBUG_STDERR=yes
```
This means you will have to to redirect `stderr` to a file like `meli 2> trace.log`.
Tracing is opt-in by build features:
```sh
cargo build --features=debug-tracing,imap-trace,smtp-trace
```
## use `.git-blame-ignore-revs` file _optional_
Use this file to ignore formatting commits from `git-blame`.
@ -9,8 +27,24 @@ It needs to be set up per project because `git-blame` will fail if it's missing.
git config blame.ignoreRevsFile .git-blame-ignore-revs
```
## Formatting with `rustfmt`
```sh
make fmt
```
## Linting with `clippy`
```sh
make lint
```
## Testing
```sh
make test
```
How to run specific tests:
```sh
@ -20,14 +54,14 @@ cargo test -p {melib, meli} (-- --nocapture) (--test test_name)
## Profiling
```sh
perf record -g target/debug/bin
perf record -g target/debug/meli
perf script | stackcollapse-perf | rust-unmangle | flamegraph > perf.svg
```
## Running fuzz targets
Note: `cargo-fuzz` requires the nightly toolchain.
```sh
cargo +nightly fuzz run envelope_parse -- -dict=fuzz/envelope_tokens.dict
```
<!-- -->
<!-- ## Running fuzz targets -->
<!-- -->
<!-- Note: `cargo-fuzz` requires the nightly toolchain. -->
<!-- -->
<!-- ```sh -->
<!-- cargo +nightly fuzz run envelope_parse -- -dict=fuzz/envelope_tokens.dict -->
<!-- ``` -->

View File

@ -24,6 +24,7 @@ TAGREF_BIN ?= tagref
CARGO_ARGS ?=
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility
CARGO_SORT_BIN = cargo-sort
CARGO_HACK_BIN = cargo-hack
PRINTF = /usr/bin/printf
# Options
@ -119,6 +120,10 @@ test: test-docs
test-docs:
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --doc
.PHONY: test-feature-permutations
test-feature-permutations:
$(CARGO_HACK_BIN) hack --feature-powerset
.PHONY: check-deps
check-deps:
@(if ! echo ${MIN_RUSTC}\\n`${CARGO_BIN} --version | grep ^cargo | cut -d ' ' -f 2` | sort -CV; then echo "rust version >= ${RED}${MIN_RUSTC}${ANSI_RESET} required, found: `which ${CARGO_BIN}` `${CARGO_BIN} --version | cut -d ' ' -f 2`" \
@ -178,10 +183,11 @@ install-bin: meli
.NOTPARALLEL: yes
install: meli install-bin install-doc
@(if [ -z $${NO_MAN+x} ]; then \
echo "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
$(PRINTF) "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
$(PRINTF) "\n or the tutorial in meli(7) (\`man 7 meli\`).\n" ;\
fi)
@echo " - Report bugs in the mailing list or git issue tracker ${UNDERLINE}https://git.meli.delivery${ANSI_RESET}"
@echo " - If you have a specific feature or workflow you want to use, you can post in the mailing list or git issue tracker."
@$(PRINTF) " - Report bugs in the mailing list or git issue tracker ${UNDERLINE}https://git.meli-email.org${ANSI_RESET}\n"
@$(PRINTF) " - If you have a specific feature or workflow you want to use, you can post in the mailing list or git issue tracker.\n"
.PHONY: dist
dist:
@ -192,7 +198,7 @@ dist:
deb-dist:
@author=$(grep -m1 authors meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
@dpkg-buildpackage -b -rfakeroot -us -uc --build-by="${author}" --release-by="${author}"
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_amd64.deb
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_`dpkg --print-architecture`.deb
.PHONY: build-rustdoc
build-rustdoc:
@ -216,3 +222,27 @@ test-makefile:
@export DATEVAL=$$(printf "%s" ${DATE} | wc -c | tr -d "[:blank:]" 2>&1); ([ "$${DATEVAL}" = "10" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${RED}ERROR${ANSI_RESET}\n'date -I' does not produce a YYYY-MM-DD output on this platform.\n" 1>&2
@$(PRINTF) "Checking that the git commit SHA can be detected. "
@([ ! -z "$(GIT_COMMIT)" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${YELLOW}WARN${ANSI_RESET}\nGIT_COMMIT env var is empty.\n" 1>&2
# Checking if mdoc changes produce new lint warnings from mandoc(1) compared to HEAD version:
#
# example invocation: `mandoc_lint meli.1`
#
# with diff(1)
# ============
#function mandoc_lint () {
#diff <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
#}
#
# with sdiff(1) (side by side)
# ============================
#
#function mandoc_lint () {
#sdiff <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
#}
#
# with delta(1)
# =============
#
#function mandoc_lint () {
#delta --side-by-side <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
#}

152
README.md
View File

@ -1,27 +1,46 @@
# meli [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli) [![IRC channel](https://img.shields.io/badge/irc.oftc.net-%23meli-blue)](ircs://irc.oftc.net:6697/%23meli)
# meli ![Established, created in 2017](https://img.shields.io/badge/Est.-2017-blue) ![Minimum Supported Rust Version](https://img.shields.io/badge/MSRV-1.70.0-blue) [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli) [![IRC channel](https://img.shields.io/badge/irc.oftc.net-%23meli-blue)](ircs://irc.oftc.net:6697/%23meli)
**BSD/Linux terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).**
**BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).**
* [mailing lists](https://lists.meli.delivery/) | `#meli` on OFTC IRC
* Main repository: <https://git.meli.delivery/meli/meli> Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
* Official mirrors:
- <https://codeberg.org/epilys/meli>
- <https://github.com/meli/meli>
Try an [old online interactive web demo](https://meli-email.org/wasm2.html "online interactive web demo") powered by WebAssembly!
* `#meli` on OFTC IRC | [mailing lists](https://lists.meli-email.org/)
* Repository:
- Main <https://git.meli-email.org/meli/meli> Report bugs and/or feature requests in [meli's issue tracker](https://git.meli-email.org/meli/meli/issues "meli gitea issue tracker")
- Official mirror <https://codeberg.org/meli/meli>
- Official mirror <https://github.com/meli/meli>
**Table of contents**:
- [Install](#install)
- [Build](#build)
- [Quick start](#quick-start)
- [Supported E-mail backends](#supported-e-mail-backends)
- [E-mail submission backends](#e-mail-submission-backends)
- [Non-exhaustive list of features](#non-exhaustive-list-of-features)
- [HTML Rendering](#html-rendering)
- [Documentation](#documentation)
## Install
- Try an [old online interactive web demo](https://meli.delivery/wasm2.html "online interactive web demo") powered by WebAssembly
- Pre-built binaries for [pkgsrc](https://pkgsrc.se/mail/meli) and [openbsd ports](https://openports.pl/path/mail/meli).
- `cargo install meli` or `cargo install --git https://git.meli.delivery/meli/meli.git meli`
- [Download and install pre-built debian package, static linux binary](https://github.com/meli/meli/releases/ "github releases for meli"), or
- Install with [Nix](https://search.nixos.org/packages?show=meli&query=meli&from=0&size=30&sort=relevance&channel=unstable#disabled "nixos package search results for 'meli'").
- [pkgsrc](https://pkgsrc.se/mail/meli)
- [openbsd ports](https://openports.pl/path/mail/meli)
- `cargo install meli` or `cargo install --git https://git.meli-email.org/meli/meli.git meli`
- [Pre-built debian package, static binaries](https://github.com/meli/meli/releases/ "github releases for meli")
- [Nix](https://search.nixos.org/packages?show=meli&query=meli&from=0&size=30&sort=relevance&channel=unstable#disabled "nixos package search results for 'meli'")
## Build
Run `cargo build --release --bin meli` or `make`.
For detailed building instructions, see [`BUILD.md`](./BUILD.md)
## Quick start
<table>
<tr><td>
```shell
```sh
# Create configuration file in ${XDG_CONFIG_HOME}/meli/config.toml:
$ meli create-config
# Edit configuration in ${EDITOR} or ${VISUAL}:
@ -36,9 +55,12 @@ $ meli
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
See also the [Quickstart tutorial](https://meli-email.org/documentation.html#quick-start) online.
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation. Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation"). `meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
Manual pages are also [hosted online](https://meli-email.org/documentation.html "meli documentation").
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`.
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, i.e.:
@ -100,92 +122,32 @@ Main view | Compact main view | Compose with embed terminal editor
- GPG signing, encryption, signing + encryption
- GPG signature verification
## Documentation
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, i.e.:
```sh
MELI_CONFIG=./test_config cargo run
```
## Build
For a quick start, build and install locally:
```sh
PREFIX=~/.local make install
```
Available subcommands for `make` are listed with `make help`.
The Makefile *should* be POSIX portable and not require a specific `make` version.
`meli` requires rust version 1.68.2 or later and rust's package manager, Cargo.
Information on how to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`.
Run `make install` to install the binary and man pages.
This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=${HOME}/.local install`.
You can build and run `meli` with one command: `cargo run --release`.
### Build features
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
- `gpgme` enables GPG support via `libgpgme` (on by default)
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (on by default)
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` (on by default).
- `regexp` provides experimental support for theming some e-mail fields based
on regular expressions.
It uses the `pcre2` library.
Since it's actual use in the code is very limited, it is not recommended to use this (off by default).
- `static` and `*-static` bundle C libraries in dependencies so that you don't need them installed in your system (on by default).
### Build Debian package (*deb*)
Building with Debian's packaged cargo might require the installation of these two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
A `*.deb` package can be built with `make deb-dist`
### Using notmuch
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system.
In Debian-like systems, install the `libnotmuch5` packages.
`meli` detects the library's presence on runtime.
### Using GPG
To use the optional gpg feature, you must have `libgpgme` installed in your system.
In Debian-like systems, install the `libgpgme11` package.
`meli` detects the library's presence on runtime.
### HTML Rendering
## HTML Rendering
HTML rendering is achieved using [w3m](https://github.com/tats/w3m) by default.
You can use the `pager.html_filter` setting to override this (for more details you can consult [`meli.conf(5)`](./meli/docs/meli.conf.5)).
# Development
Development builds can be built and/or run with
## Documentation
```
cargo build
cargo run
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
See also the [Quickstart tutorial](https://meli-email.org/documentation.html#quick-start) online.
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
Manual pages are also [hosted online](https://meli-email.org/documentation.html "meli documentation").
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, or use the `[-c, --config]` argument:
```sh
MELI_CONFIG=./test_config meli
```
There is a debug/tracing log feature that can be enabled by using the flag `--feature debug-tracing` after uncommenting the features in `Cargo.toml`.
The logs are printed in stderr when the env var `MELI_DEBUG_STDERR` is defined, thus you can run `meli` with a redirection (i.e `2> log`).
or
Code style follows the `rustfmt.toml` file.
```sh
meli -c ./test_config
```

View File

@ -22,10 +22,10 @@ body = """
{% if not version %}
## [Unreleased]
{% else %}
## [{{ version }}](https://git.meliemail.org/meli/meli/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
## [{{ version }}](https://git.meli-email.org/meli/meli/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}
{% macro commit(commit) -%}
- [{{ commit.id | truncate(length=8, end="") }}]({{ "https://git.meliemail.org/meli/meli/commit/" ~ commit.id }}) {% if commit.scope %}*({{commit.scope | lower }})* {% endif %}{{ commit.message | split(pat="\n")| first | upper_first }}{% endmacro -%}
- [{{ commit.id | truncate(length=8, end="") }}]({{ "https://git.meli-email.org/meli/meli/commit/" ~ commit.id }}) {% if commit.scope %}*({{commit.scope | lower }})* {% endif %}{{ commit.message | split(pat="\n")| first | upper_first }}{% endmacro -%}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
@ -58,7 +58,7 @@ filter_unconventional = false
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://git.meliemail.org/meli/meli/issues/${2}))" },
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://git.meli-email.org/meli/meli/issues/${2}))" },
]
# regex for parsing and grouping commits
commit_parsers = [

62
codemeta.json 100644
View File

@ -0,0 +1,62 @@
{
"@context": ["https://doi.org/10.5063/schema/codemeta-2.0", "http://schema.org/"],
"@type": "SoftwareSourceCode",
"applicationCategory": "E-mail client",
"author": [
{
"@id": "https://pitsidianak.is/",
"@type": "Person",
"name": "epilys",
"email": "manos@pitsidianak.is",
"familyName": "Pitsidianakis",
"givenName": "Manos",
"url": "https://pitsidianak.is/"
}
],
"codeRepository": "https://git.meli-email.org/meli/meli.git",
"dateCreated": "2016-04-25",
"dateModified": "2023-12-11",
"datePublished": "2017-07-23",
"description": "BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).",
"downloadUrl": "https://git.meli-email.org/meli/meli/archive/v0.8.5.tar.gz",
"identifier": "https://meli-email.org/",
"isPartOf": "https://meli-email.org/",
"keywords": [
"e-mail",
"email",
"mail",
"terminal user interface",
"client",
"mua",
"mail user agent",
"smtp",
"imap",
"jmap",
"mbox",
"maildir",
"nntp"
],
"license": [
"https://spdx.org/licenses/EUPL-1.2",
"https://spdx.org/licenses/GPL-3.0-or-later"
],
"name": "meli",
"operatingSystem": [
"Linux",
"macOS",
"OpenBSD",
"NetBSD"
],
"programmingLanguage": "Rust",
"relatedLink": [
"https://codeberg.org/meli/meli",
"https://github.com/meli/meli",
"https://lists.meli-email.org/"
],
"version": "0.8.5",
"contIntegration": "https://git.meli-email.org/meli/meli/actions",
"developmentStatus": "active",
"issueTracker": "https://git.meli-email.org/meli/meli/issues",
"readme": "https://git.meli-email.org/meli/meli/raw/commit/dedee908d1e0b42773bade8e0604e94b14810e2d/README.md",
"buildInstructions": "https://git.meli-email.org/meli/meli/raw/commit/dedee908d1e0b42773bade8e0604e94b14810e2d/BUILD.md"
}

227
debian/changelog vendored
View File

@ -1,13 +1,236 @@
meli (0.8.5-1) bookworm; urgency=low
Contributors in alphabetical order:
- Andrei Zisu
- Ethra
- Geert Stappers
- Guillaume Ranquet
- Manos Pitsidianakis
Added
=====
- 0e3a0c4b Add safe UI widget area drawing API
- 0114e695 Add next_search_result and previous_search_result shortcuts
- 0b468d88 Improve Error messages
- 5af2e1ee Add subcommand to print config file location
- 62aee464 Add subcommand to print log file location
- e2cdebe8 Add option to highlight self in mailing list threads
- cd448924 Add clear-selection command
- 3a5306e9 View manpages in pager inside meli
- a37d5fc1 Implement a key to command mapping
- ce4ba06c Add a flag set/unset command
- 148f0433 Implement flag set/unset action in UI
- 417b24cd Print invalid command on error
- 4e941a9e Add default_mailbox setting
- 974502c6 Impl Hash for Card
- ba7a97e9 Add x axis scroll support
- ccf6f9a2 Remember previous set index_style preferences
Bug Fixes
=========
- bcec745c Fix command and status bar drawing
- 62b8465f Fix ThreadView for new TUI API
- 28fa66cc Fix ThreadedListing for new TUI API
- 2c6f180d Fix macos compilation
- 24971d19 Fix compilation with 1.70.0 cargo
- 34a2d52e Fix rustdoc::redundant_explicit_links
- f63774fa Fix new clippy lints (1.75)
- 33408146 Fix feature permutation mis-compilations found with cargo-hack
- e3351d27 Fix set unseen updating all mboxes
- 8185f2cf Add deny clippy lints and fix them
- 7861fb04 Fix typos found with typos tool
- 64e60cb0 Fix select modifier regression
- 60f26f9d Fix some old pre-intradoc rustdoc links
- 1fe36192 Make conf validation recognize AccountSettings extra keys
- c332c2f5 Fix new clippy lints (mostly clippy::blocks_in_conditions)
- 070930e6 Fix auto index build when missing
- 26928e3a Fix compilation for macos
- 3884c0da Small typographic fixups
- b820bd6d Remove unused imap_trace! and fix comp
- a88b8c5e Debian/changelog warning fix
- 4ce616ae Fix lints.yaml rustup install step
- 264782d2 Various unimportant minor style/doc fixups
- 475609fe Make {prev,next}_entry shortcut behavior consistent
- a69c674c Fix new 1.77 clippy lints
- 48cb9ee2 Fix compilation for macos
- 8a16cf6d Fix wrong column index crash
- bc1b6531 Fix constant redrawing
- 29cc1bce Remove obsolete file melib/src/text/tables.rs.gz
- ab041898 Fix new warnings for 1.78.0
- 46e40856 Fix UIConfirmationDialog highlight printing
- 3b93fa8e Don't draw messages above embedded terminal
- 684fae3e Copy old content to new buf when resizing
- 5d915baa Use Screen::resize instead of CellBuffer::resize
- 6a66afe9 Make add contact dialog scrollable on overflow
- aa5737a0 Prevent drawing pager on embedded mode
- 07072e2e Prevent panic if envelope is deleted
- 8ddd673d Update all mailboxes
- 3691cd29 Send EnvelopeUpdate event after self.collection.update_flags()
- 1fcb1d59 Remove rerun when build.rs changes
- 933bf157 Ack \ as an atom
- a1cbb198 Return Results instead of panicking
- b5ddc397 Remove unwrap() from get_events() loop
Changes
=======
- 61a0c3c2 Do not clear selection after action
- 9af284b8 Don't hide unread count for mailboxes that are partly truncated
- 35408b16 Run pager filter asynchronously
- e80ea9c9 Changed default manpage install path
- 742f038f Move sent_mailbox to settings
- 86bbf1ea Refresh NotmuchMailbox counts when setting flags
- f0866a39 Make config error more user-friendly
- 11f3077b Add more possible values for manpage names
- 1eca34b3 Set lowest priority to shortcut command UIEvents
- 484712b0 Check for unrecoverable errors in is_online
- 8ec6f220 Use ShellExpandTrait::expand in more user-provided paths
Refactoring
===========
- 0500e451 Add missing EnvelopeRemove event handler
- ab14f819 Make write_string_to_grid a CellBuffer method
- e0adcdfe Move rest of methods under CellBuffer
- 0a74c7d0 Overhaul refactor
- 3b4acc15 Add tests
- 7eedd860 Remove address_list! macro
- f3e85738 Move build.rs scripts to build directory
- 77325486 Remove on-push hooks for actions w/ run on-pr
- 08518e1c Remove obsolete position.rs module
- ddab3179 Move tests to tests module
- 79520068 Remove doctests, add tests module
- 4e7b6656 Sqlite caching refactor
- b5fd3f57 Make self.view an Option
- a3aaec38 Remove unused imports
- 11a0586d Remove num_cpus dependency
- 8f3dee9b Extract mod manpages to standalone file
- 89c7972e Add suggestions to BadValue variant
- 35a9f33a Extract common FlagString logic
- 1b0bdd0a Split queries and mailbox into submodules
- 506ae9f5 Add ErrorKind::LinkedLibrary variant
- ebe1b3da Wrap *mut struct fields in NonNull<_>
- ca7d7bb9 Use message freeze/thaw for flag changes
- 4026e254 Add some doc comments
- 808aa494 Rename text_processing to text for the whole brevity thing
- bebb473d Derive extra traits for enums
- ab1b946f Don't print details if it's an empty string.
- f685726e Add backtrace field to ParsingError
- 73d5b24e Merge integration tests in one crate
- 31401fa3 Add LazyCountSet::contains method
- 0270db01 From<&[u8]> -> From<B: AsRef<[u9]>>
- 873a67d0 Replace erroneous use of set_err_kind with set_kind
- 51e3f163 Use Url instead of String in deserializing
- 8014af25 Reduce debug prints
- f31b5c40 Don't print raw bytes as escaped unicode
- 41e965b8 Split mbox/job stuff in submodules
- ec01a441 Turn some sync connections to unsync
- 3e914465 Store children process metadata
- c53a32de Re-enables horizontal thread view
- 36b7c00b Put doc text type names and co. in backtics
- 634bd191 Convert log prints to traces
- 1048ce68 Add hostname() utility function
- 7645ff1b Rename write_string{to_grid,}
- c2ae19d1 Return Option from current_pos
- b61fc3ab Add HelpView struct for shortcuts widget
- 3495ffd6 Change UIEvent::Notification structure
- 23c15261 Abstract envelope view filters away
- 031d0f7d Add area.is_empty() checks in cell iterators
- e37997d6 Store Link URL value in Link type
- b6f769b2 Add field names to row_attr! bool values
- 0da97dd8 Check row_updates in is_dirty()
- 6506fffb Rewrite email flag modifications
- 23507932 Update cache on set_flags
- 470cae6b Update thread cache on email flag modifications
- 84f3641e Re-add on-screen message display
- 54d21f25 Re-add contact list and editor support
- 458258e1 Re-enable compact listing style
- 1c1be7d6 Add display_name(), display_slice(), display_name_slice() methods
- 5dd71ef1 Upgrade JobsView component to new TUI API
- b5cc2a09 Upgrade MailboxManager component to new TUI API
- ed8a5de2 Re-enable EditAttachments component
- 77a8d9e2 Make ModSequence publicly accessible
- 64898a05 Make UIDStore constructor pub
Documentation
=============
- e4818803 Various manpage touchups and URL updates
- 38bca8f8 Mention use_oauth2=true for gmail oauth2
- 660022ce Add mailaddr.7 manpage
- c5e9e676 Add historical-manpages dir
- 5afc0785 Update README.md, DEVELOPMENT.md and create BUILD.md
- d018f07a Retouch manual pages
- 3adba40e Add macos manpage mirror url
Packaging
=========
- cd2ba80f Update metadata
- 5f8d7c80 Update deb-dist target command with author metadata
- 59c99fdc Update debian package metadata
- 97eb6363 Add dpkg --print-architecture to deb filename
- 7412c238 Bump meli version to 0.8.5-rc.3
- 500fe7f7 Update CHANGELOG.md
- 5ff4e8ae Run builds.yaml when any manifest file changes
- 0a617410 Split test.yaml to test.yaml and lints.yaml
- 3ba1603a Add manifest file only lints workflow
- 1617212c Add scripts/check_debian_changelog.sh lint
- c41f35fd Use actions/checkout@v3
- 876616d4 Use actions/upload-artifact@v3
- 2419f4bd Add debian package build workflow
- 10c3b0ea Bump version to 0.8.5-rc.1
- d16afc7d Bump version to 0.8.5-rc.2
- da251455 Bump meli version to 0.8.5-rc.2
Miscellaneous Tasks
===================
- c4344529 Add .git-blame-ignore-revs file
- f70496f1 Add codemeta.json
- b3079715 Disable flakey test_smtp()
- 8a95febb Set debuginfo=0 in test/lint builds
- 81d1c053 Add mandoc_lint.sh
- 8de8addd Add cfg for musl builds
- 70fc2b45 Update nix dependency to 0.27
- fd64fe0b Update codeberg.org URL
- 30a3205e Add clippy::doc_markdown
- c7aee725 Add clippy::doc_markdown
- b8b24282 Update all instances of old domains with meli-email.org
- ae96038f Make unicode-segmentation a hard dependency
- 255e9376 Update linkify dep from 0.8.1 to 0.10.0
- dedee908 Update notify dep from 4.0.17 to 6.1.1
- c1c41c91 Update README.md and add Codeberg mirror
- 71f3ffe7 Update Makefile
- 63a63253 Use type alias for c_char
- c751b2e8 Re-enable conversations listing style
- 3a709794 Update minimum rust version from 1.65.0 to 1.68.2
- f900dbea Use cargo-derivefmt to sort derives alphabetically
- e19f3e57 Cargo-sort all Cargo.toml files
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 05 May 2024 18:46:42 +0300
meli (0.8.5-rc.3-1) bookworm; urgency=low
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 10 Dec 2023 15:22:18 +0000
* Update to 0.8.5-rc.3
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 10 Dec 2023 15:22:18 +0000
meli (0.8.5-rc.2-1) bookworm; urgency=low
* Update to 0.8.5-rc.2
-- Manos Pitsidianakis <manos@pitsidianak.is> Mon, 4 Dec 2023 19:34:00 +0200
meli (0.8.4-1) bookworm; urgency=low
* Update to 0.8.4
-- Manos Pitsidianakis <manos@pitsidianak.is> Mon, 27 Nov 2023 19:34:00 +0200
meli (0.7.2-1) bullseye; urgency=low
Added
@ -35,7 +258,7 @@ meli (0.7.1-1) bullseye; urgency=low
- melib/nntp: implement refresh
- melib/nntp: update total/new counters on new articles
- melib/nntp: implement NNTP posting
- configs: throw error on extra unusued conf flags in some imap/nntp
- configs: throw error on extra unused conf flags in some imap/nntp
- configs: throw error on missing `composing` section with explanation
Fixed

6
debian/control vendored
View File

@ -5,9 +5,9 @@ Maintainer: Manos Pitsidianakis <manos@pitsidianak.is>
Build-Depends: debhelper-compat (=13), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
Standards-Version: 4.1.4
Rules-Requires-Root: no
Vcs-Git: https://git.meliemail.org/meli/meli.git
Vcs-Browser: https://git.meliemail.org/meli/meli
Homepage: https://meliemail.org
Vcs-Git: https://git.meli-email.org/meli/meli.git
Vcs-Browser: https://git.meli-email.org/meli/meli
Homepage: https://meli-email.org
Package: meli
Architecture: any

2
debian/copyright vendored
View File

@ -1,6 +1,6 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: meli
Source: https://git.meliemail.org/meli/meli
Source: https://git.meli-email.org/meli/meli
#
# Please double check copyright with the licensecheck(1) command.

View File

@ -2,6 +2,6 @@ WARNING: This package is not distributed by debian, it was generated from the so
Please do not report bugs to debian, but to the appropriate issue tracker for meli:
- https://git.meliemail.org/meli/meli/issues
- Send e-mail to the mailing list, "meli general" <meli-general@meliemail.org>
https://lists.meliemail.org/list/meli-general/
- https://git.meli-email.org/meli/meli/issues
- Send e-mail to the mailing list, "meli general" <meli-general@meli-email.org>
https://lists.meli-email.org/list/meli-general/

View File

@ -14,10 +14,7 @@ path = "fuzz_targets/envelope_parse.rs"
[dependencies]
libfuzzer-sys = "0.3"
[dependencies.melib]
path = "../melib"
features = ["unicode-algorithms"]
melib = { path = "../melib" }
# Prevent this from interfering with workspaces
[workspace]

View File

@ -1,14 +1,14 @@
[package]
name = "meli"
version = "0.8.5-rc.3"
version = "0.8.5"
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
edition = "2021"
rust-version = "1.68.2"
rust-version = "1.70.0"
license = "GPL-3.0-or-later"
readme = "README.md"
description = "terminal e-mail client"
homepage = "https://meli.delivery"
repository = "https://git.meli.delivery/meli/meli.git"
homepage = "https://meli-email.org"
repository = "https://git.meli-email.org/meli/meli.git"
keywords = ["mail", "mua", "maildir", "terminal", "imap"]
categories = ["command-line-utilities", "email"]
default-run = "meli"
@ -30,11 +30,9 @@ futures = "0.3.5"
indexmap = { version = "^1.6", features = ["serde-1"] }
libc = { version = "0.2.125", default-features = false, features = ["extra_traits"] }
libz-sys = { version = "1.1", features = ["static"], optional = true }
linkify = { version = "^0.8", default-features = false }
melib = { path = "../melib", version = "0.8.5-rc.3" }
nix = { version = "^0.24", default-features = false }
notify = { version = "4.0.1", default-features = false } # >:c
num_cpus = "1.12.0"
linkify = { version = "^0.10", default-features = false }
melib = { path = "../melib", version = "0.8.5", features = [] }
nix = { version = "0.27", default-features = false, features = ["signal", "poll", "term", "ioctl", "process"] }
serde = "1.0.71"
serde_derive = "1.0.71"
serde_json = "1.0"
@ -44,7 +42,7 @@ smallvec = { version = "^1.5.0", features = ["serde"] }
structopt = { version = "0.3.14", default-features = false }
svg_crate = { version = "^0.13", optional = true, package = "svg" }
termion = { version = "1.5.1", default-features = false }
toml = { version = "0.5.6", default-features = false, features = ["preserve_order"] }
toml = { version = "0.8", default-features = false, features = ["display","preserve_order","parse"] }
xdg = "2.1.0"
[dependencies.pcre2]
@ -54,7 +52,7 @@ version = "0.2.3"
optional = true
[features]
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "text-processing", "static"]
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "static"]
notmuch = ["melib/notmuch"]
jmap = ["melib/jmap"]
sqlite3 = ["melib/sqlite3"]
@ -64,7 +62,6 @@ regexp = ["dep:pcre2"]
dbus-notifications = ["dep:notify-rust"]
cli-docs = ["dep:flate2"]
svgscreenshot = ["dep:svg_crate"]
text-processing = ["melib/unicode-algorithms"]
gpgme = ["melib/gpgme"]
# Static / vendoring features.
tls-static = ["melib/tls-static"]

View File

@ -25,7 +25,6 @@ extern crate syn;
include!("config_macros.rs");
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/conf/.rebuild.overrides.rs");
override_derive(&[
("src/conf/pager.rs", "PagerSettings"),

View File

@ -56,7 +56,7 @@ pub(crate) fn override_derive(filenames: &[(&str, &str)]) {
#![allow(clippy::derivable_impls)]
//! This module is automatically generated by config_macros.rs.
//! This module is automatically generated by `config_macros.rs`.
use super::*;
use melib::HeaderName;
@ -192,7 +192,7 @@ use melib::HeaderName;
#(#attrs_tokens)*
impl Default for #override_ident {
fn default() -> Self {
#override_ident {
Self {
#(#field_idents: None),*
}
}

Binary file not shown.

View File

@ -0,0 +1,354 @@
'\" t
.\"<!-- Copyright 1998 - 2007 Double Precision, Inc. See COPYING for -->
.\"<!-- distribution information. -->
.\" Title: maildir
.\" Author: Sam Varshavchik
.\" Generator: DocBook XSL Stylesheets vsnapshot <http://docbook.sf.net/>
.\" Date: 07/24/2017
.\" Manual: Double Precision, Inc.
.\" Source: Courier Mail Server
.\" Language: English
.\"
.TH "MAILDIR" "5" "07/24/2017" "Courier Mail Server" "Double Precision, Inc\&."
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
.\" disable hyphenation
.nh
.\" disable justification (adjust text to left margin only)
.ad l
.\" -----------------------------------------------------------------
.\" * MAIN CONTENT STARTS HERE *
.\" -----------------------------------------------------------------
.SH "NAME"
maildir \- E\-mail directory
.SH "SYNOPSIS"
.sp
$HOME/Maildir
.SH "DESCRIPTION"
.PP
A
\(lqMaildir\(rq
is a structured directory that holds E\-mail messages\&. Maildirs were first implemented by the
Qmail
mail server\&. Qmail\*(Aqs maildirs were a simple data structure, nothing more than a single collection of E\-mail messages\&. The
Courier
mail server builds upon
Qmail\*(Aqs maildirs to provide extended functionality, such as folders and quotas\&. This document describes the
Courier
mail server\*(Aqs extended maildirs, without explicitly identifying The
Courier
mail server\-specific extensions\&. See
\fBmaildir\fR(5)
in Qmail\*(Aqs documentation for the original definition of maildirs\&.
.PP
Traditionally, E\-mail folders were saved as plain text files, called
\(lqmboxes\(rq\&. Mboxes have known limitations\&. Only one application can use an mbox at the same time\&. Locking is required in order to allow simultaneous concurrent access by different applications\&. Locking is often problematic, and not very reliable in network\-based filesystem requirements\&. Some network\-based filesystems don\*(Aqt offer any reliable locking mechanism at all\&. Furthermore, even bulletproof locking won\*(Aqt prevent occasional mbox corruption\&. A process can be killed or terminated in the middle of updating an mbox\&. This will likely result in corruption, and a loss of most messages in the mbox\&.
.PP
Maildirs allow multiple concurrent access by different applications\&. Maildirs do not require locking\&. Multiple applications can update a maildir at the same time, without stepping on each other\*(Aqs feet\&.
.SS "Maildir contents"
.PP
A
\(lqmaildir\(rq
is a directory that\*(Aqs created by
\m[blue]\fB\fBmaildirmake\fR(1)\fR\m[]\&\s-2\u[1]\d\s+2\&. Naturally, maildirs should not have any group or world permissions, unless you want other people to read your mail\&. A maildir contains three subdirectories:
tmp,
new, and
cur\&. These three subdirectories comprise the primary folder, where new mail is delivered by the system\&.
.PP
Folders are additional subdirectories in the maildir whose names begin with a period: such as
\&.Drafts
or
\&.Sent\&. Each folder itself contains the same three subdirectories,
tmp,
new, and
cur, and an additional zero\-length file named
maildirfolder, whose purpose is to inform any mail delivery agent that it\*(Aqs really delivering to a folder, and that the mail delivery agent should look in the parent directory for any maildir\-related information\&.
.PP
Folders are not physically nested\&. A folder subdirectory, such as
\&.Sent
does not itself contain any subfolders\&. The main maildir contains a single, flat list of subfolders\&. These folders are logically nested, and periods serve to separate folder hierarchies\&. For example,
\&.Sent\&.2002
is considered to be a subfolder called
\(lq2002\(rq
which is a subfolder of
\(lqSent\(rq\&.
.sp
.it 1 an-trap
.nr an-no-space-flag 1
.nr an-break-flag 1
.br
.ps +1
\fBFolder name encoding\fR
.RS 4
.PP
Folder names can contain any Unicode character, except for control characters\&. US\-ASCII characters, U+0x0020 \- U+0x007F, except for the period, forward\-slash, and ampersand characters (U+0x002E, U+0x002F, and U+0x0026) represent themselves\&. The ampersand is represent by the two character sequence
\(lq&\-\(rq\&. The period, forward slash, and non US\-ASCII Unicode characters are represented using the UTF\-7 character set, and encoded with a modified form of base64\-encoding\&.
.PP
The
\(lq&\(rq
character starts the modified base64\-encoded sequence; the sequence is terminated by the
\(lq\-\(rq
character\&. The sequence of 16\-bit Unicode characters is written in big\-endian order, and encoded using the base64\-encoding method described in section 5\&.2 of
\m[blue]\fBRFC 1521\fR\m[]\&\s-2\u[2]\d\s+2, with the following modifications:
.sp
.RS 4
.ie n \{\
\h'-04'\(bu\h'+03'\c
.\}
.el \{\
.sp -1
.IP \(bu 2.3
.\}
The
\(lq=\(rq
padding character is omitted\&. When decoding, an incomplete 16\-bit character is discarded\&.
.RE
.sp
.RS 4
.ie n \{\
\h'-04'\(bu\h'+03'\c
.\}
.el \{\
.sp -1
.IP \(bu 2.3
.\}
The comma character,
\(lq,\(rq
is used in place of the
\(lq/\(rq
character in the base64 alphabet\&.
.RE
.PP
For example, the word
\(lqResume\(rq
with both "e"s being the e\-acute character, U+0x00e9, is encoded as
\(lqR&AOk\-sum&AOk\-\(rq
(so a folder of that name would be a maildir subdirectory called
\(lq\&.R&AOk\-sum&AOk\-\(rq)\&.
.RE
.sp
.it 1 an-trap
.nr an-no-space-flag 1
.nr an-break-flag 1
.br
.ps +1
\fBOther maildir contents\fR
.RS 4
.PP
Software that uses maildirs may also create additional files besides the
tmp,
new, and
cur
subdirectories \-\- in the main maildir or a subfolder \-\- for its own purposes\&.
.RE
.SS "Messages"
.PP
E\-mail messages are stored in separate, individual files, one E\-mail message per file\&. The
tmp
subdirectory temporarily stores E\-mail messages that are in the process of being delivered to this maildir\&.
tmp
may also store other kinds of temporary files, as long as they are created in the same way that message files are created in
tmp\&. The
new
subdirectory stores messages that have been delivered to this maildir, but have not yet been seen by any mail application\&. The
cur
subdirectory stores messages that have already been seen by mail applications\&.
.SS "Adding new mail to maildirs"
.PP
The following process delivers a new message to the maildir:
.PP
A new unique filename is created using one of two possible forms:
\(lqtime\&.MusecPpid\&.host\(rq, or
\(lqtime\&.MusecPpid_unique\&.host\(rq\&.
\(lqtime\(rq
and
\(lqusec\(rq
is the current system time, obtained from
\fBgettimeofday\fR(2)\&.
\(lqpid\(rq
is the process number of the process that is delivering this message to the maildir\&.
\(lqhost\(rq
is the name of the machine where the mail is being delivered\&. In the event that the same process creates multiple messages, a suffix unique to each message is appended to the process id; preferrably an underscore, followed by an increasing counter\&. This applies whether messages created by a process are all added to the same, or different, maildirs\&. This protocol allows multiple processes running on multiple machines on the same network to simultaneously create new messages without stomping on each other\&.
.PP
The filename created in the previous step is checked for existence by executing the
\fBstat\fR(2)
system call\&. If
\fBstat\fR(2)
results in ANYTHING OTHER than the system error
ENOENT, the process must sleep for two seconds, then go back and create another unique filename\&. This is an extra step to insure that each new message has a completely unique filename\&.
.PP
Other applications that wish to use
tmp
for temporary storage should observe the same protocol (but see READING MAIL FROM MAILDIRS below, because old files in
tmp
will be eventually deleted)\&.
.PP
If the
\fBstat\fR(2)
system call returned
ENOENT, the process may proceed to create the file in the
tmp
subdirectory, and save the entire message in the new file\&. The message saved MUST NOT have the
\(lqFrom_\(rq
header that is used to mboxes\&. The message also MUST NOT have any
\(lqFrom_\(rq
lines in the contents of the message prefixed by the
\(lq>\(rq
character\&.
.PP
When saving the message, the number of bytes returned by the
\fBwrite\fR(2)
system call must be checked, in order to make sure that the complete message has been written out\&.
.PP
After the message is saved, the file descriptor is
\fBfstat\fR(2)\-ed\&. The file\*(Aqs device number, inode number, and the its byte size, are saved\&. The file is closed and is then immediately moved/renamed into the
new
subdirectory\&. The name of the file in
new
should be
\(lqtime\&.MusecPpidVdevIino\&.host,S=\fIcnt\fR\(rq, or
\(lqtime\&.MusecPpidVdevIino_unique\&.host,S=\fIcnt\fR\(rq\&.
\(lqdev\(rq
is the message\*(Aqs device number,
\(lqino\(rq
is the message\*(Aqs inode number (from the previous
\fBfstat\fR(2)
call); and
\(lqcnt\(rq
is the message\*(Aqs size, in bytes\&.
.PP
The
\(lq,S=\fIcnt\fR\(rq
part optimizes the
\m[blue]\fBCourier\fR\m[]\&\s-2\u[3]\d\s+2
mail server\*(Aqs maildir quota enhancement; it allows the size of all the mail stored in the maildir to be added up without issuing the
\fBstat\fR(2)
system call for each individual message (this can be quite a performance drain with certain network filesystems)\&.
.SS "READING MAIL FROM MAILDIRS"
.PP
Applications that read mail from maildirs should do it in the following order:
.PP
When opening a maildir or a maildir folder, read the
tmp
subdirectory and delete any files in there that are at least 36 hours old\&.
.PP
Look for new messages in the
new
subdirectory\&. Rename
\fInew/filename\fR, as
\fIcur/filename:2,info\fR\&. Here,
\fIinfo\fR
represents the state of the message, and it consists of zero or more boolean flags chosen from the following:
\(lqD\(rq
\- this is a \*(Aqdraft\*(Aq message,
\(lqR\(rq
\- this message has been replied to,
\(lqS\(rq
\- this message has been viewed (seen),
\(lqT\(rq
\- this message has been marked to be deleted (trashed), but is not yet removed (messages are removed from maildirs simply by deleting their file),
\(lqF\(rq
\- this message has been marked by the user, for some purpose\&. These flags must be stored in alphabetical order\&. New messages contain only the
:2,
suffix, with no flags, indicating that the messages were not seen, replied, marked, or deleted\&.
.PP
Maildirs may have maximum size quotas defined, but these quotas are purely voluntary\&. If you need to implement mandatory quotas, you should use any quota facilities provided by the underlying filesystem that is used to store the maildirs\&. The maildir quota enhancement is designed to be used in certain situations where filesystem\-based quotas cannot be used for some reason\&. The implementation is designed to avoid the use of any locking\&. As such, at certain times the calculated quota may be imprecise, and certain anomalous situations may result in the maildir actually going over the stated quota\&. One such situation would be when applications create messages without updating the quota estimate for the maildir\&. Eventually it will be precisely recalculated, but wherever possible new messages should be created in compliance with the voluntary quota protocol\&.
.PP
The voluntary quota protocol involves some additional procedures that must be followed when creating or deleting messages within a given maildir or its subfolders\&. The
\m[blue]\fB\fBdeliverquota\fR(8)\fR\m[]\&\s-2\u[4]\d\s+2
command is a tiny application that delivers a single message to a maildir using the voluntary quota protocol, and hopefully it can be used as a measure of last resort\&. Alternatively, applications can use the
libmaildir\&.a
library to handle all the low\-level dirty details for them\&. The voluntary quota enhancement is described in the
\m[blue]\fB\fBmaildirquota\fR(7)\fR\m[]\&\s-2\u[5]\d\s+2
man page\&.
.SS "Maildir Quotas"
.PP
This is a voluntary mechanism for enforcing "loose" quotas on the maximum sizes of maildirs\&. This mechanism is enforced in software, and not by the operating system\&. Therefore it is only effective as long as the maildirs themselves are not directly accessible by their users, since this mechanism is trivially disabled\&.
.PP
If possible, operating system\-enforced quotas are preferrable\&. Where operating system quota enforcement is not available, or not possible, this voluntary quota enforcement mechanism might be an acceptable compromise\&. Since it\*(Aqs enforced in software, all software that modifies or accesses the maildirs is required to voluntary obey and enforce a quota\&. The voluntary quota implementation is flexible enough to allow non quota\-aware applications to also access the maildirs, without any drastic consequences\&. There will be some non\-drastic consequences, though\&. Of course, non quota\-aware applications will not enforce any defined quotas\&. Furthermore, this voluntary maildir quota mechanism works by estimating the current size of the maildir, with periodic exact recalculation\&. Obviously non quota\-aware maildir applications will not update the maildir size estimation, so the estimate will be thrown off for some period of time, until the next recalculation\&.
.PP
This voluntary quota mechanism is designed to be a reasonable compromise between effectiveness, and performance\&. The entire purpose of using maildir\-based mail storage is to avoid any kind of locking, and to permit parallel access to mail by multiple applications\&. In order to compute the exact size of a maildir, the maildir must be locked somehow to prevent any modifications while its contents are added up\&. Obviously something like that defeats the original purpose of using maildirs, therefore the voluntary quota mechanism does not use locking, and that\*(Aqs why the current recorded maildir size is always considered to be an estimate\&. Regular size recalculations will compensate for any occasional race conditions that result in the estimate to be thrown off\&.
.PP
A quota for an existing maildir is installed by running maildirmake with the
\-q
option, and naming an existing maildir\&. The
\-q
option takes a parameter,
\fIquota\fR, which is a comma\-separated list of quota specifications\&. A quota specification consists of a number followed by either \*(AqS\*(Aq, indicating the maximum message size in bytes, or \*(AqC\*(Aq, maximum number of messages\&. For example:
.sp
.if n \{\
.RS 4
.\}
.nf
\fBmaildirmake \-q 5000000S,1000C \&./Maildir\fR
.fi
.if n \{\
.RE
.\}
.PP
This sets the quota to 5,000,000 bytes or 1000 messages, whichever comes first\&.
.sp
.if n \{\
.RS 4
.\}
.nf
\fBmaildirmake \-q 1000000S \&./Maildir\fR
.fi
.if n \{\
.RE
.\}
.PP
This sets the quota to 1,000,000 bytes, without limiting the number of messages\&.
.PP
A quota of an existing maildir can be changed by rerunning the
\fBmaildirmake\fR
command with a new
\-q
option\&. To delete a quota entirely, delete the
\fIMaildir\fR/maildirsize
file\&.
.SH "SEE ALSO"
.PP
\m[blue]\fB\fBmaildirmake\fR(1)\fR\m[]\&\s-2\u[1]\d\s+2\&.
.SH "AUTHOR"
.PP
\fBSam Varshavchik\fR
.RS 4
Author
.RE
.SH "NOTES"
.IP " 1." 4
\fBmaildirmake\fR(1)
.RS 4
\%http://www.courier-mta.org/maildirmake.html
.RE
.IP " 2." 4
RFC 1521
.RS 4
\%http://www.rfc-editor.org/rfc/rfc1521.txt
.RE
.IP " 3." 4
Courier
.RS 4
\%http://www.courier-mta.org
.RE
.IP " 4." 4
\fBdeliverquota\fR(8)
.RS 4
\%http://www.courier-mta.org/deliverquota.html
.RE
.IP " 5." 4
\fBmaildirquota\fR(7)
.RS 4
\%http://www.courier-mta.org/maildirquota.html
.RE

View File

@ -0,0 +1,187 @@
'\" t
.\" -*-nroff-*-
.\"
.\" Copyright (C) 2000 Thomas Roessler <roessler@does-not-exist.org>
.\"
.\" This document is in the public domain and may be distributed and
.\" changed arbitrarily.
.\"
.TH mbox 5 "February 19th, 2002" Unix "User Manuals"
.\"
.SH NAME
mbox \- Format for mail message storage.
.\"
.SH DESCRIPTION
This document describes the format traditionally used by Unix hosts
to store mail messages locally.
.B mbox
files typically reside in the system's mail spool, under various
names in users' Mail directories, and under the name
.B mbox
in users' home directories.
.PP
An
.B mbox
is a text file containing an arbitrary number of e-mail messages.
Each message consists of a postmark, followed by an e-mail message
formatted according to \fBRFC822\fP, \fBRFC2822\fP. The file format
is line-oriented. Lines are separated by line feed characters (ASCII 10).
.PP
A postmark line consists of the four characters "From", followed by
a space character, followed by the message's envelope sender
address, followed by whitespace, and followed by a time stamp. This
line is often called From_ line.
.PP
The sender address is expected to be
.B addr-spec
as defined in \fBRFC2822\fP 3.4.1. The date is expected to be
.B date-time
as output by
.BR asctime (3).
For compatibility reasons with legacy software, two-digit years
greater than or equal to 70 should be interpreted as the years
1970+, while two-digit years less than 70 should be interpreted as
the years 2000-2069. Software reading files in this format should
also be prepared to accept non-numeric timezone information such as
"CET DST" for Central European Time, daylight saving time.
.PP
Example:
.IP "" 1
>From example@example.com Fri Jun 23 02:56:55 2000
.PP
In order to avoid misinterpretation of lines in message bodies
which begin with the four characters "From", followed by a space
character, the mail delivery agent must quote any occurrence
of "From " at the start of a body line.
.sp
There are two different quoting schemes, the first (\fBMBOXO\fP) only
quotes plain "From " lines in the body by prepending a '>' to the
line; the second (\fBMBOXRD\fP) also quotes already quoted "From "
lines by prepending a '>' (i.e. ">From ", ">>From ", ...). The later
has the advantage that lines like
.IP "" 1
>From the command line you can use the '\-p' option
.PP
aren't dequoted wrongly as a \fBMBOXRD\fP-MDA would turn the line
into
.IP "" 1
>>From the command line you can use the '\-p' option
.PP
before storing it. Besides \fBMBOXO\fP and \fBMBOXRD\fP there is also
\fBMBOXCL\fP which is \fBMBOXO\fP with a "Content-Length:"\-field with the
number of bytes in the message body; some MUAs (like
.BR mutt (1))
do automatically transform \fBMBOXO\fP mailboxes into \fBMBOXCL\fP ones when
ever they write them back as \fBMBOXCL\fP can be read by any \fBMBOXO\fP-MUA
without any problems.
.PP
If the modification-time (usually determined via
.BR stat (2))
of a nonempty
.B mbox
file is greater than the access-time the file has new mail. Many MUAs
place a Status: header in each message to indicate which messages have
already been read.
.\"
.SH LOCKING
Since
.B mbox
files are frequently accessed by multiple programs in parallel,
.B mbox
files should generally not be accessed without locking.
.PP
Three different locking mechanisms (and combinations thereof) are in
general use:
.IP "\(bu"
.BR fcntl (2)
locking is mostly used on recent, POSIX-compliant systems. Use of
this locking method is, in particular, advisable if
.B mbox
files are accessed through the Network File System (NFS), since it
seems the only way to reliably invalidate NFS clients' caches.
.IP "\(bu"
.BR flock (2)
locking is mostly used on BSD-based systems.
.IP "\(bu"
Dotlocking is used on all kinds of systems. In order to lock an
.B mbox
file named \fIfolder\fR, an application first creates a temporary file
with a unique name in the directory in which the
\fIfolder\fR resides. The application then tries to use the
.BR link (2)
system call to create a hard link named \fIfolder.lock\fR
to the temporary file. The success of the
.BR link (2)
system call should be additionally verified using
.BR stat (2)
calls. If the link has succeeded, the mail folder is considered
dotlocked. The temporary file can then safely be unlinked.
.IP ""
In order to release the lock, an application just unlinks the
\fIfolder.lock\fR file.
.PP
If multiple methods are combined, implementors should make sure to
use the non-blocking variants of the
.BR fcntl (2)
and
.BR flock (2)
system calls in order to avoid deadlocks.
.PP
If multiple methods are combined, an
.B mbox
file must not be considered to have been successfully locked before
all individual locks were obtained. When one of the individual
locking methods fails, an application should release all locks it
acquired successfully, and restart the entire locking procedure from
the beginning, after a suitable delay.
.PP
The locking mechanism used on a particular system is a matter of
local policy, and should be consistently used by all applications
installed on the system which access
.B mbox
files. Failure to do so may result in loss of e-mail data, and in
corrupted
.B mbox
files.
.\"
.SH FILES
.IR /var/spool/mail/$LOGNAME
.RS
\fB$LOGNAME\fP's incoming mail folder.
.RE
.PP
.IR $HOME/mbox
.RS
user's archived mail messages, in his \fB$HOME\fP directory.
.RE
.PP
.IR $HOME/Mail/
.RS
A directory in user's \fB$HOME\fP directory which is commonly used to hold
.B mbox
format folders.
.RE
.PP
.\"
.SH "SEE ALSO"
.BR mutt (1),
.BR fcntl (2),
.BR flock (2),
.BR link (2),
.BR stat (2),
.BR asctime (3),
.BR maildir (5),
.BR mmdf (5),
.BR RFC822 ,
.BR RFC976 ,
.BR RFC2822
.\"
.SH AUTHOR
Thomas Roessler <roessler@does-not-exist.org>, Urs Janssen <urs@tin.org>
.\"
.SH HISTORY
The
.B mbox
format occurred in Version 6 AT&T Unix.
.br
A variant of this format was documented in \fBRFC976\fP.

View File

@ -0,0 +1,235 @@
.TH mbox 5
.SH "NAME"
mbox \- file containing mail messages
.SH "INTRODUCTION"
The most common format for storage of mail messages is
.I mbox
format.
An
.I mbox
is a single file containing zero or more mail messages.
.SH "MESSAGE FORMAT"
A message encoded in
.I mbox
format begins with a
.B From_
line, continues with a series of
.B \fRnon-\fBFrom_
lines,
and ends with a blank line.
A
.B From_
line means any line that begins with the characters
F, r, o, m, space:
.EX
From god@heaven.af.mil Sat Jan 3 01:05:34 1996
.br
Return-Path: <god@heaven.af.mil>
.br
Delivered-To: djb@silverton.berkeley.edu
.br
Date: 3 Jan 1996 01:05:34 -0000
.br
From: God <god@heaven.af.mil>
.br
To: djb@silverton.berkeley.edu (D. J. Bernstein)
.br
.br
How's that mail system project coming along?
.br
.EE
The final line is a completely blank line (no spaces or tabs).
Notice that blank lines may also appear elsewhere in the message.
The
.B From_
line always looks like
.B From
.I envsender
.I date
.IR moreinfo .
.I envsender
is one word, without spaces or tabs;
it is usually the envelope sender of the message.
.I date
is the delivery date of the message.
It always contains exactly 24 characters in
.B asctime
format.
.I moreinfo
is optional; it may contain arbitrary information.
Between the
.B From_
line and the blank line is a message in RFC 822 format,
as described in
.BR qmail-header(5) ,
subject to
.B >From quoting
as described below.
.SH "HOW A MESSAGE IS DELIVERED"
Here is how a program appends a message to an
.I mbox
file.
It first creates a
.B From_
line given the message's envelope sender and the current date.
If the envelope sender is empty (i.e., if this is a bounce message),
the program uses
.B MAILER-DAEMON
instead.
If the envelope sender contains spaces, tabs, or newlines,
the program replaces them with hyphens.
The program then copies the message, applying
.B >From quoting
to each line.
.B >From quoting
ensures that the resulting lines are not
.B From_
lines:
the program prepends a
.B >
to any
.B From_
line,
.B >From_
line,
.B >>From_
line,
.B >>>From_
line,
etc.
Finally the program appends a blank line to the message.
If the last line of the message was a partial line,
it writes two newlines;
otherwise it writes one.
.SH "HOW A MESSAGE IS READ"
A reader scans through an
.I mbox
file looking for
.B From_
lines.
Any
.B From_
line marks the beginning of a message.
The reader should not attempt to take advantage of the fact that every
.B From_
line (past the beginning of the file)
is preceded by a blank line.
Once the reader finds a message,
it extracts a (possibly corrupted) envelope sender
and delivery date out of the
.B From_
line.
It then reads until the next
.B From_
line or end of file, whichever comes first.
It strips off the final blank line
and
deletes the
quoting of
.B >From_
lines and
.B >>From_
lines and so on.
The result is an RFC 822 message.
.SH "COMMON MBOX VARIANTS"
There are many variants of
.I mbox
format.
The variant described above is
.I mboxrd
format, popularized by Rahul Dhesi in June 1995.
The original
.I mboxo
format quotes only
.B From_
lines, not
.B >From_
lines.
As a result it is impossible to tell whether
.EX
From: djb@silverton.berkeley.edu (D. J. Bernstein)
.br
To: god@heaven.af.mil
.br
.br
>From now through August I'll be doing beta testing.
.br
Thanks for your interest.
.EE
was quoted in the original message.
An
.I mboxrd
reader will always strip off the quoting.
.I mboxcl
format is like
.I mboxo
format, but includes a Content-Length field with the
number of bytes in the message.
.I mboxcl2
format is like
.I mboxcl
but has no
.B >From
quoting.
These formats are used by SVR4 mailers.
.I mboxcl2
cannot be read safely by
.I mboxrd
readers.
.SH "UNSPECIFIED DETAILS"
There are many locking mechanisms for
.I mbox
files.
.B qmail-local
always uses
.B flock
on systems that have it, otherwise
.BR lockf .
The delivery date in a
.B From_
line does not specify a time zone.
.B qmail-local
always creates the delivery date in GMT
so that
.I mbox
files can be safely transported from one time zone to another.
If the mtime on a nonempty
.I mbox
file is greater than the atime,
the file has new mail.
If the mtime is smaller than the atime,
the new mail has been read.
If the atime equals the mtime,
there is no way to tell whether the file has new mail,
since
.B qmail-local
takes much less than a second to run.
One solution is for a mail reader to artificially set the
atime to the mtime plus 1.
Then the file has new mail if and only if the atime is
less than or equal to the mtime.
Some mail readers place
.B Status
fields in each message to indicate which messages have been read.
.SH "SEE ALSO"
maildir(5),
qmail-header(5),
qmail-local(8)

View File

@ -0,0 +1,239 @@
.TH maildir 5
.SH "NAME"
maildir \- directory for incoming mail messages
.SH "INTRODUCTION"
.I maildir
is a structure for
directories of incoming mail messages.
It solves the reliability problems that plague
.I mbox
files and
.I mh
folders.
.SH "RELIABILITY ISSUES"
A machine may crash while it is delivering a message.
For both
.I mbox
files and
.I mh
folders this means that the message will be silently truncated.
Even worse: for
.I mbox
format, if the message is truncated in the middle of a line,
it will be silently joined to the next message.
The mail transport agent will try again later to deliver the message,
but it is unacceptable that a corrupted message should show up at all.
In
.IR maildir ,
every message is guaranteed complete upon delivery.
A machine may have two programs simultaneously delivering mail
to the same user.
The
.I mbox
and
.I mh
formats require the programs to update a single central file.
If the programs do not use some locking mechanism,
the central file will be corrupted.
There are several
.I mbox
and
.I mh
locking mechanisms,
none of which work portably and reliably.
In contrast, in
.IR maildir ,
no locks are ever necessary.
Different delivery processes never touch the same file.
A user may try to delete messages from his mailbox at the same
moment that the machine delivers a new message.
For
.I mbox
and
.I mh
formats, the user's mail-reading program must know
what locking mechanism the mail-delivery programs use.
In contrast, in
.IR maildir ,
any delivered message
can be safely updated or deleted by a mail-reading program.
Many sites use Sun's
.B Network F\fPa\fBil\fPur\fBe System
(NFS),
presumably because the operating system vendor does not offer
anything else.
NFS exacerbates all of the above problems.
Some NFS implementations don't provide
.B any
reliable locking mechanism.
With
.I mbox
and
.I mh
formats,
if two machines deliver mail to the same user,
or if a user reads mail anywhere except the delivery machine,
the user's mail is at risk.
.I maildir
works without trouble over NFS.
.SH "THE MAILDIR STRUCTURE"
A directory in
.I maildir
format has three subdirectories,
all on the same filesystem:
.BR tmp ,
.BR new ,
and
.BR cur .
Each file in
.B new
is a newly delivered mail message.
The modification time of the file is the delivery date of the message.
The message is delivered
.I without
an extra UUCP-style
.B From_
line,
.I without
any
.B >From
quoting,
and
.I without
an extra blank line at the end.
The message is normally in RFC 822 format,
starting with a
.B Return-Path
line and a
.B Delivered-To
line,
but it could contain arbitrary binary data.
It might not even end with a newline.
Files in
.B cur
are just like files in
.BR new .
The big difference is that files in
.B cur
are no longer new mail:
they have been seen by the user's mail-reading program.
.SH "HOW A MESSAGE IS DELIVERED"
The
.B tmp
directory is used to ensure reliable delivery,
as discussed here.
A program delivers a mail message in six steps.
First, it
.B chdir()\fPs
to the
.I maildir
directory.
Second, it
.B stat()s
the name
.BR tmp/\fItime.pid.host ,
where
.I time
is the number of seconds since the beginning of 1970 GMT,
.I pid
is the program's process ID,
and
.I host
is the host name.
Third, if
.B stat()
returned anything other than ENOENT,
the program sleeps for two seconds, updates
.IR time ,
and tries the
.B stat()
again, a limited number of times.
Fourth, the program
creates
.BR tmp/\fItime.pid.host .
Fifth, the program
.I NFS-writes
the message to the file.
Sixth, the program
.BR link() s
the file to
.BR new/\fItime.pid.host .
At that instant the message has been successfully delivered.
The delivery program is required to start a 24-hour timer before
creating
.BR tmp/\fItime.pid.host ,
and to abort the delivery
if the timer expires.
Upon error, timeout, or normal completion,
the delivery program may attempt to
.B unlink()
.BR tmp/\fItime.pid.host .
.I NFS-writing
means
(1) as usual, checking the number of bytes returned from each
.B write()
call;
(2) calling
.B fsync()
and checking its return value;
(3) calling
.B close()
and checking its return value.
(Standard NFS implementations handle
.B fsync()
incorrectly
but make up for it by abusing
.BR close() .)
.SH "HOW A MESSAGE IS READ"
A mail reader operates as follows.
It looks through the
.B new
directory for new messages.
Say there is a new message,
.BR new/\fIunique .
The reader may freely display the contents of
.BR new/\fIunique ,
delete
.BR new/\fIunique ,
or rename
.B new/\fIunique
as
.BR cur/\fIunique:info .
See
.B http://pobox.com/~djb/proto/maildir.html
for the meaning of
.IR info .
The reader is also expected to look through the
.B tmp
directory and to clean up any old files found there.
A file in
.B tmp
may be safely removed if it
has not been accessed in 36 hours.
It is a good idea for readers to skip all filenames in
.B new
and
.B cur
starting with a dot.
Other than this, readers should not attempt to parse filenames.
.SH "ENVIRONMENT VARIABLES"
Mail readers supporting
.I maildir
use the
.B MAILDIR
environment variable
as the name of the user's primary mail directory.
.SH "SEE ALSO"
mbox(5),
qmail-local(8)

View File

@ -17,7 +17,8 @@
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.Dd November 11, 2022
.\".Dd November 11, 2022
.Dd March 10, 2024
.Dt MELI-THEMES 5
.Os
.Sh NAME
@ -31,15 +32,15 @@ comes with two themes,
.Ic dark
(default) and
.Ic light .
.sp
.Pp
Custom themes are defined as lists of key-values in the configuration files:
.Bl -bullet -compact
.Bl -item -compact -offset 2
.It
.Pa $XDG_CONFIG_HOME/meli/config.toml
.It
.Pa $XDG_CONFIG_HOME/meli/themes/*.toml
.El
.sp
.Pp
The application theme is defined in the configuration as follows:
.Bd -literal
[terminal]
@ -56,9 +57,9 @@ keys are settings for the
.Ic compact
mail listing style.
A setting contains three fields: fg for foreground color, bg for background color, and attrs for text attribute.
.sp
.Pp
.Dl \&"widget.key.label\&" = { fg = \&"Default\&", bg = \&"Default\&", attrs = \&"Default\&" }
.sp
.Pp
Each field contains a value, which may be either a color/attribute, a link (key name) or a valid alias.
An alias is a string starting with the \&"\&$\&" character and must be declared in advance in the
.Ic color_aliases
@ -69,10 +70,14 @@ An alias' value can be any valid value, including links and other aliases, as lo
In the case of a link the setting's real value depends on the value of the referred key.
This allows for defaults within a group of associated values.
Cyclic references in a theme results in an error:
.sp
.Pp
.Dl spooky theme contains a cycle: fg: mail.listing.compact.even -> mail.listing.compact.highlighted -> mail.listing.compact.odd -> mail.listing.compact.even
.Pp
Two themes are included by default, `light` and `dark`.
Two themes are included by default,
.Ql light
and
.Ql dark Ns
\&.
.Sh EXAMPLES
Specific settings from already defined themes can be overwritten:
.Bd -literal
@ -100,18 +105,18 @@ Custom themes can be included in your configuration files or be saved independen
.Pa $XDG_CONFIG_HOME/meli/themes/
directory as TOML files.
To start creating a theme right away, you can begin by editing the default theme keys and values:
.sp
.Pp
.Dl meli print-default-theme > ~/.config/meli/themes/new_theme.toml
.sp
.Pp
.Pa new_theme.toml
will now include all keys and values of the "dark" theme.
.sp
.Pp
.Dl meli print-loaded-themes
.sp
.Pp
will print all loaded themes with the links resolved.
.Sh VALID ATTRIBUTE VALUES
Case-sensitive.
.Bl -bullet -compact
.Bl -dash -compact
.It
"Default"
.It
@ -133,7 +138,7 @@ Any combo of the above separated by a bitwise XOR "\&|" eg "Dim | Italics"
.El
.Sh VALID COLOR VALUES
Color values are of type String with the following valid contents:
.Bl -bullet -compact
.Bl -dash -compact
.It
"Default" is the terminal default. (Case-sensitive)
.It
@ -146,8 +151,10 @@ Three character shorthand is also valid, e.g. #09c → #0099cc (Case-insensitive
name but with some modifications (for a full table see COLOR NAMES addendum) (Case-sensitive)
.El
.Sh NO COLOR
To completely disable ANSI colors, there are two options:
.Bl -bullet -compact
To completely disable
.Tn ANSI
colors, there are two options:
.Bl -dash -compact
.It
Set the
.Ic use_color
@ -157,17 +164,22 @@ option (section
.It
The
.Ev NO_COLOR
environmental variable, when present (regardless of its value), prevents the addition of ANSI color.
environmental variable, when present (regardless of its value), prevents the addition of
.Tn ANSI
color.
When the configuration value
.Ic use_color
is explicitly set to true by the user,
.Ev NO_COLOR
is ignored.
.El
.sp
In this mode, cursor locations (i.e., currently selected entries/items) will use the "reverse video" ANSI attribute to invert the terminal's default foreground/background colors.
.Pp
In this mode, cursor locations (i.e., currently selected entries/items) will use the
.Ql reverse video
.Tn ANSI
attribute to invert the terminal's default foreground/background colors.
.Sh VALID KEYS
.Bl -bullet -compact
.Bl -dash -compact
.It
theme_default
.It
@ -312,7 +324,7 @@ pager.highlight_search_current
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Aqua:14:_:Black:0
Aquamarine1:122:_:Maroon:1
Aquamarine2:86:_:Green:2
@ -348,7 +360,7 @@ DarkMagenta1:91:_:SpringGreen6:29
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
DarkOliveGreen1:192:_:Turquoise4:30
DarkOliveGreen2:155:_:DeepSkyBlue3:31
DarkOliveGreen3:191:_:DeepSkyBlue4:32
@ -384,7 +396,7 @@ DeepPink4:125:_:Grey37:59
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
DeepPink6:162:_:MediumPurple6:60
DeepPink7:89:_:SlateBlue2:61
DeepPink8:53:_:SlateBlue3:62
@ -420,7 +432,7 @@ Grey19:236:_:DeepPink7:89
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Grey23:237:_:DarkMagenta:90
Grey27:238:_:DarkMagenta1:91
Grey3:232:_:DarkViolet1:92
@ -456,7 +468,7 @@ HotPink2:169:_:LightGreen:119
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
HotPink3:132:_:LightGreen1:120
HotPink4:168:_:PaleGreen1:121
IndianRed:131:_:Aquamarine1:122
@ -492,7 +504,7 @@ LightSlateGrey:103:_:DarkOliveGreen6:149
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
LightSteelBlue:147:_:DarkSeaGreen6:150
LightSteelBlue1:189:_:DarkSeaGreen3:151
LightSteelBlue3:146:_:LightCyan3:152
@ -528,7 +540,7 @@ NavajoWhite3:144:_:LightGoldenrod3:179
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Navy:4:_:Tan:180
NavyBlue:17:_:MistyRose3:181
Olive:3:_:Thistle3:182
@ -564,7 +576,7 @@ Purple5:55:_:Salmon1:209
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Red:9:_:LightCoral:210
Red1:196:_:PaleVioletRed1:211
Red2:124:_:Orchid2:212
@ -600,7 +612,7 @@ Tan:180:_:Grey30:239
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Teal:6:_:Grey35:240
Thistle1:225:_:Grey39:241
Thistle3:182:_:Grey42:242
@ -621,16 +633,34 @@ Yellow6:148:_:Grey93:255
.Sh SEE ALSO
.Xr meli 1 ,
.Xr meli.conf 5
.Sh CONFORMING TO
TOML Standard v.0.5.0
.Lk https://toml.io/en/v0.5.0
.sp
.Lk https://no-color.org/
.Sh STANDARDS
.Bl -item -compact
.It
.Lk https://toml.io/en/v0.5.0 "TOML Standard v.0.5.0"
.It
.Lk https://no\-color.org/ "NO_COLOR: disabling ANSI color output by default"
.El
.Sh AUTHORS
Copyright 2017-2019
.An Manos Pitsidianakis Aq manos@pitsidianak.is
Copyright 2017\(en2024
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
.Pp
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind.
(See COPYING for full copyright and warranty notices.)
.sp
.Lk https://meli.delivery
.Po
See
.Pa COPYING
for full copyright and warranty notices.
.Pc
.Ss Links
.Bl -item -compact
.It
.Lk https://meli\-email.org "Website"
.It
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
.It
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
.It
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
.It
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
.El

View File

@ -17,6 +17,10 @@
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.de HorizontalRule
.\"\l'\n(.l\(ru1.25'
.sp
..
.de Shortcut
.Sm
.Aq \\$1
@ -40,12 +44,13 @@
.Ed
.sp
..
.Dd November 11, 2022
.\".Dd November 11, 2022
.Dd March 10, 2024
.Dt MELI 1
.Os
.Sh NAME
.Nm meli
.Nd terminal e-mail client
.Nd terminal e\-mail client
.Em μέλι
is the Greek word for honey
.Sh SYNOPSIS
@ -65,36 +70,44 @@ Create configuration file in
.Pa path
if given, or at
.Pa $XDG_CONFIG_HOME/meli/config.toml
.It Cm edit-config
Edit configuration files with
.Ev EDITOR
or
.Ev VISUAL Ns
\&.
.It Cm test-config Op Ar path
Test a configuration file for syntax issues or missing options.
.It Cm man Op Ar page
Print documentation page and exit (Piping to a pager is recommended).
.It Cm install-man Op Ar path
Install manual pages to the first location provided by
.Ar MANPATH
.Ev MANPATH
or
.Xr manpath 1 ,
unless you specify the directory as an argument.
.It Cm compiled-with
Print compile time feature flags of this binary.
.It Cm edit-config
Edit configuration files with
.Ev EDITOR
or
.Ev VISUAL Ns
\&.
.It Cm help
Prints help information or the help of the given subcommand(s).
.It Cm print-app-directories
Print all directories that
.Ns Nm
creates and uses.
.It Cm print-config-path
Print location of configuration file that will be loaded on normal app startup.
.It Cm print-default-theme
Print default theme keys and values in TOML syntax, to be used as a blueprint.
.It Cm print-loaded-themes
Print all loaded themes in TOML syntax.
.It Cm print-used-paths
Print all paths that are created and used.
.It Cm compiled-with
Print compile time feature flags of this binary.
.It Cm print-log-path
Print log file location.
.It Cm view
View mail from input file.
.El
.Sh DESCRIPTION
.Nm
is a terminal mail client aiming for extensive and user-frendly configurability.
is a terminal mail client aiming for extensive and user-friendly configurability.
.Bd -literal
^^ .-=-=-=-. ^^
^^ (`-=-=-=-=-`) ^^
@ -128,11 +141,28 @@ At any time, you may press
for a searchable list of all available actions and shortcuts, along with every possible setting and command that your version supports.
.Pp
The main visual navigation tool, the left-side sidebar may be toggled with
.ShortcutPeriod ` listing toggle_menu_visibility
.ShortcutPeriod \(ga listing toggle_menu_visibility
\&.
.Pp
Each mailbox may be viewed in 4 modes:
Plain views each mail individually, Threaded shows their thread relationship visually, Conversations collapses each thread of emails into a single entry, Compact shows one row per thread.
.Bl -dash -compact
.It
.Tg index-style-plain
.Em Plain
views each mail individually,
.It
.Tg index-style-threaded
.Em Threaded
shows their thread relationship visually,
.It
.Tg index-style-conversations
.Em Conversations
collapses each thread of e\-mails into a single entry,
.It
.Tg index-style-compact
.Em Compact
shows one row per thread.
.El
.Pp
If you're using a light color palette in your terminal, you should set
.Em theme = "light"
@ -148,6 +178,10 @@ See
for a more detailed tutorial on using
.Nm Ns
\&.
.Sh SHORTCUTS
See
.Xr meli.conf 5 SHORTCUTS
for shortcuts and their default values.
.Sh VIEWING MAIL
Open attachments by typing their index in the attachments list and then
.ShortcutPeriod a envelope_view open_attachment
@ -173,7 +207,7 @@ If the path provided is a directory, the attachment is saved with its filename s
If the 0th index is provided, the entire message is saved.
If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message-id.
.Sh SEARCH
Each e-mail storage backend has a default search method assigned.
Each e\-mail storage backend has a default search method assigned.
.Em IMAP
uses the SEARCH command,
.Em notmuch
@ -222,9 +256,8 @@ alias:
.Pc
String keywords with spaces must be quoted.
Quotes should always be escaped.
.sp
.Sy Important Notice about IMAP/JMAP
.sp
.Ss Important Notice about IMAP/JMAP
.HorizontalRule
To prevent downloading all your messages from your IMAP/JMAP server, don't set
.Em search_backend
to
@ -233,9 +266,10 @@ to
.Nm
will relay your queries to the IMAP server.
Expect a delay between query and response.
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable delay.
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable delay.
.Ss QUERY ABNF SYNTAX
.Bl -bullet
.HorizontalRule
.Bl -dash -compact
.It
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | alladdresses | subject | flags | has_attachments | query \&"or\&" query | query \&"and\&" query | not query
.It
@ -265,10 +299,23 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable
.It
.Li flags = \&"flags:\&" flag | \&"tags:\&" flag | \&"is:\&" flag
.El
.Sh FLAGS
.Nm
supports the basic maildir flags: passed, replied, seen, trashed, draft and flagged.
Flags can be searched with the
.Ns Ql flags:
prefix in a search query, and can be modified by
.Command flag set FLAG
and
.Command flag unset FLAG
.Sh TAGS
.Nm
supports tagging in notmuch and IMAP/JMAP backends.
Tags can be searched with the `tags:` or `flags:` prefix in a search query, and can be modified by
Tags can be searched with the
.Ns Ql tags:
or
.Ns Ql flags:
prefix in a search query, and can be modified by
.Command tag add TAG
and
.Command tag remove TAG
@ -289,7 +336,8 @@ To reply to a mail, press
\&.
Both these actions open the mail composer view in a new tab.
.Ss Editing text
.Bl -bullet -compact
.HorizontalRule
.Bl -dash -compact
.It
Edit the header fields by selecting with the arrow keys and pressing
.Shortcut Enter general focus_in_text_field
@ -332,12 +380,14 @@ and to resume editing press the
command again.
.El
.Ss Attachments
.HorizontalRule
Attachments may be handled with the
.Cm add-attachment Ns
,
.Cm remove-attachment
commands (see below).
.Ss Sending
.HorizontalRule
Finally, pressing
.Shortcut s composing send_mail
will send your message according to your settings
@ -355,6 +405,7 @@ On complete failure to save your draft or sent message it will be saved in your
.Em tmp
directory instead and you will be notified of its location.
.Ss Drafts
.HorizontalRule
To save your draft without sending it, issue
.Em COMMAND
.Cm close
@ -366,8 +417,7 @@ To open a draft for further editing, select your draft in the mail listing and p
.Sh CONTACTS
.Nm
supports three kinds of contact backends:
.sp
.Bl -enum -compact -offset indent
.Bl -enum -compact
.It
an internal format that gets saved under
.Pa $XDG_DATA_HOME/meli/account_name/addressbook Ns
@ -389,7 +439,7 @@ compatible alias file in the option
.sp
See
.Xr meli.conf 5 ACCOUNTS
for the complete account configuration values.
for the complete account contact configuration values.
.Sh MODES
.Bl -tag -compact -width 8n
.It NORMAL
@ -409,8 +459,9 @@ captures all input as text input, and is exited with
.Cm Esc
key.
.El
.Ss COMMAND Mode
.Sh COMMAND
.Ss Mail listing commands
.HorizontalRule
.Bl -tag -width 36n
.It Cm set Ar plain | threaded | compact | conversations
set the way mailboxes are displayed
@ -445,6 +496,8 @@ Escape exits search results.
select threads matching
.Ar STRING
query.
.It Cm clear-selection
Clear current selection.
.It Cm set seen, set unseen
Set seen status of message.
.It Cm import Ar FILEPATH Ar MAILBOX_PATH
@ -452,25 +505,26 @@ Import mail from file into given mailbox.
.It Cm copyto, moveto Ar MAILBOX_PATH
Copy or move to other mailbox.
.It Cm copyto, moveto Ar ACCOUNT Ar MAILBOX_PATH
Copy or move to another account's mailbox.
Copy or move to another account's mailbox.
.It Cm delete
Delete selected threads.
.It Cm export-mbox Ar FILEPATH
Export selected threads to mboxcl2 file.
.It Cm create-mailbox Ar ACCOUNT Ar MAILBOX_PATH
.It Cm create\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
create mailbox with given path.
Be careful with backends and separator sensitivity (eg IMAP)
.It Cm subscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
.It Cm subscribe\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
subscribe to mailbox with given path
.It Cm unsubscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
.It Cm unsubscribe\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
unsubscribe to mailbox with given path
.It Cm rename-mailbox Ar ACCOUNT Ar MAILBOX_PATH_SRC Ar MAILBOX_PATH_DEST
.It Cm rename\-mailbox Ar ACCOUNT Ar MAILBOX_PATH_SRC Ar MAILBOX_PATH_DEST
rename mailbox
.It Cm delete-mailbox Ar ACCOUNT Ar MAILBOX_PATH
.It Cm delete\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
deletes mailbox in the mail backend.
This action is unreversible.
This action is irreversible.
.El
.Ss Mail view commands
.HorizontalRule
.Bl -tag -width 36n
.It Cm pipe Ar EXECUTABLE Ar ARGS
pipe pager contents to binary
@ -484,7 +538,8 @@ unsubscribe automatically from list of viewed envelope
open list archive with
.Cm xdg-open
.El
.Ss composing mail commands
.Ss Composing mail commands
.HorizontalRule
.Bl -tag -width 36n
.It Cm mailto Ar MAILTO_ADDRESS
Opens a composer tab with initial values parsed from the
@ -519,7 +574,8 @@ for PGP configuration.
.It Cm save-draft
saves a copy of the draft in the Draft folder
.El
.Ss generic commands
.Ss Generic commands
.HorizontalRule
.Bl -tag -width 36n
.It Cm open-in-tab
opens envelope view in new tab
@ -543,10 +599,6 @@ Useful if you want to reload some settings without restarting
.Nm Ns
\&.
.El
.Sh SHORTCUTS
See
.Xr meli.conf 5 SHORTCUTS
for shortcuts and their default values.
.Sh EXIT STATUS
.Nm
exits with 0 on a successful run.
@ -564,7 +616,9 @@ Specifies the editor to use
.It Ev MELI_CONFIG
Override the configuration file
.It Ev NO_COLOR
When present (regardless of its value), prevents the addition of ANSI color.
When defined (regardless of its value), prevents the addition of
.Tn ANSI
color.
The configuration value
.Ic use_color
overrides this.
@ -621,75 +675,265 @@ Mailcap entries are searched for in the following files, in this order:
.It
.Pa /usr/local/etc/mailcap
.El
.Sh STANDARDS
.Bl -dash -compact
.It
.Rs
.%B XDG Base Directory Specification
.%O Version 0.8
.%A Waldo Bastian
.%A Allison Karlitskaya
.%A Lennart Poettering
.%A Johannes Löthberg
.%U https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
.%D May 08, 2021
.Re
.It
.Rs
.%B maildir
.%A Daniel J. Bernstein
.%U https://cr.yp.to/proto/maildir.html
.%D 1995
.Re
.It
.Rs
.%B RFC1524 A User Agent Configuration Mechanism For Multimedia Mail Format Information
.%O mailcap file
.%I Legacy
.%D September 01, 1993
.%A Dr. Nathaniel S. Borenstein
.%U https://datatracker.ietf.org/doc/rfc1524/
.Re
.It
.Rs
.%B RFC2047 MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
.%I IETF
.%D November 01, 1996
.%A Keith Moore
.%U https://datatracker.ietf.org/doc/rfc2047/
.Re
.It
.Rs
.%B RFC2183 Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
.%I Legacy
.%D August 01, 1997
.%A Rens Troost
.%A Steve Dorner
.%A Keith Moore
.%U https://datatracker.ietf.org/doc/rfc2183/
.Re
.It
.Rs
.%B RFC2369 The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
.%I Legacy
.%D July 01, 1998
.%A Joshua D. Baer
.%A Grant Neufeld
.%U https://datatracker.ietf.org/doc/rfc2369/
.Re
.It
.Rs
.%B RFC2426 vCard MIME Directory Profile
.%O vCard Version 3
.%I IETF
.%D September 01, 1998
.%A Frank Dawson
.%A Tim Howes
.%U https://datatracker.ietf.org/doc/rfc2426/
.Re
.It
.Rs
.%B RFC3156 MIME Security with OpenPGP
.%I IETF
.%D August 01, 2001
.%A Thomas Roessler
.%A Michael Elkins
.%A Raph Levien
.%A Dave Del Torto
.%U https://datatracker.ietf.org/doc/rfc3156/
.Re
.It
.Rs
.%B RFC3461 Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
.%I IETF
.%D January 23, 2003
.%A Keith Moore
.%U https://datatracker.ietf.org/doc/rfc3461/
.Re
.It
.Rs
.%B RFC3501 INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
.%I IETF
.%D March 18, 2003
.%A Mark Crispin
.%U https://datatracker.ietf.org/doc/rfc3501/
.Re
.It
.Rs
.%B RFC3676 The Text/Plain Format and DelSp Parameters
.%I IETF
.%D February 19, 2004
.%A Randall Gellens
.%U https://datatracker.ietf.org/doc/rfc3676/
.Re
.It
.Rs
.%B RFC3691 Internet Message Access Protocol (IMAP) UNSELECT command
.%I IETF
.%D February 20, 2004
.%A Alexey Melnikov
.%U https://datatracker.ietf.org/doc/rfc3691/
.Re
.It
.Rs
.%B RFC3977 Network News Transfer Protocol (NNTP)
.%I IETF
.%D October 26, 2006
.%A Clive Feather
.%U https://datatracker.ietf.org/doc/rfc3977/
.Re
.It
.Rs
.%B RFC4549 Synchronization Operations for Disconnected IMAP4 Clients
.%I IETF
.%D June 16, 2006
.%A Alexey Melnikov
.%U https://datatracker.ietf.org/doc/rfc4549/
.Re
.It
.Rs
.%B RFC4616 The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
.%I IETF
.%D August 31, 2006
.%A Kurt Zeilenga
.%U https://datatracker.ietf.org/doc/rfc4616/
.Re
.It
.Rs
.%B RFC4954 SMTP Service Extension for Authentication
.%I IETF
.%D July 23, 2007
.%A Rob Siemborski
.%A Alexey Melnikov
.%U https://datatracker.ietf.org/doc/rfc4954/
.Re
.It
.Rs
.%B RFC5321 Simple Mail Transfer Protocol
.%I IETF
.%D October 01, 2008
.%A Dr. John C. Klensin
.%U https://datatracker.ietf.org/doc/rfc5321/
.Re
.It
.Rs
.%B RFC5322 Internet Message Format
.%I IETF
.%D October 01, 2008
.%A Pete Resnick
.%U https://datatracker.ietf.org/doc/rfc5322/
.Re
.It
.Rs
.%B RFC6048 Network News Transfer Protocol (NNTP) Additions to LIST Command
.%I IETF
.%D November 22, 2010
.%A Julien ÉLIE
.%U https://datatracker.ietf.org/doc/rfc6048/
.Re
.It
.Rs
.%B RFC6152 SMTP Service Extension for 8-bit MIME Transport
.%I IETF
.%D March 07, 2011
.%A Dave Crocker
.%A Dr. John C. Klensin
.%A Dr. Marshall T. Rose
.%A Ned Freed
.%U https://datatracker.ietf.org/doc/rfc6152/
.Re
.It
.Rs
.%B RFC6350 vCard Format Specification
.%O vCard Version 4
.%I IETF
.%D August 31, 2011
.%A Simon Perreault
.%U https://datatracker.ietf.org/doc/rfc6350/
.Re
.It
.Rs
.%B RFC6532 Internationalized Email Headers
.%I IETF
.%D February 17, 2012
.%A Abel Yang
.%A Shawn Steele
.%A Ned Freed
.%U https://datatracker.ietf.org/doc/rfc6532/
.Re
.It
.Rs
.%B RFC6868 Parameter Value Encoding in iCalendar and vCard
.%I IETF
.%D February 14, 2013
.%A Cyrus Daboo
.%U https://datatracker.ietf.org/doc/rfc6868/
.Re
.It
.Rs
.%B RFC7162 IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
.%I IETF
.%D May 23, 2014
.%A Alexey Melnikov
.%A Dave Cridland
.%U https://datatracker.ietf.org/doc/rfc7162/
.Re
.It
.Rs
.%B RFC8620 The JSON Meta Application Protocol (JMAP)
.%I IETF
.%D July 18, 2019
.%A Neil Jenkins
.%A Chris Newman
.%U https://datatracker.ietf.org/doc/rfc8620/
.Re
.It
.Rs
.%B RFC8621 The JSON Meta Application Protocol (JMAP) for Mail
.%I IETF
.%D August 08, 2019
.%A Neil Jenkins
.%A Chris Newman
.%U https://datatracker.ietf.org/doc/rfc8621/
.Re
.El
.Sh SEE ALSO
.Xr meli.conf 5 ,
.Xr meli-themes 5 ,
.Xr meli 7 ,
.Xr xdg-open 1 ,
.Xr mailcap 5
.Sh CONFORMING TO
.Bl -bullet -compact
.It
XDG Standard
.Lk https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
\&.
.It
mailcap file, RFC 1524: A User Agent Configuration Mechanism For Multimedia Mail Format Information
.It
RFC 5322: Internet Message Format
.It
RFC 6532: Internationalized Email Headers
.It
RFC 2047: MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
.It
RFC 3676: The Text/Plain Format and DelSp Parameters
.It
RFC 3156: MIME Security with OpenPGP
.It
RFC 2183: Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
.It
RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
.It
.Li maildir
.Lk https://cr.yp.to/proto/maildir.html Ns
\&.
.It
RFC 5321: Simple Mail Transfer Protocol
.It
RFC 3461: Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
.It
RFC 4954: SMTP Service Extension for Authentication
.It
RFC 6152: SMTP Service Extension for 8-bit MIME Transport
.It
RFC 4616: The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
.It
RFC 3501: INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
.It
RFC 3691: Internet Message Access Protocol (IMAP) UNSELECT command
.It
RFC 4549: Synch Ops for Disconnected IMAP4 Clients
.It
RFC 7162: IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
.It
RFC 8620: The JSON Meta Application Protocol (JMAP)
.It
RFC 8621: The JSON Meta Application Protocol (JMAP) for Mail
.It
RFC 3977: Network News Transfer Protocol (NNTP)
.It
RFC 6048: Network News Transfer Protocol (NNTP) Additions to LIST Command
.It
vCard Version 3, RFC 2426: vCard MIME Directory Profile
.It
vCard Version 4, RFC 6350: vCard Format Specification
.It
RFC 6868 Parameter Value Encoding in iCalendar and vCard
.El
.Sh AUTHORS
Copyright 2017-2022
.An Manos Pitsidianakis Aq manos@pitsidianak.is
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind (See COPYING for full copyright and warranty notices).
Copyright 2017\(en2024
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
.Pp
.Lk https://meli.delivery
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind.
.Po
See
.Pa COPYING
for full copyright and warranty notices.
.Pc
.Ss Links
.Bl -item -compact
.It
.Lk https://meli\-email.org "Website"
.It
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
.It
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
.It
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
.It
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
.El

View File

@ -40,22 +40,23 @@
.Pc Ns
..
.de Command
.Bd -offset 1n -ragged
.Bd -ragged -offset 1n
.Cm \\$*
.Ed
..
.Dd November 11, 2022
.\".Dd November 11, 2022
.Dd March 10, 2024
.Dt MELI 7
.Os
.Sh NAME
.Nm meli
.Nd Tutorial for the meli terminal e-mail client
.Nd Tutorial for the meli terminal e\-mail client
.Sh SYNOPSIS
.Nm
.Op ...
.Sh DESCRIPTION
.Nm
is a terminal mail client aiming for extensive and user-frendly configurability.
is a terminal mail client aiming for extensive and user\-friendly configurability.
.Bd -literal -offset center
^^ .-=-=-=-. ^^
^^ (`-=-=-=-=-`) ^^
@ -158,9 +159,9 @@ key.
.It EMBED
This is the mode of the embed terminal emulator.
To exit an embedded application, issue
.Aq Ctrl-C
.Aq Ctrl\-C
to kill it or
.Aq Ctrl-Z
.Aq Ctrl\-Z
to stop the program and follow the instructions on
.Nm
to exit.
@ -229,7 +230,7 @@ This is the view you will spend more time with in
\&.
.Pp
Press
.Shortcut ` listing toggle_menu_visibility
.Shortcut \(ga listing toggle_menu_visibility
to toggle the sidebars visibility.
.Pp
Press
@ -237,16 +238,16 @@ Press
to switch focus on the sidebar menu.
Press
.Shortcut Right listing focus_left
to switch focus on the e-mail list.
to switch focus on the e\-mail list.
.Pp
On the e-mail list, press
On the e\-mail list, press
.Shortcut k listing scroll_up
to scroll up, and
.Shortcut j listing scroll_down
to scroll down.
Press
.Shortcut Enter listing open_entry
to open an e-mail entry and
to open an e\-mail entry and
.Shortcut i listing exit_entry
to exit it.
.Bd -ragged
@ -294,9 +295,9 @@ See
for details.
.Pp
You can increase the sidebar's width with
.Shortcut Ctrl-p listing increase_sidebar
.Shortcut Ctrl\-p listing increase_sidebar
and decrease with
.ShortcutPeriod Ctrl-o listing decrease_sidebar
.ShortcutPeriod Ctrl\-o listing decrease_sidebar
\&.
.Bd -ragged
.Sy The status bar.
@ -310,7 +311,7 @@ and decrease with
The status bar shows which mode you are, and the status message of the current view.
In the pictured example, it shows the status of a mailbox called
.Dq Inbox
with lots of e-mails.
with lots of e\-mails.
.Bd -ragged
.Sy The number modifier buffer.
.Ed
@ -330,7 +331,7 @@ entries.
Another use of the number buffer is opening URLs inside the pager.
See
.Sx PAGER
for an explanation of interacting with URLs in e-mails.
for an explanation of interacting with URLs in e\-mails.
.Pp
Pressing numbers in
.Sy NORMAL
@ -343,16 +344,16 @@ There are four different list styles:
.Bl -hyphen -compact
.It
.Qq plain
which shows one line per e-mail.
which shows one line per e\-mail.
.It
.Qq threaded
which shows a threaded view with drawn tree structure.
.It
.Qq compact
which shows one line per thread which can include multiple e-mails.
which shows one line per thread which can include multiple e\-mails.
.It
.Qq conversations
which shows more than one line per thread which can include multiple e-mails with more details about the thread.
which shows more than one line per thread which can include multiple e\-mails with more details about the thread.
.El
.Bd -ragged
.Sy Plain view\&.
@ -421,13 +422,13 @@ Simple set operations can be performed on a selection with these shortcut modifi
.Bl -hyphen -compact
.It
Union modifier:
.Shortcut Ctrl-u listing union_modifier
.Shortcut Ctrl\-u listing union_modifier
.It
Difference modifier:
.Shortcut Ctrl-d listing diff_modifier
.Shortcut Ctrl\-d listing diff_modifier
.It
Intersection modifier:
.Shortcut Ctrl-i listing intersection_modifier
.Shortcut Ctrl\-i listing intersection_modifier
.El
.Pp
To set an entry as
@ -445,7 +446,11 @@ which also has its complement
.sp
action.
.Pp
For e-mail backends that support tags
For e\-mail backends that support flags you can use the following commands on entries and selections to modify them:
.Command flag set FLAG
.Command flag unset FLAG
.Pp
For e\-mail backends that support tags
.Po
like
.Qq IMAP
@ -463,10 +468,13 @@ you can use the following commands on entries and selections to modify them:
and
.Ic ignore_tags
for how to set tag colors and tag visibility)
You can clear the selection with the
.Aq Esc
key.
.Sh PAGER
You can open an e-mail entry by pressing
You can open an e\-mail entry by pressing
.ShortcutPeriod Enter listing open_entry
\&. This brings up the e-mail view with the e-mail content inside a pager.
\&. This brings up the e\-mail view with the e\-mail content inside a pager.
.Bd -literal -offset center
┌────────────────────────────────────────────────────────────┐
│Date: Sat, 21 May 2022 16:16:11 +0300 ▀│
@ -494,14 +502,14 @@ You can open an e-mail entry by pressing
└────────────────────────────────────────────────────────────┘
.Ed
.Bd -ragged -offset 3n
.Em The\ pager\ displaying\ an\ e-mail\&.
.Em The\ pager\ displaying\ an\ e\-mail\&.
.Ed
.Pp
The pager is simple to use.
Scroll with the following:
.Bl -hang -width 27n
.It Go to next pager page
.Shortcut PageDown pager page_down
.Shortcut PageDown pager page_down
.It Go to previous pager page
.Shortcut PageUp pager page_up
.It Scroll down pager.
@ -516,7 +524,7 @@ which will act as a multiplier.
.Pp
The pager can enter a special
.Em url
mode which will prefix all detected hyperlinks and e-mail addresses with a number inside square brackets
mode which will prefix all detected hyperlinks and e\-mail addresses with a number inside square brackets
.ShortcutPeriod u pager toggle_url_mode
\&.
Writing down a chosen number as a number modifier
@ -547,13 +555,13 @@ for more details
.Pc Ns
\&.
.Sh MAIL VIEW
Other things you can do when viewing e-mail:
.Bl -bullet -compact
Other things you can do when viewing e\-mail:
.Bl -dash -compact
.It
Most importantly, you can exit the mail view with:
.Shortcut i listing exit_entry
.It
Add addresses from the e-mail headers to contacts:
Add addresses from the e\-mail headers to contacts:
.Shortcut c envelope_view add_addresses_to_contacts
.It
Open an attachment by entering its index as a number modifier and pressing:
@ -569,39 +577,39 @@ Reply to envelope:
.Shortcut R envelope_view reply
.It
Reply to author:
.Shortcut Ctrl-r envelope_view reply_to_author
.Shortcut Ctrl\-r envelope_view reply_to_author
.It
Reply to all/Reply to list/Follow up:
.Shortcut Ctrl-g envelope_view reply_to_all
.Shortcut Ctrl\-g envelope_view reply_to_all
.It
Forward email:
.Shortcut Ctrl-f envelope_view forward
Forward e\-mail:
.Shortcut Ctrl\-f envelope_view forward
.It
Expand extra headers: (References and others)
.Shortcut h envelope_view toggle_expand_headerk
.It
View envelope source in a pager: (toggles between raw and decoded source)
.Shortcut M-r envelope_view view_raw_source
.Shortcut M\-r envelope_view view_raw_source
.It
Return to envelope_view if viewing raw source or attachment:
.Shortcut r envelope_view return_to_normal_view
.El
.Sh COMPOSING
To compose an e-mail, you can either start with an empty draft by pressing
To compose an e\-mail, you can either start with an empty draft by pressing
.Shortcut m listing new_mail
which opens a composer view in a new tab.
To reply to a specific e-mail, when in envelope view you can select the specific action you want to take:
To reply to a specific e\-mail, when in envelope view you can select the specific action you want to take:
.sp
.Bl -bullet -compact
.Bl -dash -compact
.It
Reply to envelope.
.Shortcut R envelope_view reply
.It
Reply to author.
.Shortcut Ctrl-r envelope_view reply_to_author
.Shortcut Ctrl\-r envelope_view reply_to_author
.It
Reply to all.
.Shortcut Ctrl-g envelope_view reply_to_all
.Shortcut Ctrl\-g envelope_view reply_to_all
.El
.sp
To launch your editor, press
@ -688,25 +696,25 @@ the\ actual\ embedding\ is\ seamless\&.
.Ed
.Ss composing mail commands
.Bl -tag -width 36n
.It Cm add-attachment Ar PATH
.It Cm add\-attachment Ar PATH
in composer, add
.Ar PATH
as an attachment
.It Cm add-attachment < Ar CMD Ar ARGS
.It Cm add\-attachment < Ar CMD Ar ARGS
in composer, pipe
.Ar CMD Ar ARGS
output into an attachment
.It Cm add-attachment-file-picker
.It Cm add\-attachment\-file\-picker
Launch command defined in the configuration value
.Ic file_picker_command
in
.Xr meli.conf 5 TERMINAL
.It Cm add-attachment-file-picker < Ar CMD Ar ARGS
.It Cm add\-attachment\-file\-picker < Ar CMD Ar ARGS
Launch command
.Ar CMD Ar ARGS Ns
\&.
The command should print file paths in stderr, separated by NULL bytes.
.It Cm remove-attachment Ar INDEX
.It Cm remove\-attachment Ar INDEX
remove attachment with given index
.It Cm toggle sign
toggle between signing and not signing this message.
@ -714,7 +722,7 @@ If the gpg invocation fails then the mail won't be sent.
See
.Xr meli.conf 5 PGP
for PGP configuration.
.It Cm save-draft
.It Cm save\-draft
saves a copy of the draft in the Draft folder
.El
.\" [ref:TODO]: add contacts section
@ -731,12 +739,26 @@ for documentation on how to theme
.Xr xdg-open 1 ,
.Xr mailcap 5
.Sh AUTHORS
Copyright 2017-2022
.An Manos Pitsidianakis Aq manos@pitsidianak.is
Copyright 2017\(en2024
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
.Pp
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind.
(See COPYING for full copyright and warranty notices.)
.Pp
.Lk https://meli.delivery
.Lk https://github.com/meli/meli
.Lk https://crates.io/crates/meli
.Po
See
.Pa COPYING
for full copyright and warranty notices.
.Pc
.Ss Links
.Bl -item -compact
.It
.Lk https://meli\-email.org "Website"
.It
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
.It
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
.It
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
.It
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
.El

View File

@ -17,7 +17,33 @@
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.Dd November 11, 2022
.de HorizontalRule
.\"\l'\n(.l\(ru1.25'
.sp
..
.de LiteralStringValue
.Sm
.Po Qo
.Em Li \\$1
.Qc Pc
.Sm
..
.de LiteralStringValueRenders
.LiteralStringValue \\$1
.shift 1
.Bo
.Sm
Rendered as:
.Li r##
.Qo
\\$1
.Qc
.Li ##
.Bc
.Sm
..
.\".Dd November 11, 2022
.Dd March 10, 2024
.Dt MELI.CONF 5
.Os
.Sh NAME
@ -44,7 +70,7 @@ is written in
.Tn TOML
which has a few things to consider (quoting the specification):
.sp
.Bl -bullet -compact
.Bl -dash -compact
.It
.Tn TOML
is case sensitive.
@ -94,7 +120,7 @@ include macro:
.\"
.Sh SECTIONS
The top level sections of the configuration are:
.Bl -bullet -compact
.Bl -dash -compact
.It
accounts
.It
@ -178,19 +204,24 @@ theme = "light"
.\"
.sp
Available options are listed below.
Default values are shown in parentheses.
.\"
.\"
.\"
.\"
.\"
.Sh ACCOUNTS
.Ss Account configuration settings
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic root_mailbox Ar String
The backend-specific path of the root_mailbox, usually
.Sy INBOX Ns
\&.
.It Ic default_mailbox Ar String
.Pq Em optional
The mailbox that is the default to open or view for this account.
Must be a valid mailbox path.
If not specified, the default will be the root mailbox.
.It Ic format Ar String Op maildir mbox imap notmuch jmap
The format of the mail backend.
.It Ic subscribed_mailboxes Ar [String,]
@ -215,14 +246,6 @@ When replying to an e-mail addressed to one of these identities, the
:
header will be adjusted to its value instead of the default identity.
.El
.TS
allbox tab(:);
lb l.
conversations:shows one entry per thread
compact:shows one row per thread
threaded:shows threads as a tree structure
plain:shows one row per mail, regardless of threading
.TE
.Bl -tag -width 36n
.It Ic display_name Ar String
.Pq Em optional
@ -278,6 +301,7 @@ Its format is described below in
\&.
.El
.Ss notmuch only
.HorizontalRule
notmuch is supported by loading the dynamic library
.Sy libnotmuch Ns
\&.
@ -346,6 +370,7 @@ format = "notmuch"
.\"
.\"
.Ss IMAP only
.HorizontalRule
.Tn IMAP
specific options are:
.Bl -tag -width 36n
@ -439,17 +464,20 @@ seconds means there is no timeout.
.Pq Em 16 \" default value
.El
.Ss Gmail
.HorizontalRule
.Tn Gmail
has non-standard
.Tn IMAP
behaviors that need to be worked around.
.Ss Gmail - sending mail
.HorizontalRule
Option
.Ic store_sent_mail
should be disabled since
.Tn Gmail
auto-saves sent mail by its own.
.Ss Gmail OAUTH2
.HorizontalRule
To use
.Tn OAUTH2 Ns
, you must go through a process to register your own private
@ -458,7 +486,12 @@ with
.Tn Google
that can use
.Tn OAUTH2
tokens.
tokens,
and set the option
.Ic use_oauth2
as
.Ql true
in the account configuration section.
For convenience in the
.Sy meli
repository under the
@ -472,7 +505,7 @@ to generate and request the appropriate data to perform
authentication.
.sp
Steps:
.Bl -bullet -compact
.Bl -dash -compact
.It
In
.Tn Google API Ns
@ -505,8 +538,27 @@ should evaluate this command which if successful must only return a
.Tn base64 Ns
-encoded token ready to be passed to
.Tn IMAP.
.Pp
Your account section should look like this:
.Bd -literal
[accounts."gmail"]
root_mailbox = '[Gmail]'
format = "imap"
server_hostname='imap.gmail.com'
server_username="username@gmail.com"
use_oauth2 = true
server_password_command = "TOKEN=$(py...th2_string --quiet --access_token=$TOKEN"
server_port="993"
listing.index_style = "Conversations"
identity = "username@gmail.com"
display_name = "Name Name"
subscribed_mailboxes = ["*" ]
composing.store_sent_mail = false
composing.send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type = "xoauth2", token_command = "...", require_auth = true }, security = { type = "STARTTLS" } }
.Ed
.El
.Ss JMAP only
.HorizontalRule
.Tn JMAP
specific options
.Bl -tag -width 36n
@ -527,6 +579,7 @@ certificates.
.Pq Em false \" default value
.El
.Ss mbox only
.HorizontalRule
.Tn mbox
specific options:
.Bl -tag -width 36n
@ -542,7 +595,7 @@ If the preferred format fails, the message is retried with mboxrd and then if
it fails again there is a recover attempt, which discards the invalid message.
.sp
Valid values
.Bl -bullet -compact
.Bl -dash -compact
.It
.Ar auto
.It
@ -575,6 +628,7 @@ mailboxes."Python mailing list" = { path = "~/.mail/python.mbox", subscribe = tr
.\"
.\"
.Ss NNTP
.HorizontalRule
.Tn NNTP
specific options
.Bl -tag -width 36n
@ -585,7 +639,7 @@ example:
Server username
.It Ic server_password Ar String
Server password
.It Ic require_auth Ar bool
.It Ic require_auth Ar boolean
.Pq Em optional
require authentication in every case
.Pq Em true \" default value
@ -646,6 +700,7 @@ composing.send_mail = "server_submission"
.\"
.\"
.Ss MAILBOXES
.HorizontalRule
.Bl -tag -width 36n
.It Ic alias Ar String
.Pq Em optional
@ -672,7 +727,7 @@ Silently insert updates for this mailbox, if any.
.Pq Em optional
special usage of this mailbox.
Valid values are:
.Bl -bullet -compact
.Bl -dash -compact
.It
.Ar Normal
.Pq Em default
@ -748,7 +803,7 @@ mailboxes.
.\"
.\"
.Sh COMPOSING
Composing specific options.
.Ss Composing specific configuration settings
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic send_mail Ar String|SmtpServerConf
@ -825,7 +880,7 @@ This setting is meant to be disabled for non-standard behaviour in
.Pq Em optional
The attribution line appears above the quoted reply text.
The format specifiers for the replied address are:
.Bl -bullet -compact
.Bl -dash -compact
.It
.Li %+f
— the sender's name and email address.
@ -859,8 +914,10 @@ Alternative lists of reply prefixes (etc. ["Re:", "RE:", ...]) to strip.
.It Ic reply_prefix Ar String
.Pq Em optional
The prefix to use in reply subjects.
The default prefix is "Re:".
.Pq Em `Re:` \" default value
The default prefix is
.Ns Ql Re: Ns
\&.
.Pq Ql Re: \" default value
.Pp
RFC 2822, "Internet Message Format" has this to say on the matter:
.\"
@ -902,7 +959,7 @@ compose-hooks run before submitting an e-mail.
They perform draft validation and/or transformations.
If a hook encounters an error or warning, it will show up as a notification.
The currently available hooks are:
.Bl -bullet -compact
.Bl -dash -compact
.It
.Ic past-date-warn
— Warn if
@ -939,9 +996,10 @@ or draft body mention attachments but they are missing.
.\"
.\"
.Sh SHORTCUTS
Default values are shown in parentheses.
.Ss Values corresponding to keyboard keys, keycodes
Shortcuts can take the following values:
.Bl -bullet -compact
.sp
.Bl -dash -compact
.It
.Em Backspace
.It
@ -979,9 +1037,13 @@ Shortcuts can take the following values:
.It
.Em char
.El
.Em char
is a single character string.
.sp
Where
.Em char
is a single character string, maximum 4 bytes long, like the corresponding type
in Rust.
.Pp
In the next subsection, you will find lists for each shortcut category.
The headings before each list indicate the map key of the shortcut list.
For example for the first list titled
.Em general
@ -1013,6 +1075,28 @@ exit_entry = 'i'
.\"
.sp
.Pp
.Em commands
.sp
In addition, each shortcuts section supports a TOML array of commands to
associate a key to an array of
.Em COMMAND
mode commands.
.sp
.\"
.\"
.\"
.Bd -literal
[shortcuts.listing]
commands = [ { command = [ "tag remove trash", "flag unset trash" ], shortcut = "D" },
{ command = [ "tag add trash", "flag set trash" ], shortcut = "d" } ]
.Ed
.\"
.\"
.\"
.Ss Shortcut configuration settings
.HorizontalRule
Default values are shown in parentheses.
.sp
.Em general
.Bl -tag -width 36n
.It Ic toggle_help
@ -1153,7 +1237,7 @@ current sorting.
.Pq Em C-p \" default value
.It Ic toggle_menu_visibility
Toggle visibility of side menu in mail list.
.Pq Em ` \" default value
.Pq Em \(ga \" default value
.It Ic focus_left
Switch focus on the left.
.Pq Em Left \" default value
@ -1212,7 +1296,7 @@ Go to previous account.
.Pq Em L \" default value
.It Ic toggle_menu_visibility
Toggle visibility of side menu in mail list.
.Pq Em ` \" default value
.Pq Em \(ga \" default value
.El
.sp
.sp
@ -1333,6 +1417,7 @@ Toggle between horizontal and vertical layout.
.\"
.\"
.Sh NOTIFICATIONS
.Ss Notification configuration settings
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic enable Ar boolean
@ -1367,6 +1452,7 @@ Play sound file in notifications if possible.
.\"
.\"
.Sh PAGER
.Ss Pager (viewing text) configuration settings
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic sticky_headers Ar boolean
@ -1385,11 +1471,11 @@ A command to open html files.
.Pq Em optional
A command to pipe mail output through for viewing in pager.
.Pq Em none \" default value
.It Ic format_flowed Ar bool
.It Ic format_flowed Ar boolean
.Pq Em optional
Respect format=flowed
.Pq Em true \" default value
.It Ic split_long_lines Ar bool
.It Ic split_long_lines Ar boolean
.Pq Em optional
Split long lines that would overflow on the x axis.
.Pq Em true \" default value
@ -1451,6 +1537,7 @@ INBOX = {}
.\"
.\"
.Sh LISTING
.Ss Listing (lists of e-mail entries in a mailbox) configuration settings
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic show_menu_scrollbar Ar boolean
@ -1465,7 +1552,9 @@ Datetime formatting passed verbatim to
.Pq Em \&%Y-\&%m-\&%d \&%T \" default value
.It Ic recent_dates Ar Boolean
.Pq Em optional
Show recent dates as `X {minutes,hours,days} ago`, up to 7 days.
Show recent dates as
.Ns Ql X {minutes,hours,days} ago
.Ns , up to 7 days.
.Pq Em true \" default value
.It Ic filter Ar Query
.Pq Em optional
@ -1488,6 +1577,14 @@ filter = "not flags:seen" # show only unseen messages
.\"
.It Ic index_style Ar String
Sets the way mailboxes are displayed.
.TS
allbox tab(:);
lb l.
conversations:shows one entry per thread
compact:shows one row per thread
threaded:shows threads as a tree structure
plain:shows one row per mail, regardless of threading
.TE
.It Ic sidebar_mailbox_tree_has_sibling Ar String
.Pq Em optional
Sets the string to print in the mailbox tree for a level where its root has a
@ -1518,33 +1615,43 @@ Flag to show if thread entry contains unseen mail.
.Pq Em "●" \" default value
.It Ic thread_snoozed_flag Ar Option<String>
Flag to show if thread has been snoozed.
.Pq Em "💤" \" default value
.LiteralStringValueRenders 💤\e\uu{FE0E} 💤︎ \" default value
.It Ic selected_flag Ar Option<String>
Flag to show if thread entry has been selected.
.Pq Em "☑️" \" default value
.LiteralStringValueRenders ☑️ \e\uu{2007} ☑️ 
.It Ic attachment_flag Ar Option<String>
Flag to show if thread entry contains attachments.
.Pq Em "📎" \" default value
.It Ic thread_subject_pack Ar bool
.LiteralStringValueRenders 📎\e\uu{FE0E} 📎︎ \" default value
.It Ic highlight_self_flag Ar Option<String>
Flag to show if any thread entry contains your address as a receiver.
Useful to make mailing list threads that CC you stand out.
.Pq Em "✸" \" default value
.It Ic highlight_self Ar boolean
Show
.Ic highlight_self_flag
or not.
.Pq Em false \" default value
.It Ic thread_subject_pack Ar boolean
Should threads with differentiating Subjects show a list of those subjects on
the entry title?
.Pq Em "true" \" default value
.It Ic threaded_repeat_identical_from_values Ar bool
.Pq Em true \" default value
.It Ic threaded_repeat_identical_from_values Ar boolean
In threaded listing style, repeat identical From column values within a thread.
Not repeating adds empty space in the From column which might result in less
visual clutter.
.Pq Em "false" \" default value
.It Ic relative_menu_indices Ar bool
.Pq Em false \" default value
.It Ic relative_menu_indices Ar boolean
Show relative indices in menu mailboxes to quickly help with jumping to them.
.Pq Em true \" default value
.It Ic relative_list_indices Ar bool
.It Ic relative_list_indices Ar boolean
Show relative indices in listings to quickly help with jumping to them.
.Pq Em true \" default value
.It Ic hide_sidebar_on_launch Ar bool
.It Ic hide_sidebar_on_launch Ar boolean
Start app with sidebar hidden.
.Pq Em false \" default value
.El
.Ss Examples of sidebar mailbox tree customization
.HorizontalRule
The default values
.sp
.\"
@ -1652,6 +1759,7 @@ no_sibling_leaf = " \\_"
.\"
.\"
.Sh TAGS
.Ss Tags (e-mail metadata in backends that support them) configuration settings
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic colours Ar hash table String[Color]
@ -1675,7 +1783,7 @@ colors = { signed="#Ff6600", replied="DeepSkyBlue4", draft="#f00", replied="8" }
\&...
[accounts.dummy.mailboxes]
# per mailbox override:
"INBOX" = { tags.ignore_tags=["inbox", ] }
"INBOX" = { tags.ignore_tags=["inbox", ] }
.Ed
.\"
.\"
@ -1740,7 +1848,7 @@ Use comma to separate values.
.Pq Em Local,WKD \" default value
.Pp
Possible mechanisms:
.Bl -bullet -compact
.Bl -dash -compact
.It
.Em cert
.It
@ -1765,6 +1873,29 @@ Possible mechanisms:
.\"
.\"
.Sh TERMINAL
.Ss Note about emojis and other multi-width characters in string values
Some useful unicode combining marks
.Po
invisible characters that modify the presentation of visible characters before
them
.Pc
are:
.sp
.Bl -tag -width 15n
.It Ns
.Li \e\uu{FE0E}
Emoji variation sequence select 15: renders an emoji as text style (monochrome)
.It Ns
.Li \e\uu{FE0F}
Emoji variation sequence select 16: renders an emoji in color
.It Ns
.Li \e\uu{2007}
Figure space, a space character with the width of a digit in a monospace
typeface
.El
.sp
.Ss Terminal configuration settings
.HorizontalRule
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic theme Ar String
@ -1777,7 +1908,9 @@ If true, box drawing will be done with ASCII characters.
.Pq Em false \" default value
.It Ic use_color Ar boolean
.Pq Em optional
If false, no ANSI colors are used.
If false, no
.Tn ANSI
colors are used.
.Pq Em true \" default value
.It Ic window_title Ar String
.Pq Em optional
@ -1817,7 +1950,7 @@ theme = "themeB"
.\"
.\"
.\"
.It Ic use_mouse Ar bool
.It Ic use_mouse Ar boolean
Use mouse events.
This will disable text selection, but you will be able to resize some widgets.
This setting can be toggled with
@ -1909,6 +2042,7 @@ progress_spinner_sequence = { interval_ms = 150, frames = [ "-", "=", "≡" ] }
.\"
.\"
.Sh LOG
.Ss Logging configuration settings
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic log_file Ar String
@ -1922,7 +2056,7 @@ All levels less or equal to the
.Ic maximum_level
will be appended to the log file.
Available levels are, in partial order:
.Bl -bullet -compact
.Bl -dash -compact
.It
.Em OFF
.It
@ -1949,6 +2083,7 @@ to
.\"
.\"
.Sh SMTP Connections
.Ss SMTP configuration settings
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic hostname Ar String
@ -1994,7 +2129,7 @@ For type
.Bl -tag -width 36n
.It Ic username Ar String
.It Ic password Ar SmtpPassword
.It Ic require_auth Ar bool
.It Ic require_auth Ar boolean
.Pq Em optional
require authentication in every case.
.Pq Em true \" default value
@ -2008,7 +2143,7 @@ For type
Command to evaluate that returns an
.Tn XOAUTH2
token.
.It Ic require_auth Ar bool
.It Ic require_auth Ar boolean
.Pq Em optional
require authentication in every case.
.Pq Em true \" default value
@ -2077,7 +2212,7 @@ Default security type is
\&.
.Bl -tag -width 36n
.It Ic type Ar "none" | "auto" | "starttls" | "tls"
.It Ic danger_accept_invalid_certs Ar bool
.It Ic danger_accept_invalid_certs Ar boolean
Accept invalid
.Tn SSL
/
@ -2087,13 +2222,13 @@ certificates
.El
.Ss SmtpExtensions
.Bl -tag -width 36n
.It Ic pipelining Ar bool
.It Ic pipelining Ar boolean
RFC2920
.Pq Em true \" default value
.It Ic chunking Ar bool
.It Ic chunking Ar boolean
RFC3030
.Pq Em true \" default value
.It Ic prdr Ar bool
.It Ic prdr Ar boolean
draft-hall-prdr-00
.Pq Em true \" default value
.It Ic dsn_notify Ar String
@ -2108,16 +2243,35 @@ RFC3461
.Sh SEE ALSO
.Xr meli 1 ,
.Xr meli-themes 5
.Sh CONFORMING TO
.Sh STANDARDS
.Bl -item -compact
.It
.Tn TOML
Standard
.Li v.0.5.0
.Lk https://toml.io/en/v0.5.0
.El
.Sh AUTHORS
Copyright 2017-2019
.An Manos Pitsidianakis Aq manos@pitsidianak.is
Copyright 2017\(en2024
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
.Pp
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind.
(See COPYING for full copyright and warranty notices.)
.Pp
.Lk https://meli.delivery
.Po
See
.Pa COPYING
for full copyright and warranty notices.
.Pc
.Ss Links
.Bl -item -compact
.It
.Lk https://meli\-email.org "Website"
.It
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
.It
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
.It
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
.It
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
.El

View File

@ -22,7 +22,6 @@
//! Account management from user configuration.
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap, HashSet, VecDeque},
convert::TryFrom,
fs,
@ -36,17 +35,14 @@ use std::{
time::Duration,
};
use futures::{
future::FutureExt,
stream::{Stream, StreamExt},
};
use futures::{future::FutureExt, stream::StreamExt};
use indexmap::IndexMap;
use melib::{
backends::*,
email::*,
error::{Error, ErrorKind, Result},
log,
text_processing::GlobMatch,
text::GlobMatch,
thread::Threads,
AddressBook, Collection, LogLevel, SortField, SortOrder,
};
@ -57,11 +53,19 @@ use crate::command::actions::AccountAction;
use crate::{
conf::{AccountConf, FileMailboxConf},
jobs::{JobId, JoinHandle},
types::UIEvent::{self, EnvelopeRemove, EnvelopeRename, EnvelopeUpdate, Notification},
types::{
ForkType,
UIEvent::{self, EnvelopeRemove, EnvelopeRename, EnvelopeUpdate, Notification},
},
MainLoopHandler, StatusEvent, ThreadEvent,
};
mod backend_ops;
mod jobs;
mod mailbox;
pub use jobs::*;
pub use mailbox::*;
#[macro_export]
macro_rules! try_recv_timeout {
@ -79,6 +83,7 @@ macro_rules! try_recv_timeout {
}};
}
#[macro_export]
macro_rules! is_variant {
($n:ident, $($var:tt)+) => {
#[inline]
@ -88,86 +93,6 @@ macro_rules! is_variant {
};
}
#[derive(Clone, Debug, Default)]
pub enum MailboxStatus {
Available,
Failed(Error),
/// first argument is done work, and second is total work
Parsing(usize, usize),
#[default]
None,
}
impl MailboxStatus {
is_variant! { is_available, Available }
is_variant! { is_parsing, Parsing(_, _) }
}
#[derive(Clone, Debug)]
pub struct MailboxEntry {
pub status: MailboxStatus,
pub name: String,
pub path: String,
pub ref_mailbox: Mailbox,
pub conf: FileMailboxConf,
}
impl MailboxEntry {
pub fn new(
status: MailboxStatus,
name: String,
ref_mailbox: Mailbox,
conf: FileMailboxConf,
) -> Self {
let mut ret = Self {
status,
name,
path: ref_mailbox.path().into(),
ref_mailbox,
conf,
};
match ret.conf.mailbox_conf.extra.get("encoding") {
None => {}
Some(v) if ["utf-8", "utf8"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {}
Some(v) if ["utf-7", "utf7"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {
ret.name = melib::backends::utf7::decode_utf7_imap(&ret.name);
ret.path = melib::backends::utf7::decode_utf7_imap(&ret.path);
}
Some(other) => {
log::warn!(
"mailbox `{}`: unrecognized mailbox name charset: {}",
&ret.name,
other
);
}
}
ret
}
pub fn status(&self) -> String {
match self.status {
MailboxStatus::Available => format!(
"{} [{} messages]",
self.name(),
self.ref_mailbox.count().ok().unwrap_or((0, 0)).1
),
MailboxStatus::Failed(ref e) => e.to_string(),
MailboxStatus::None => "Retrieving mailbox.".to_string(),
MailboxStatus::Parsing(done, total) => {
format!("Parsing messages. [{}/{}]", done, total)
}
}
}
pub fn name(&self) -> &str {
if let Some(name) = self.conf.mailbox_conf.alias.as_ref() {
name
} else {
self.ref_mailbox.name()
}
}
}
#[derive(Clone, Debug, Default)]
pub enum IsOnline {
#[default]
@ -194,6 +119,19 @@ impl IsOnline {
};
}
}
pub fn is_recoverable(err: &Error) -> bool {
!(err.kind.is_authentication()
|| err.kind.is_configuration()
|| err.kind.is_bug()
|| err.kind.is_external()
|| (err.kind.is_network() && !err.kind.is_network_down())
|| err.kind.is_not_implemented()
|| err.kind.is_not_supported()
|| err.kind.is_protocol_error()
|| err.kind.is_protocol_not_supported()
|| err.kind.is_value_error())
}
}
#[derive(Debug)]
@ -204,7 +142,6 @@ pub struct Account {
pub mailbox_entries: IndexMap<MailboxHash, MailboxEntry>,
pub mailboxes_order: Vec<MailboxHash>,
pub tree: Vec<MailboxNode>,
pub sent_mailbox: Option<MailboxHash>,
pub collection: Collection,
pub address_book: AddressBook,
pub settings: AccountConf,
@ -217,199 +154,6 @@ pub struct Account {
pub backend_capabilities: MailBackendCapabilities,
}
pub enum JobRequest {
Mailboxes {
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
},
Fetch {
mailbox_hash: MailboxHash,
#[allow(clippy::type_complexity)]
handle: JoinHandle<(
Option<Result<Vec<Envelope>>>,
Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>,
)>,
},
Generic {
name: Cow<'static, str>,
log_level: LogLevel,
handle: JoinHandle<Result<()>>,
on_finish: Option<crate::types::CallbackFn>,
},
IsOnline {
handle: JoinHandle<Result<()>>,
},
Refresh {
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<()>>,
},
SetFlags {
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
flags: SmallVec<[FlagOp; 8]>,
handle: JoinHandle<Result<()>>,
},
SaveMessage {
bytes: Vec<u8>,
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<()>>,
},
SendMessage,
SendMessageBackground {
handle: JoinHandle<Result<()>>,
},
DeleteMessages {
env_hashes: EnvelopeHashBatch,
handle: JoinHandle<Result<()>>,
},
CreateMailbox {
path: String,
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
},
DeleteMailbox {
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
},
//RenameMailbox,
SetMailboxPermissions {
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<()>>,
},
SetMailboxSubscription {
mailbox_hash: MailboxHash,
new_value: bool,
handle: JoinHandle<Result<()>>,
},
Watch {
handle: JoinHandle<Result<()>>,
},
}
impl Drop for JobRequest {
fn drop(&mut self) {
match self {
JobRequest::Generic { handle, .. } |
JobRequest::IsOnline { handle, .. } |
JobRequest::Refresh { handle, .. } |
JobRequest::SetFlags { handle, .. } |
JobRequest::SaveMessage { handle, .. } |
//JobRequest::RenameMailbox,
JobRequest::SetMailboxPermissions { handle, .. } |
JobRequest::SetMailboxSubscription { handle, .. } |
JobRequest::Watch { handle, .. } |
JobRequest::SendMessageBackground { handle, .. } => {
handle.cancel();
}
JobRequest::DeleteMessages { handle, .. } => {
handle.cancel();
}
JobRequest::CreateMailbox { handle, .. } => {
handle.cancel();
}
JobRequest::DeleteMailbox { handle, .. } => {
handle.cancel();
}
JobRequest::Fetch { handle, .. } => {
handle.cancel();
}
JobRequest::Mailboxes { handle, .. } => {
handle.cancel();
}
JobRequest::SendMessage => {}
}
}
}
impl std::fmt::Debug for JobRequest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
JobRequest::Generic { name, .. } => write!(f, "JobRequest::Generic({})", name),
JobRequest::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
JobRequest::Fetch { mailbox_hash, .. } => {
write!(f, "JobRequest::Fetch({})", mailbox_hash)
}
JobRequest::IsOnline { .. } => write!(f, "JobRequest::IsOnline"),
JobRequest::Refresh { .. } => write!(f, "JobRequest::Refresh"),
JobRequest::SetFlags {
env_hashes,
mailbox_hash,
flags,
..
} => f
.debug_struct(stringify!(JobRequest::SetFlags))
.field("env_hashes", &env_hashes)
.field("mailbox_hash", &mailbox_hash)
.field("flags", &flags)
.finish(),
JobRequest::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"),
JobRequest::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"),
JobRequest::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
JobRequest::DeleteMailbox { mailbox_hash, .. } => {
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
}
//JobRequest::RenameMailbox,
JobRequest::SetMailboxPermissions { .. } => {
write!(f, "JobRequest::SetMailboxPermissions")
}
JobRequest::SetMailboxSubscription { .. } => {
write!(f, "JobRequest::SetMailboxSubscription")
}
JobRequest::Watch { .. } => write!(f, "JobRequest::Watch"),
JobRequest::SendMessage => write!(f, "JobRequest::SendMessage"),
JobRequest::SendMessageBackground { .. } => {
write!(f, "JobRequest::SendMessageBackground")
}
}
}
}
impl std::fmt::Display for JobRequest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
JobRequest::Generic { name, .. } => write!(f, "{}", name),
JobRequest::Mailboxes { .. } => write!(f, "Get mailbox list"),
JobRequest::Fetch { .. } => write!(f, "Mailbox fetch"),
JobRequest::IsOnline { .. } => write!(f, "Online status check"),
JobRequest::Refresh { .. } => write!(f, "Refresh mailbox"),
JobRequest::SetFlags {
env_hashes, flags, ..
} => write!(
f,
"Set flags for {} message{}: {:?}",
env_hashes.len(),
if env_hashes.len() == 1 { "" } else { "s" },
flags
),
JobRequest::SaveMessage { .. } => write!(f, "Save message"),
JobRequest::DeleteMessages { env_hashes, .. } => write!(
f,
"Delete {} message{}",
env_hashes.len(),
if env_hashes.len() == 1 { "" } else { "s" }
),
JobRequest::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
JobRequest::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
//JobRequest::RenameMailbox,
JobRequest::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
JobRequest::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
JobRequest::Watch { .. } => write!(f, "Background watch"),
JobRequest::SendMessageBackground { .. } | JobRequest::SendMessage => {
write!(f, "Sending message")
}
}
}
}
impl JobRequest {
is_variant! { is_watch, Watch { .. } }
is_variant! { is_online, IsOnline { .. } }
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
matches!(self, JobRequest::Fetch {
mailbox_hash: h, ..
} if *h == mailbox_hash)
}
}
impl Drop for Account {
fn drop(&mut self) {
if let Ok(data_dir) = xdg::BaseDirectories::with_profile("meli", &self.name) {
@ -461,15 +205,6 @@ impl Drop for Account {
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct MailboxNode {
pub hash: MailboxHash,
pub depth: usize,
pub indentation: u32,
pub has_sibling: bool,
pub children: Vec<MailboxNode>,
}
impl Account {
pub fn new(
hash: AccountHash,
@ -552,7 +287,7 @@ impl Account {
#[cfg(feature = "sqlite3")]
if settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
let db_path = match crate::sqlite3::db_path() {
let db_path = match crate::sqlite3::AccountCache::db_path(&name) {
Err(err) => {
main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
@ -562,7 +297,7 @@ impl Account {
)));
None
}
Ok(path) => Some(path),
Ok(path) => path,
};
if let Some(db_path) = db_path {
if !db_path.exists() {
@ -578,7 +313,7 @@ impl Account {
}
}
Ok(Account {
Ok(Self {
hash,
name,
is_online: if !backend.capabilities().is_remote {
@ -590,7 +325,6 @@ impl Account {
mailboxes_order: Default::default(),
tree: Default::default(),
address_book,
sent_mailbox: Default::default(),
collection: backend.collection(),
settings,
main_loop_handler,
@ -608,8 +342,6 @@ impl Account {
IndexMap::with_capacity_and_hasher(ref_mailboxes.len(), Default::default());
let mut mailboxes_order: Vec<MailboxHash> = Vec::with_capacity(ref_mailboxes.len());
let mut sent_mailbox = None;
/* Keep track of which mailbox config values we encounter in the actual
* mailboxes returned by the backend. For each of the actual
* mailboxes, delete the key from the hash set. If any are left, they
@ -621,9 +353,19 @@ impl Account {
.keys()
.cloned()
.collect::<HashSet<String>>();
let mut default_mailbox = self
.settings
.conf
.default_mailbox
.clone()
.into_iter()
.collect::<HashSet<String>>();
for f in ref_mailboxes.values_mut() {
if let Some(conf) = self.settings.mailbox_confs.get_mut(f.path()) {
mailbox_conf_hash_set.remove(f.path());
if default_mailbox.remove(f.path()) {
self.settings.default_mailbox = Some(f.hash());
}
conf.mailbox_conf.usage = if f.special_usage() != SpecialUsageMailbox::Normal {
Some(f.special_usage())
} else {
@ -635,11 +377,11 @@ impl Account {
};
match conf.mailbox_conf.usage {
Some(SpecialUsageMailbox::Sent) => {
sent_mailbox = Some(f.hash());
self.settings.sent_mailbox = Some(f.hash());
}
None => {
if f.special_usage() == SpecialUsageMailbox::Sent {
sent_mailbox = Some(f.hash());
self.settings.sent_mailbox = Some(f.hash());
}
}
_ => {}
@ -665,7 +407,7 @@ impl Account {
tmp
};
if new.mailbox_conf.usage == Some(SpecialUsageMailbox::Sent) {
sent_mailbox = Some(f.hash());
self.settings.sent_mailbox = Some(f.hash());
}
mailbox_entries.insert(
@ -718,6 +460,23 @@ impl Account {
)));
}
match self.settings.conf.default_mailbox {
Some(ref v) if !default_mailbox.is_empty() => {
let err = Error::new(format!(
"Account `{}` has default mailbox set as `{}` but it doesn't exist.",
&self.name, v
))
.set_kind(ErrorKind::Configuration);
self.is_online.set_err(err.clone());
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash, None,
)));
return Err(err);
}
_ => {}
}
let mut tree: Vec<MailboxNode> = Vec::new();
for (h, f) in ref_mailboxes.iter() {
if !f.is_subscribed() {
@ -766,7 +525,6 @@ impl Account {
self.mailboxes_order = mailboxes_order;
self.mailbox_entries = mailbox_entries;
self.tree = tree;
self.sent_mailbox = sent_mailbox;
Ok(())
}
@ -872,9 +630,9 @@ impl Account {
};
#[cfg(feature = "sqlite3")]
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
let handle = self.main_loop_handler.job_executor.spawn_blocking(
let handle = self.main_loop_handler.job_executor.spawn_specialized(
"sqlite3::insert".into(),
crate::sqlite3::insert(
crate::sqlite3::AccountCache::insert(
(*envelope).clone(),
self.backend.clone(),
self.name.clone(),
@ -951,15 +709,18 @@ impl Account {
}
#[cfg(feature = "sqlite3")]
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
if let Err(err) = crate::sqlite3::remove(env_hash) {
let envelopes = self.collection.envelopes.read().unwrap();
log::error!(
"Failed to remove envelope {} [{}] in cache: {}",
&envelopes[&env_hash].message_id_display(),
env_hash,
err
);
}
let fut = crate::sqlite3::AccountCache::remove(self.name.clone(), env_hash);
let handle = self
.main_loop_handler
.job_executor
.spawn_specialized("remove envelope from cache".into(), fut);
self.insert_job(
handle.job_id,
JobRequest::Refresh {
mailbox_hash,
handle,
},
);
}
let thread_hash = self.collection.get_env(env_hash).thread();
if !self
@ -1009,6 +770,7 @@ impl Account {
}
None
}
pub fn refresh(&mut self, mailbox_hash: MailboxHash) -> Result<()> {
if let Some(ref refresh_command) = self.settings.conf().refresh_command {
let child = std::process::Command::new("sh")
@ -1022,9 +784,11 @@ impl Account {
StatusEvent::DisplayMessage(format!("Running command {}", refresh_command)),
)));
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Fork(
crate::ForkType::Generic(child),
)));
.send(ThreadEvent::UIEvent(UIEvent::Fork(ForkType::Generic {
id: refresh_command.to_string().into(),
command: Some(refresh_command.clone().into()),
child,
})));
return Ok(());
}
let refresh_job = self.backend.write().unwrap().refresh(mailbox_hash);
@ -1050,7 +814,9 @@ impl Account {
}
pub fn watch(&mut self) {
if self.settings.account().manual_refresh {
if self.settings.account().manual_refresh
|| matches!(self.is_online, IsOnline::Err { ref value, ..} if !IsOnline::is_recoverable(value))
{
return;
}
@ -1107,9 +873,10 @@ impl Account {
ret
}
pub fn mailboxes_order(&self) -> &Vec<MailboxHash> {
pub fn mailboxes_order(&self) -> &[MailboxHash] {
&self.mailboxes_order
}
pub fn name(&self) -> &str {
&self.name
}
@ -1568,17 +1335,7 @@ impl Account {
ref mut retries,
} => {
let ret = Err(value.clone());
if value.kind.is_authentication()
|| value.kind.is_bug()
|| value.kind.is_configuration()
|| value.kind.is_external()
|| (value.kind.is_network() && !value.kind.is_network_down())
|| value.kind.is_not_implemented()
|| value.kind.is_not_supported()
|| value.kind.is_protocol_error()
|| value.kind.is_protocol_not_supported()
|| value.kind.is_value_error()
{
if !IsOnline::is_recoverable(value) {
return ret;
}
let wait = if value.kind.is_timeout()
@ -1590,7 +1347,7 @@ impl Account {
*retries *= 2;
}
Some(Duration::from_millis(
oldval * (4 * melib::utils::random::random_u8() as u64),
oldval * (4 * u64::from(melib::utils::random::random_u8())),
))
} else {
None
@ -1643,7 +1400,9 @@ impl Account {
let query = melib::search::Query::try_from(search_term)?;
match self.settings.conf.search_backend {
#[cfg(feature = "sqlite3")]
crate::conf::SearchBackend::Sqlite3 => crate::sqlite3::search(&query, _sort),
crate::conf::SearchBackend::Sqlite3 => Ok(Box::pin(
crate::sqlite3::AccountCache::search(self.name.clone(), query, _sort),
)),
crate::conf::SearchBackend::Auto | crate::conf::SearchBackend::None => {
if self.backend_capabilities.supports_search {
self.backend
@ -1667,6 +1426,12 @@ impl Account {
}
}
pub fn default_mailbox(&self) -> Option<MailboxHash> {
self.settings
.default_mailbox
.or_else(|| Some(*self.mailboxes_order.first()?))
}
pub fn mailbox_by_path(&self, path: &str) -> Result<MailboxHash> {
if let Some((mailbox_hash, _)) = self
.mailbox_entries
@ -1691,21 +1456,22 @@ impl Account {
JobRequest::Mailboxes { ref mut handle } => {
if let Ok(Some(mailboxes)) = handle.chan.try_recv() {
if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) {
if err.kind.is_authentication() {
if !IsOnline::is_recoverable(&err) {
self.main_loop_handler.send(ThreadEvent::UIEvent(
UIEvent::Notification {
title: Some(
format!("{}: authentication error", &self.name).into(),
),
source: None,
title: Some(self.name.clone().into()),
source: Some(err.clone()),
body: err.to_string().into(),
kind: Some(crate::types::NotificationType::Error(err.kind)),
},
));
self.is_online.set_err(err);
self.main_loop_handler.send(ThreadEvent::UIEvent(
UIEvent::AccountStatusChange(self.hash, None),
UIEvent::AccountStatusChange(
self.hash,
Some(err.to_string().into()),
),
));
self.is_online.set_err(err);
self.main_loop_handler
.job_executor
.set_job_success(job_id, false);
@ -1808,10 +1574,11 @@ impl Account {
.into_iter()
.map(|e| (e.hash(), e))
.collect::<HashMap<EnvelopeHash, Envelope>>();
if let Some(updated_mailboxes) =
self.collection
.merge(envelopes, mailbox_hash, self.sent_mailbox)
{
if let Some(updated_mailboxes) = self.collection.merge(
envelopes,
mailbox_hash,
self.settings.sent_mailbox,
) {
for f in updated_mailboxes {
self.main_loop_handler.send(ThreadEvent::UIEvent(
UIEvent::MailboxUpdate((self.hash, f)),
@ -1825,13 +1592,17 @@ impl Account {
}
}
JobRequest::IsOnline { ref mut handle, .. } => {
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !IsOnline::is_recoverable(value))
{
return true;
}
if let Ok(Some(is_online)) = handle.chan.try_recv() {
self.main_loop_handler.send(ThreadEvent::UIEvent(
UIEvent::AccountStatusChange(self.hash, None),
));
match is_online {
Ok(()) => {
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !value.kind.is_authentication())
if matches!(self.is_online, IsOnline::Err { .. })
|| matches!(self.is_online, IsOnline::Uninit)
{
self.watch();
@ -1840,7 +1611,7 @@ impl Account {
return true;
}
Err(value) => {
self.is_online = IsOnline::Err { value, retries: 1 };
self.is_online.set_err(value);
}
}
}
@ -1859,12 +1630,15 @@ impl Account {
};
}
JobRequest::Refresh { ref mut handle, .. } => {
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !IsOnline::is_recoverable(value))
{
return true;
}
match handle.chan.try_recv() {
Err(_) => { /* canceled */ }
Ok(None) => {}
Ok(Some(Ok(()))) => {
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !value.kind.is_authentication())
{
if matches!(self.is_online, IsOnline::Err { .. }) {
self.watch();
}
self.is_online = IsOnline::True;
@ -1939,12 +1713,11 @@ impl Account {
drop(env_lck);
self.update_cached_env(env, None);
}
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::EnvelopeUpdate(env_hash)));
}
for env_hash in env_hashes.iter() {
self.collection.update_flags(env_hash, *mailbox_hash);
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::EnvelopeUpdate(env_hash)));
}
}
Err(_) | Ok(None) => {}
@ -2151,8 +1924,8 @@ impl Account {
{
self.tree.remove(pos);
}
if self.sent_mailbox == Some(mailbox_hash) {
self.sent_mailbox = None;
if self.settings.sent_mailbox == Some(mailbox_hash) {
self.settings.sent_mailbox = None;
}
self.collection
.threads
@ -2404,226 +2177,3 @@ impl IndexMut<&MailboxHash> for Account {
self.mailbox_entries.get_mut(index).unwrap()
}
}
fn build_mailboxes_order(
tree: &mut Vec<MailboxNode>,
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
mailboxes_order: &mut Vec<MailboxHash>,
) {
tree.clear();
mailboxes_order.clear();
for (h, f) in mailbox_entries.iter() {
if f.ref_mailbox.parent().is_none() {
fn rec(
h: MailboxHash,
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
depth: usize,
) -> MailboxNode {
let mut node = MailboxNode {
hash: h,
children: Vec::new(),
depth,
indentation: 0,
has_sibling: false,
};
for &c in mailbox_entries[&h].ref_mailbox.children() {
if mailbox_entries.contains_key(&c) {
node.children.push(rec(c, mailbox_entries, depth + 1));
}
}
node
}
tree.push(rec(*h, mailbox_entries, 0));
}
}
macro_rules! mailbox_eq_key {
($mailbox:expr) => {{
if let Some(sort_order) = $mailbox.conf.mailbox_conf.sort_order {
(0, sort_order, $mailbox.ref_mailbox.path())
} else {
(1, 0, $mailbox.ref_mailbox.path())
}
}};
}
tree.sort_unstable_by(|a, b| {
if mailbox_entries[&b.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&b.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Greater
} else if mailbox_entries[&a.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&a.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Less
} else {
mailbox_eq_key!(mailbox_entries[&a.hash])
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
}
});
let mut stack: SmallVec<[Option<&MailboxNode>; 16]> = SmallVec::new();
for n in tree.iter_mut() {
mailboxes_order.push(n.hash);
n.children.sort_unstable_by(|a, b| {
if mailbox_entries[&b.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&b.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Greater
} else if mailbox_entries[&a.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&a.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Less
} else {
mailbox_eq_key!(mailbox_entries[&a.hash])
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
}
});
stack.extend(n.children.iter().rev().map(Some));
while let Some(Some(next)) = stack.pop() {
mailboxes_order.push(next.hash);
stack.extend(next.children.iter().rev().map(Some));
}
}
drop(stack);
for node in tree.iter_mut() {
fn rec(
node: &mut MailboxNode,
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
mut indentation: u32,
has_sibling: bool,
) {
node.indentation = indentation;
node.has_sibling = has_sibling;
let mut iter = (0..node.children.len())
.filter(|i| {
mailbox_entries[&node.children[*i].hash]
.ref_mailbox
.is_subscribed()
})
.collect::<SmallVec<[_; 8]>>()
.into_iter()
.peekable();
if has_sibling {
indentation <<= 1;
indentation |= 1;
} else {
indentation <<= 1;
}
while let Some(i) = iter.next() {
let c = &mut node.children[i];
rec(c, mailbox_entries, indentation, iter.peek().is_some());
}
}
rec(node, mailbox_entries, 0, false);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mailbox_utf7() {
#[derive(Debug)]
struct TestMailbox(String);
impl melib::BackendMailbox for TestMailbox {
fn hash(&self) -> MailboxHash {
unimplemented!()
}
fn name(&self) -> &str {
&self.0
}
fn path(&self) -> &str {
&self.0
}
fn children(&self) -> &[MailboxHash] {
unimplemented!()
}
fn clone(&self) -> Mailbox {
unimplemented!()
}
fn special_usage(&self) -> SpecialUsageMailbox {
unimplemented!()
}
fn parent(&self) -> Option<MailboxHash> {
unimplemented!()
}
fn permissions(&self) -> MailboxPermissions {
unimplemented!()
}
fn is_subscribed(&self) -> bool {
unimplemented!()
}
fn set_is_subscribed(&mut self, _: bool) -> Result<()> {
unimplemented!()
}
fn set_special_usage(&mut self, _: SpecialUsageMailbox) -> Result<()> {
unimplemented!()
}
fn count(&self) -> Result<(usize, usize)> {
unimplemented!()
}
}
for (n, d) in [
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-", "Отправленные"),
] {
let ref_mbox = TestMailbox(n.to_string());
let mut conf: melib::MailboxConf = Default::default();
conf.extra.insert("encoding".to_string(), "utf7".into());
let entry = MailboxEntry::new(
MailboxStatus::None,
n.to_string(),
Box::new(ref_mbox),
FileMailboxConf {
mailbox_conf: conf,
..Default::default()
},
);
assert_eq!(&entry.path, d);
}
}
}

View File

@ -60,28 +60,31 @@ impl Account {
pub(super) fn update_cached_env(&mut self, env: Envelope, old_hash: Option<EnvelopeHash>) {
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
let msg_id = env.message_id_display().to_string();
match crate::sqlite3::remove(old_hash.unwrap_or(env.hash()))
.map(|_| crate::sqlite3::insert(env, self.backend.clone(), self.name.clone()))
{
Ok(job) => {
let handle = self
.main_loop_handler
.job_executor
.spawn_blocking("sqlite3::remove".into(), job);
self.insert_job(
handle.job_id,
JobRequest::Generic {
name: format!("Update envelope {} in sqlite3 cache", msg_id).into(),
handle,
log_level: LogLevel::TRACE,
on_finish: None,
},
);
}
Err(err) => {
log::error!("Failed to update envelope {} in cache: {}", msg_id, err);
}
}
let name = self.name.clone();
let backend = self.backend.clone();
let fut = async move {
crate::sqlite3::AccountCache::remove(
name.clone(),
old_hash.unwrap_or_else(|| env.hash()),
)
.await?;
crate::sqlite3::AccountCache::insert(env, backend, name).await?;
Ok(())
};
let handle = self
.main_loop_handler
.job_executor
.spawn_specialized("sqlite3::remove".into(), fut);
self.insert_job(
handle.job_id,
JobRequest::Generic {
name: format!("Update envelope {} in sqlite3 cache", msg_id).into(),
handle,
log_level: LogLevel::TRACE,
on_finish: None,
},
);
}
}
}

View File

@ -0,0 +1,222 @@
//
// meli - accounts module.
//
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use std::{borrow::Cow, collections::HashMap, pin::Pin};
use futures::stream::Stream;
use melib::{backends::*, email::*, error::Result, LogLevel};
use smallvec::SmallVec;
use crate::{is_variant, jobs::JoinHandle};
pub enum JobRequest {
Mailboxes {
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
},
Fetch {
mailbox_hash: MailboxHash,
#[allow(clippy::type_complexity)]
handle: JoinHandle<(
Option<Result<Vec<Envelope>>>,
Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>,
)>,
},
Generic {
name: Cow<'static, str>,
log_level: LogLevel,
handle: JoinHandle<Result<()>>,
on_finish: Option<crate::types::CallbackFn>,
},
IsOnline {
handle: JoinHandle<Result<()>>,
},
Refresh {
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<()>>,
},
SetFlags {
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
flags: SmallVec<[FlagOp; 8]>,
handle: JoinHandle<Result<()>>,
},
SaveMessage {
bytes: Vec<u8>,
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<()>>,
},
SendMessage,
SendMessageBackground {
handle: JoinHandle<Result<()>>,
},
DeleteMessages {
env_hashes: EnvelopeHashBatch,
handle: JoinHandle<Result<()>>,
},
CreateMailbox {
path: String,
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
},
DeleteMailbox {
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
},
//RenameMailbox,
SetMailboxPermissions {
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<()>>,
},
SetMailboxSubscription {
mailbox_hash: MailboxHash,
new_value: bool,
handle: JoinHandle<Result<()>>,
},
Watch {
handle: JoinHandle<Result<()>>,
},
}
impl Drop for JobRequest {
fn drop(&mut self) {
match self {
Self::Generic { handle, .. } |
Self::IsOnline { handle, .. } |
Self::Refresh { handle, .. } |
Self::SetFlags { handle, .. } |
Self::SaveMessage { handle, .. } |
//JobRequest::RenameMailbox,
Self::SetMailboxPermissions { handle, .. } |
Self::SetMailboxSubscription { handle, .. } |
Self::Watch { handle, .. } |
Self::SendMessageBackground { handle, .. } => {
handle.cancel();
}
Self::DeleteMessages { handle, .. } => {
handle.cancel();
}
Self::CreateMailbox { handle, .. } => {
handle.cancel();
}
Self::DeleteMailbox { handle, .. } => {
handle.cancel();
}
Self::Fetch { handle, .. } => {
handle.cancel();
}
Self::Mailboxes { handle, .. } => {
handle.cancel();
}
Self::SendMessage => {}
}
}
}
impl std::fmt::Debug for JobRequest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Generic { name, .. } => write!(f, "JobRequest::Generic({})", name),
Self::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
Self::Fetch { mailbox_hash, .. } => {
write!(f, "JobRequest::Fetch({})", mailbox_hash)
}
Self::IsOnline { .. } => write!(f, "JobRequest::IsOnline"),
Self::Refresh { .. } => write!(f, "JobRequest::Refresh"),
Self::SetFlags {
env_hashes,
mailbox_hash,
flags,
..
} => f
.debug_struct(stringify!(JobRequest::SetFlags))
.field("env_hashes", &env_hashes)
.field("mailbox_hash", &mailbox_hash)
.field("flags", &flags)
.finish(),
Self::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"),
Self::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"),
Self::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
Self::DeleteMailbox { mailbox_hash, .. } => {
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
}
//JobRequest::RenameMailbox,
Self::SetMailboxPermissions { .. } => {
write!(f, "JobRequest::SetMailboxPermissions")
}
Self::SetMailboxSubscription { .. } => {
write!(f, "JobRequest::SetMailboxSubscription")
}
Self::Watch { .. } => write!(f, "JobRequest::Watch"),
Self::SendMessage => write!(f, "JobRequest::SendMessage"),
Self::SendMessageBackground { .. } => {
write!(f, "JobRequest::SendMessageBackground")
}
}
}
}
impl std::fmt::Display for JobRequest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Generic { name, .. } => write!(f, "{}", name),
Self::Mailboxes { .. } => write!(f, "Get mailbox list"),
Self::Fetch { .. } => write!(f, "Mailbox fetch"),
Self::IsOnline { .. } => write!(f, "Online status check"),
Self::Refresh { .. } => write!(f, "Refresh mailbox"),
Self::SetFlags {
env_hashes, flags, ..
} => write!(
f,
"Set flags for {} message{}: {:?}",
env_hashes.len(),
if env_hashes.len() == 1 { "" } else { "s" },
flags
),
Self::SaveMessage { .. } => write!(f, "Save message"),
Self::DeleteMessages { env_hashes, .. } => write!(
f,
"Delete {} message{}",
env_hashes.len(),
if env_hashes.len() == 1 { "" } else { "s" }
),
Self::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
Self::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
//JobRequest::RenameMailbox,
Self::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
Self::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
Self::Watch { .. } => write!(f, "Background watch"),
Self::SendMessageBackground { .. } | Self::SendMessage => {
write!(f, "Sending message")
}
}
}
}
impl JobRequest {
is_variant! { is_watch, Watch { .. } }
is_variant! { is_online, IsOnline { .. } }
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
matches!(self, Self::Fetch {
mailbox_hash: h, ..
} if *h == mailbox_hash)
}
}

View File

@ -0,0 +1,347 @@
//
// meli - accounts module.
//
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use indexmap::IndexMap;
use melib::{
backends::{Mailbox, MailboxHash},
error::Error,
log,
};
use smallvec::SmallVec;
use crate::{conf::FileMailboxConf, is_variant};
#[derive(Clone, Debug, Default)]
pub enum MailboxStatus {
Available,
Failed(Error),
/// first argument is done work, and second is total work
Parsing(usize, usize),
#[default]
None,
}
impl MailboxStatus {
is_variant! { is_available, Available }
is_variant! { is_parsing, Parsing(_, _) }
}
#[derive(Clone, Debug)]
pub struct MailboxEntry {
pub status: MailboxStatus,
pub name: String,
pub path: String,
pub ref_mailbox: Mailbox,
pub conf: FileMailboxConf,
}
impl MailboxEntry {
pub fn new(
status: MailboxStatus,
name: String,
ref_mailbox: Mailbox,
conf: FileMailboxConf,
) -> Self {
let mut ret = Self {
status,
name,
path: ref_mailbox.path().into(),
ref_mailbox,
conf,
};
match ret.conf.mailbox_conf.extra.get("encoding") {
None => {}
Some(v) if ["utf-8", "utf8"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {}
Some(v) if ["utf-7", "utf7"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {
ret.name = melib::backends::utf7::decode_utf7_imap(&ret.name);
ret.path = melib::backends::utf7::decode_utf7_imap(&ret.path);
}
Some(other) => {
log::warn!(
"mailbox `{}`: unrecognized mailbox name charset: {}",
&ret.name,
other
);
}
}
ret
}
pub fn status(&self) -> String {
match self.status {
MailboxStatus::Available => format!(
"{} [{} messages]",
self.name(),
self.ref_mailbox.count().ok().unwrap_or((0, 0)).1
),
MailboxStatus::Failed(ref e) => e.to_string(),
MailboxStatus::None => "Retrieving mailbox.".to_string(),
MailboxStatus::Parsing(done, total) => {
format!("Parsing messages. [{}/{}]", done, total)
}
}
}
pub fn name(&self) -> &str {
if let Some(name) = self.conf.mailbox_conf.alias.as_ref() {
name
} else {
self.ref_mailbox.name()
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct MailboxNode {
pub hash: MailboxHash,
pub depth: usize,
pub indentation: u32,
pub has_sibling: bool,
pub children: Vec<MailboxNode>,
}
pub fn build_mailboxes_order(
tree: &mut Vec<MailboxNode>,
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
mailboxes_order: &mut Vec<MailboxHash>,
) {
tree.clear();
mailboxes_order.clear();
for (h, f) in mailbox_entries.iter() {
if f.ref_mailbox.parent().is_none() {
fn rec(
h: MailboxHash,
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
depth: usize,
) -> MailboxNode {
let mut node = MailboxNode {
hash: h,
children: Vec::new(),
depth,
indentation: 0,
has_sibling: false,
};
for &c in mailbox_entries[&h].ref_mailbox.children() {
if mailbox_entries.contains_key(&c) {
node.children.push(rec(c, mailbox_entries, depth + 1));
}
}
node
}
tree.push(rec(*h, mailbox_entries, 0));
}
}
macro_rules! mailbox_eq_key {
($mailbox:expr) => {{
if let Some(sort_order) = $mailbox.conf.mailbox_conf.sort_order {
(0, sort_order, $mailbox.ref_mailbox.path())
} else {
(1, 0, $mailbox.ref_mailbox.path())
}
}};
}
tree.sort_unstable_by(|a, b| {
if mailbox_entries[&b.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&b.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Greater
} else if mailbox_entries[&a.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&a.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Less
} else {
mailbox_eq_key!(mailbox_entries[&a.hash])
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
}
});
let mut stack: SmallVec<[Option<&MailboxNode>; 16]> = SmallVec::new();
for n in tree.iter_mut() {
mailboxes_order.push(n.hash);
n.children.sort_unstable_by(|a, b| {
if mailbox_entries[&b.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&b.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Greater
} else if mailbox_entries[&a.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&a.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Less
} else {
mailbox_eq_key!(mailbox_entries[&a.hash])
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
}
});
stack.extend(n.children.iter().rev().map(Some));
while let Some(Some(next)) = stack.pop() {
mailboxes_order.push(next.hash);
stack.extend(next.children.iter().rev().map(Some));
}
}
drop(stack);
for node in tree.iter_mut() {
fn rec(
node: &mut MailboxNode,
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
mut indentation: u32,
has_sibling: bool,
) {
node.indentation = indentation;
node.has_sibling = has_sibling;
let mut iter = (0..node.children.len())
.filter(|i| {
mailbox_entries[&node.children[*i].hash]
.ref_mailbox
.is_subscribed()
})
.collect::<SmallVec<[_; 8]>>()
.into_iter()
.peekable();
indentation <<= 1;
if has_sibling {
indentation |= 1;
}
while let Some(i) = iter.next() {
let c = &mut node.children[i];
rec(c, mailbox_entries, indentation, iter.peek().is_some());
}
}
rec(node, mailbox_entries, 0, false);
}
}
#[cfg(test)]
mod tests {
use melib::{
backends::{Mailbox, MailboxHash},
error::Result,
MailboxPermissions, SpecialUsageMailbox,
};
use crate::accounts::{FileMailboxConf, MailboxEntry, MailboxStatus};
#[test]
fn test_mailbox_utf7() {
#[derive(Debug)]
struct TestMailbox(String);
impl melib::BackendMailbox for TestMailbox {
fn hash(&self) -> MailboxHash {
unimplemented!()
}
fn name(&self) -> &str {
&self.0
}
fn path(&self) -> &str {
&self.0
}
fn children(&self) -> &[MailboxHash] {
unimplemented!()
}
fn clone(&self) -> Mailbox {
unimplemented!()
}
fn special_usage(&self) -> SpecialUsageMailbox {
unimplemented!()
}
fn parent(&self) -> Option<MailboxHash> {
unimplemented!()
}
fn permissions(&self) -> MailboxPermissions {
unimplemented!()
}
fn is_subscribed(&self) -> bool {
unimplemented!()
}
fn set_is_subscribed(&mut self, _: bool) -> Result<()> {
unimplemented!()
}
fn set_special_usage(&mut self, _: SpecialUsageMailbox) -> Result<()> {
unimplemented!()
}
fn count(&self) -> Result<(usize, usize)> {
unimplemented!()
}
}
for (n, d) in [
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-", "Отправленные"),
] {
let ref_mbox = TestMailbox(n.to_string());
let mut conf: melib::MailboxConf = Default::default();
conf.extra.insert("encoding".to_string(), "utf7".into());
let entry = MailboxEntry::new(
MailboxStatus::None,
n.to_string(),
Box::new(ref_mbox),
FileMailboxConf {
mailbox_conf: conf,
..Default::default()
},
);
assert_eq!(&entry.path, d);
}
}
}

View File

@ -22,6 +22,8 @@
//! Command line arguments.
use super::*;
#[cfg(feature = "cli-docs")]
use crate::manpages;
#[derive(Debug, StructOpt)]
#[structopt(name = "meli", about = "terminal mail client", version_short = "v")]
@ -40,13 +42,16 @@ pub enum SubCommand {
PrintDefaultTheme,
/// print loaded themes in full to stdout and exit.
PrintLoadedThemes,
/// print all paths that meli creates/uses.
PrintUsedPaths,
/// print all directories that meli creates/uses.
PrintAppDirectories,
/// print location of configuration file that will be loaded on normal app
/// startup.
PrintConfigPath,
/// edit configuration files with `$EDITOR`/`$VISUAL`.
EditConfig,
/// create a sample configuration file with available configuration options.
/// If PATH is not specified, meli will try to create it in
/// $XDG_CONFIG_HOME/meli/config.toml
/// If `PATH` is not specified, meli will try to create it in
/// `$XDG_CONFIG_HOME/meli/config.toml`
#[structopt(display_order = 1)]
CreateConfig {
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
@ -64,15 +69,17 @@ pub enum SubCommand {
Man(ManOpt),
#[structopt(display_order = 4)]
/// Install manual pages to the first location provided by $MANPATH /
/// manpath(1), unless you specify the directory as an argument.
/// Install manual pages to the first location provided by `$MANPATH` /
/// `manpath(1)`, unless you specify the directory as an argument.
InstallMan {
#[structopt(value_name = "DESTINATION_PATH", parse(from_os_str))]
destination_path: Option<PathBuf>,
},
#[structopt(display_order = 5)]
/// print compile time feature flags of this binary
/// Print compile time feature flags of this binary
CompiledWith,
/// Print log file location.
PrintLogPath,
/// View mail from input file.
View {
#[structopt(value_name = "INPUT", parse(from_os_str))]
@ -82,126 +89,15 @@ pub enum SubCommand {
#[derive(Debug, StructOpt)]
pub struct ManOpt {
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], value_name="PAGE", parse(try_from_str = manpages::parse_manpage))]
#[cfg(feature = "cli-docs")]
#[cfg_attr(feature = "cli-docs", structopt(default_value = "meli", possible_values=manpages::POSSIBLE_VALUES, value_name="PAGE", parse(try_from_str = manpages::parse_manpage)))]
/// Name of manual page.
pub page: manpages::ManPages,
/// If true, output text in stdout instead of spawning $PAGER.
#[structopt(long = "no-raw", alias = "no-raw", value_name = "bool")]
/// If true, output text in stdout instead of spawning `$PAGER`.
#[cfg(feature = "cli-docs")]
#[cfg_attr(
feature = "cli-docs",
structopt(long = "no-raw", alias = "no-raw", value_name = "bool")
)]
pub no_raw: Option<Option<bool>>,
}
#[cfg(feature = "cli-docs")]
pub mod manpages {
use std::{
env, fs,
path::{Path, PathBuf},
sync::Arc,
};
use melib::log;
use crate::{Error, Result};
pub fn parse_manpage(src: &str) -> Result<ManPages> {
match src {
"" | "meli" | "meli.1" | "main" => Ok(ManPages::Main),
"meli.7" | "guide" => Ok(ManPages::Guide),
"meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
"meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => {
Ok(ManPages::Themes)
}
_ => Err(Error::new(format!("Invalid documentation page: {src}",))),
}
}
#[derive(Clone, Copy, Debug)]
/// Choose manpage
pub enum ManPages {
/// meli(1)
Main = 0,
/// meli.conf(5)
Conf = 1,
/// meli-themes(5)
Themes = 2,
/// meli(7)
Guide = 3,
}
impl std::fmt::Display for ManPages {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
fmt,
"{}",
match self {
Self::Main => "meli.1",
Self::Conf => "meli.conf.5",
Self::Themes => "meli-themes.5",
Self::Guide => "meli.7",
}
)
}
}
impl ManPages {
pub fn install(destination: Option<PathBuf>) -> Result<PathBuf> {
fn path_valid(p: &Path, tries: &mut Vec<PathBuf>) -> bool {
tries.push(p.into());
p.exists()
&& p.is_dir()
&& fs::metadata(p)
.ok()
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
}
let mut tries = vec![];
let Some(mut path) = destination
.filter(|p| path_valid(p, &mut tries))
.or_else(|| {
if let Some(paths) = env::var_os("MANPATH") {
if let Some(path) =
env::split_paths(&paths).find(|p| path_valid(p, &mut tries))
{
return Some(path);
}
}
None
})
.or_else(|| {
#[allow(deprecated)]
env::home_dir()
.map(|p| p.join("local").join("share"))
.filter(|p| path_valid(p, &mut tries))
})
else {
return Err(format!("Could not write to any of these paths: {:?}", tries).into());
};
for (p, dir) in [
(ManPages::Main, "man1"),
(ManPages::Conf, "man5"),
(ManPages::Themes, "man5"),
(ManPages::Guide, "man7"),
] {
let text = crate::subcommands::man(p, true)?;
path.push(dir);
std::fs::create_dir_all(&path).map_err(|err| {
Error::new(format!("Could not create {} directory.", path.display()))
.set_source(Some(Arc::new(err)))
})?;
path.push(&p.to_string());
fs::write(&path, text.as_bytes()).map_err(|err| {
Error::new(format!("Could not write to {}", path.display()))
.set_source(Some(Arc::new(err)))
})?;
log::trace!("Installed {} to {}", p, path.display());
path.pop();
path.pop();
}
Ok(path)
}
}
}

View File

@ -55,14 +55,15 @@ pub use crate::actions::{
AccountAction::{self, *},
Action::{self, *},
ComposeAction::{self, *},
FlagAction,
ListingAction::{self, *},
MailingListAction::{self, *},
TabAction::{self, *},
TagAction::{self, *},
TagAction,
ViewAction::{self, *},
};
/// Helper macro to convert an array of tokens into a TokenStream
/// Helper macro to convert an array of tokens into a `TokenStream`
macro_rules! to_stream {
($token: expr) => {
TokenStream {
@ -142,6 +143,11 @@ impl TokenStream {
tokens.append(&mut m);
}
}
AlternativeStrings(v) => {
for t in v.iter() {
sugg.insert(format!("{}{}", if s.is_empty() { " " } else { "" }, t));
}
}
Seq(_s) => {}
RestOfStringValue => {
sugg.insert(String::new());
@ -195,6 +201,20 @@ impl TokenStream {
*s = "";
}
}
AlternativeStrings(v) => {
for lit in v.iter() {
if lit.starts_with(*s) && lit.len() != s.len() {
sugg.insert(lit[s.len()..].to_string());
tokens.push((s, *t.inner()));
return tokens;
} else if s.starts_with(lit) {
tokens.push((&s[..lit.len()], *t.inner()));
*s = &s[lit.len()..];
} else {
return vec![];
}
}
}
Seq(_s) => {
return vec![];
}
@ -250,6 +270,7 @@ pub enum Token {
Literal(&'static str),
Filepath,
Alternatives(&'static [TokenStream]),
AlternativeStrings(&'static [&'static str]),
Seq(&'static [TokenAdicity]),
AccountName,
MailboxPath,
@ -339,6 +360,11 @@ define_commands!([
tokens: &[One(Literal("search")), One(RestOfStringValue)],
parser: parser::search
},
{ tags: ["clear-selection"],
desc: "clear-selection",
tokens: &[One(Literal("clear-selection"))],
parser: parser::select
},
{ tags: ["select"],
desc: "select <TERM>, selects envelopes matching with given term",
tokens: &[One(Literal("select")), One(RestOfStringValue)],
@ -482,6 +508,18 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
tokens: &[One(Literal("manage-mailboxes"))],
parser: parser::manage_mailboxes
},
{ tags: ["man"],
desc: "read documentation",
tokens: {
#[cfg(feature = "cli-docs")]
{
&[One(Literal("man")), One(AlternativeStrings(crate::manpages::POSSIBLE_VALUES))]
}
#[cfg(not(feature = "cli-docs"))]
{ &[] }
},
parser: parser::view_manpage
},
{ tags: ["manage-jobs"],
desc: "view and manage jobs",
tokens: &[One(Literal("manage-jobs"))],
@ -502,7 +540,7 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
/// Get command suggestions for input
pub fn command_completion_suggestions(input: &str) -> Vec<String> {
use crate::melib::ShellExpandTrait;
let mut sugg = Default::default();
let mut sugg: HashSet<String> = Default::default();
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
let _m = tokens.matches(&mut &(*input), &mut sugg);
if _m.is_empty() {
@ -527,17 +565,15 @@ mod tests {
let mut input = "sort".to_string();
macro_rules! match_input {
($input:expr) => {{
let mut sugg = Default::default();
let mut vec = vec![];
let mut sugg: HashSet<String> = Default::default();
//print!("{}", $input);
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
let m = tokens.matches(&mut $input.as_str(), &mut sugg);
if !m.is_empty() {
vec.push(tokens);
//print!("{:?} ", desc);
//println!(" result = {:#?}\n\n", m);
}
// //println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
let _ = tokens.matches(&mut $input.as_str(), &mut sugg);
// if !m.is_empty() {
// //print!("{:?} ", desc);
// //println!(" result = {:#?}\n\n", m);
// }
}
//println!("suggestions = {:#?}", sugg);
sugg.into_iter()
@ -586,7 +622,7 @@ mod tests {
match io::stdin().read_line(&mut input) {
Ok(_n) => {
println!("Input is {:?}", input.as_str().trim());
let mut sugg = Default::default();
let mut sugg: HashSet<String> = Default::default();
let mut vec = vec![];
//print!("{}", input);
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
@ -652,9 +688,15 @@ mod tests {
assert_eq!(
parse_command(b"set foo").unwrap_err().to_string(),
BadValue {
inner: "Bad argument for `set`. Accepted arguments are [seen, unseen, plain, \
threaded, compact, conversations]."
.into(),
inner: "foo".into(),
suggestions: Some(&[
"seen",
"unseen",
"plain",
"threaded",
"compact",
"conversations"
])
}
.to_string(),
);

View File

@ -23,10 +23,16 @@
use std::path::PathBuf;
use melib::{email::mailto::Mailto, SortField, SortOrder};
use melib::{email::mailto::Mailto, Flag, SortField, SortOrder};
use crate::components::{Component, ComponentId};
#[derive(Debug)]
pub enum FlagAction {
Set(Flag),
Unset(Flag),
}
#[derive(Debug)]
pub enum TagAction {
Add(String),
@ -52,6 +58,8 @@ pub enum ListingAction {
Delete,
OpenInNewTab,
Tag(TagAction),
Flag(FlagAction),
ClearSelection,
ToggleThreadSnooze,
}
@ -62,6 +70,8 @@ pub enum TabAction {
New(Option<Box<dyn Component>>),
ManageMailboxes,
ManageJobs,
#[cfg(feature = "cli-docs")]
Man(crate::manpages::ManPages),
}
#[derive(Debug)]
@ -136,10 +146,10 @@ impl Action {
pub fn needs_confirmation(&self) -> bool {
matches!(
self,
Action::Listing(ListingAction::Delete)
| Action::MailingListAction(_)
| Action::Mailbox(_, _)
| Action::Quit
Self::Listing(ListingAction::Delete)
| Self::MailingListAction(_)
| Self::Mailbox(_, _)
| Self::Quit
)
}
}

View File

@ -29,6 +29,7 @@ pub enum CommandError {
},
BadValue {
inner: Cow<'static, str>,
suggestions: Option<&'static [&'static str]>,
},
WrongNumberOfArguments {
too_many: bool,
@ -63,7 +64,25 @@ impl std::fmt::Display for CommandError {
Self::Parsing { inner, kind: _ } => {
write!(fmt, "Could not parse command: {}", inner)
}
Self::BadValue { inner } => {
Self::BadValue {
inner,
suggestions: Some(suggs),
} => {
write!(fmt, "Bad value/argument: {}. Possible values are: ", inner)?;
let len = suggs.len();
for (i, val) in suggs.iter().enumerate() {
if i == len.saturating_sub(1) {
write!(fmt, "{}", val)?;
} else {
write!(fmt, "{}, ", val)?;
}
}
write!(fmt, "")
}
Self::BadValue {
inner,
suggestions: None,
} => {
write!(fmt, "Bad value/argument: {}", inner)
}
Self::WrongNumberOfArguments {
@ -121,3 +140,28 @@ impl std::fmt::Display for CommandError {
}
impl std::error::Error for CommandError {}
#[cfg(test)]
mod tests {
use super::CommandError;
#[test]
fn test_command_error_display() {
assert_eq!(
&CommandError::BadValue {
inner: "foo".into(),
suggestions: Some(&[
"seen",
"unseen",
"plain",
"threaded",
"compact",
"conversations"
])
}
.to_string(),
"Bad value/argument: foo. Possible values are: seen, unseen, plain, threaded, \
compact, conversations"
);
}
}

View File

@ -24,22 +24,37 @@
use super::*;
use crate::command::{argcheck::*, error::*};
const FLAG_SUGGESTIONS: &[&str] = &[
"passed",
"replied",
"seen or read",
"junk or trash or trashed",
"draft",
"flagged",
];
macro_rules! command_err {
(nom $b:expr, $input: expr, $msg:literal) => {{
(nom $b:expr, $input: expr, $msg:expr, $suggs:expr) => {{
let evaluated: IResult<&'_ [u8], _> = { $b };
match evaluated {
Err(_) => {
let err = CommandError::BadValue { inner: $msg.into() };
let err = CommandError::BadValue {
inner: $msg.into(),
suggestions: $suggs,
};
return Ok(($input, Err(err)));
}
Ok(v) => v,
}
}};
($b:expr, $input: expr, $msg:literal) => {{
($b:expr, $input: expr, $msg:expr, $suggs:expr) => {{
let evaluated = { $b };
match evaluated {
Err(_) => {
let err = CommandError::BadValue { inner: $msg.into() };
let err = CommandError::BadValue {
inner: $msg.into(),
suggestions: $suggs,
};
return Ok(($input, Err(err)));
}
Ok(v) => v,
@ -101,6 +116,7 @@ pub fn listing_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandErro
open_in_new_tab,
export_mbox,
_tag,
flag,
))(input)
}
@ -123,7 +139,7 @@ pub fn view(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
}
pub fn new_tab(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
alt((manage_mailboxes, manage_jobs, compose_action))(input)
alt((manage_mailboxes, manage_jobs, compose_action, view_manpage))(input)
}
pub fn parse_command(input: &[u8]) -> Result<Action, CommandError> {
@ -154,6 +170,125 @@ pub fn parse_command(input: &[u8]) -> Result<Action, CommandError> {
.and_then(|(_, v)| v)
}
/// Set/unset a flag.
///
/// # Example
///
/// ```
/// # use meli::{melib::Flag, command::{Action,ListingAction, FlagAction, parser}};
///
/// let (rest, parsed) = parser::flag(b"flag set junk").unwrap();
/// assert_eq!(rest, b"");
/// assert!(
/// matches!(
/// parsed,
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Set(
/// Flag::TRASHED
/// ))))
/// ),
/// "{:?}",
/// parsed
/// );
///
/// let (rest, parsed) = parser::flag(b"flag unset junk").unwrap();
/// assert_eq!(rest, b"");
/// assert!(
/// matches!(
/// parsed,
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Unset(
/// Flag::TRASHED
/// ))))
/// ),
/// "{:?}",
/// parsed
/// );
///
/// let (rest, parsed) = parser::flag(b"flag set draft").unwrap();
/// assert_eq!(rest, b"");
/// assert!(
/// matches!(
/// parsed,
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Set(
/// Flag::DRAFT
/// ))))
/// ),
/// "{:?}",
/// parsed
/// );
///
/// let (rest, parsed) = parser::flag(b"flag set xunk").unwrap();
/// assert_eq!(rest, b"");
/// assert_eq!(
/// &parsed.unwrap_err().to_string(),
/// "Bad value/argument: xunk is not a valid flag name. Possible values are: passed, replied, \
/// seen or read, junk or trash or trashed, draft, flagged"
/// );
/// ```
pub fn flag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
use melib::Flag;
fn parse_flag(s: &str) -> Option<Flag> {
match s {
o if o.eq_ignore_ascii_case("passed") => Some(Flag::PASSED),
o if o.eq_ignore_ascii_case("replied") => Some(Flag::REPLIED),
o if o.eq_ignore_ascii_case("seen") => Some(Flag::SEEN),
o if o.eq_ignore_ascii_case("read") => Some(Flag::SEEN),
o if o.eq_ignore_ascii_case("junk") => Some(Flag::TRASHED),
o if o.eq_ignore_ascii_case("trash") => Some(Flag::TRASHED),
o if o.eq_ignore_ascii_case("trashed") => Some(Flag::TRASHED),
o if o.eq_ignore_ascii_case("draft") => Some(Flag::DRAFT),
o if o.eq_ignore_ascii_case("flagged") => Some(Flag::FLAGGED),
_ => None,
}
}
preceded(
tag("flag"),
alt((
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg: 2, flag};
let (input, _) = tag("set")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, flag) = quoted_argument(input.trim())?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
let Some(flag) = parse_flag(flag) else {
return Ok((
b"",
Err(CommandError::BadValue {
inner: format!("{flag} is not a valid flag name").into(),
suggestions: Some(FLAG_SUGGESTIONS),
}),
));
};
Ok((input, Ok(Listing(Flag(FlagAction::Set(flag))))))
},
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg: 2, flag};
let (input, _) = tag("unset")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, flag) = quoted_argument(input.trim())?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
let Some(flag) = parse_flag(flag) else {
return Ok((
b"",
Err(CommandError::BadValue {
inner: format!("{flag} is not a valid flag name").into(),
suggestions: Some(FLAG_SUGGESTIONS),
}),
));
};
Ok((input, Ok(Listing(Flag(FlagAction::Unset(flag))))))
},
)),
)(input.trim())
}
pub fn set(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, set};
@ -173,9 +308,13 @@ pub fn set(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, ret) = command_err!(nom
alt((map(tag("seen"), |_| Listing(SetSeen)), map(tag("unseen"), |_| Listing(SetUnseen))))(input),
alt((
map(tag("seen"), |_| Listing(SetSeen)),
map(tag("unseen"), |_| Listing(SetUnseen)
)))(input),
input,
"Bad argument for `set`. Accepted arguments are [seen, unseen, plain, threaded, compact, conversations].");
String::from_utf8_lossy(input.trim()).to_string(),
Some(&["seen", "unseen", "plain", "threaded", "compact", "conversations"]));
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(ret)))
@ -280,7 +419,8 @@ pub fn goto(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let (input, nth) = command_err!(nom
usize_c(input),
input,
"Argument must be an integer.");
"Argument must be an integer.",
None);
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Action::ViewMailbox(nth))))
@ -336,6 +476,26 @@ pub fn search(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
Ok((input, Ok(Listing(Search(String::from(string))))))
}
pub fn select(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
#[inline]
fn clear_selection(input: &[u8]) -> Option<IResult<&[u8], Result<Action, CommandError>>> {
if !input.trim().starts_with(b"clear-selection") {
return None;
}
#[inline]
fn inner(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:0, max_arg: 0, clear_selection};
let (input, _) = tag("clear-selection")(input.ltrim())?;
arg_chk!(start check, input);
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Listing(ListingAction::ClearSelection))))
}
Some(inner(input))
}
if let Some(retval) = clear_selection(input) {
return retval;
}
let mut check = arg_init! { min_arg:1, max_arg: {u8::MAX}, select};
let (input, _) = tag("select")(input.trim())?;
arg_chk!(start check, input);
@ -433,7 +593,8 @@ pub fn mailto(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let (input, val) = command_err!(
parser(val.as_bytes()),
val.as_bytes(),
"Could not parse mailto value. If the value is valid, please report this bug."
"Could not parse mailto value. If the value is valid, please report this bug.",
None
);
Ok((input, Ok(Compose(Mailto(val)))))
}
@ -708,6 +869,19 @@ pub fn add_addresses_to_contacts(input: &[u8]) -> IResult<&[u8], Result<Action,
let (input, _) = eof(input)?;
Ok((input, Ok(View(AddAddressesToContacts))))
}
/// Set/unset a tag.
///
/// # Example
///
/// ```
/// # use meli::command::{Action,ListingAction, TagAction, parser::_tag};
///
/// let (rest, parsed) = _tag(b"tag add newsletters").unwrap();
/// println!("parsed is {:?}", parsed);
/// assert_eq!(rest, b"");
/// assert!(matches!(parsed, Ok(Action::Listing(ListingAction::Tag(TagAction::Add(ref tagname)))) if tagname == "newsletters"), "{:?}", parsed);
/// ```
pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
preceded(
tag("tag"),
@ -721,7 +895,7 @@ pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
let (input, tag) = quoted_argument(input.trim())?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Listing(Tag(Add(tag.to_string()))))))
Ok((input, Ok(Listing(Tag(TagAction::Add(tag.to_string()))))))
},
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg: 2, tag};
@ -732,11 +906,12 @@ pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
let (input, tag) = quoted_argument(input.trim())?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Listing(Tag(Remove(tag.to_string()))))))
Ok((input, Ok(Listing(Tag(TagAction::Remove(tag.to_string()))))))
},
)),
)(input.trim())
}
pub fn print_account_setting(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg: 2, print};
let (input, _) = tag("print")(input.trim())?;
@ -796,7 +971,8 @@ pub fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
return Ok((
input,
Err(CommandError::BadValue {
inner: "Valid toggle values are thread_snooze, mouse, sign, encrypt.".into(),
inner: String::from_utf8_lossy(input).to_string().into(),
suggestions: Some(&["thread_snooze", "mouse", "sign", "encrypt"]),
}),
));
}
@ -823,6 +999,39 @@ pub fn manage_jobs(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>>
let (input, _) = eof(input)?;
Ok((input, Ok(Tab(ManageJobs))))
}
#[cfg(feature = "cli-docs")]
pub fn view_manpage(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, view_manpage };
let (input, _) = tag("man")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, manpage) = map_res(not_line_ending, std::str::from_utf8)(input.trim())?;
let (input, _) = eof(input)?;
arg_chk!(finish check, input);
match crate::manpages::parse_manpage(manpage) {
Ok(m) => Ok((input, Ok(Tab(Man(m))))),
Err(err) => Ok((
input,
Err(CommandError::BadValue {
inner: err.to_string().into(),
suggestions: Some(crate::manpages::POSSIBLE_VALUES),
}),
)),
}
}
#[cfg(not(feature = "cli-docs"))]
pub fn view_manpage(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
Ok((
input,
Err(CommandError::Other {
inner: "this meli binary has not been compiled with the cli-docs feature".into(),
}),
))
}
pub fn quit(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:0, max_arg: 0, quit};
let (input, _) = tag("quit")(input.trim())?;

View File

@ -29,9 +29,14 @@ use std::{
collections::HashSet,
io::Read,
process::{Command, Stdio},
sync::Arc,
};
use melib::{backends::TagHash, search::Query, SortField, SortOrder, StderrLogger};
use melib::{
backends::{MailboxHash, TagHash},
search::Query,
ShellExpandTrait, SortField, SortOrder, StderrLogger,
};
use crate::{
conf::deserializers::non_empty_opt_string,
@ -62,7 +67,7 @@ use std::{
use indexmap::IndexMap;
use melib::{
conf::{AccountSettings, MailboxConf, ToggleFlag},
conf::{AccountSettings, ActionFlag, MailboxConf, ToggleFlag},
error::*,
};
use pager::PagerSettings;
@ -161,11 +166,17 @@ use crate::conf::deserializers::extra_settings;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct FileAccount {
pub root_mailbox: String,
/// The mailbox that is the default to open / view for this account. Must be
/// a valid mailbox path.
///
/// If not specified, the default is [`Self::root_mailbox`].
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
pub default_mailbox: Option<String>,
pub format: String,
pub identity: String,
#[serde(default)]
pub extra_identities: Vec<String>,
#[serde(default = "none")]
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default = "false_val")]
@ -180,14 +191,18 @@ pub struct FileAccount {
pub order: (SortField, SortOrder),
#[serde(default = "false_val")]
pub manual_refresh: bool,
#[serde(default = "none")]
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
pub refresh_command: Option<String>,
#[serde(flatten)]
pub conf_override: MailUIConf,
#[serde(flatten)]
#[serde(deserialize_with = "extra_settings")]
pub extra: IndexMap<String, String>, /* use custom deserializer to convert any given value
* (eg bool, number, etc) to string */
#[serde(
deserialize_with = "extra_settings",
skip_serializing_if = "IndexMap::is_empty"
)]
/// Use custom deserializer to convert any given value (eg `bool`, number,
/// etc) to `String`.
pub extra: IndexMap<String, String>,
}
impl FileAccount {
@ -195,10 +210,6 @@ impl FileAccount {
&self.mailboxes
}
pub fn mailbox(&self) -> &str {
&self.root_mailbox
}
pub fn search_backend(&self) -> &SearchBackend {
&self.search_backend
}
@ -230,6 +241,8 @@ pub struct FileSettings {
#[derive(Clone, Debug, Default, Serialize)]
pub struct AccountConf {
pub account: AccountSettings,
pub default_mailbox: Option<MailboxHash>,
pub sent_mailbox: Option<MailboxHash>,
pub conf: FileAccount,
pub conf_override: MailUIConf,
pub mailbox_confs: IndexMap<String, FileMailboxConf>,
@ -279,8 +292,10 @@ impl From<FileAccount> for AccountConf {
};
let mailbox_confs = x.mailboxes.clone();
AccountConf {
Self {
account: acc,
default_mailbox: None,
sent_mailbox: None,
conf_override: x.conf_override.clone(),
conf: x,
mailbox_confs,
@ -289,24 +304,23 @@ impl From<FileAccount> for AccountConf {
}
pub fn get_config_file() -> Result<PathBuf> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").map_err(|err| {
Error::new(format!(
"Could not detect XDG directories for user: {}",
err
))
.set_source(Some(std::sync::Arc::new(Box::new(err))))
})?;
match env::var("MELI_CONFIG") {
Ok(path) => Ok(PathBuf::from(path)),
Err(_) => Ok(xdg_dirs
.place_config_file("config.toml")
.chain_err_summary(|| {
format!(
"Cannot create configuration directory in {}",
xdg_dirs.get_config_home().display()
)
})?),
if let Ok(path) = env::var("MELI_CONFIG") {
return Ok(PathBuf::from(path).expand());
}
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").map_err(|err| {
Error::new("Could not detect XDG directories for user")
.set_source(Some(std::sync::Arc::new(Box::new(err))))
.set_kind(ErrorKind::NotSupported)
})?;
xdg_dirs
.place_config_file("config.toml")
.chain_err_summary(|| {
format!(
"Cannot create configuration directory in {}",
xdg_dirs.get_config_home().display()
)
})
.chain_err_kind(ErrorKind::OSError)
}
pub fn get_included_configs(conf_path: PathBuf) -> Result<Vec<PathBuf>> {
@ -322,7 +336,7 @@ changequote(`"', `"')dnl
let mut contents = String::new();
while let Some((parent, p)) = stack.pop() {
if !p.exists() || p.is_dir() {
return Err(format!(
return Err(Error::new(format!(
"Path {}{included}{in_parent} {msg}.",
p.display(),
included = if parent.is_some() {
@ -340,8 +354,8 @@ changequote(`"', `"')dnl
} else {
"is a directory, not a text file"
}
)
.into());
))
.set_kind(ErrorKind::Configuration));
}
contents.clear();
let mut file = std::fs::File::open(&p)?;
@ -354,11 +368,18 @@ changequote(`"', `"')dnl
.spawn()
{
Ok(handle) => handle,
Err(error) => match error.kind() {
Err(err) => match err.kind() {
io::ErrorKind::NotFound => {
return Err("`m4` executable not found. Please install.".into())
return Err(Error::new(
"`m4` executable not found in PATH. Please provide an m4 binary.",
)
.set_kind(ErrorKind::Platform))
}
_ => {
return Err(Error::new("Could not process configuration with `m4`")
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::OSError))
}
_ => return Err(error.into()),
},
};
@ -393,7 +414,7 @@ define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl
file.read_to_string(&mut contents)?;
let mut handle = Command::new("m4")
.current_dir(conf_path.parent().unwrap_or(Path::new("/")))
.current_dir(conf_path.parent().unwrap_or_else(|| Path::new("/")))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@ -407,12 +428,13 @@ define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl
}
impl FileSettings {
pub fn new() -> Result<FileSettings> {
pub fn new() -> Result<Self> {
let config_path = get_config_file()?;
if !config_path.exists() {
let path_string = config_path.display().to_string();
if path_string.is_empty() {
return Err(Error::new("No configuration found."));
return Err(Error::new("Given configuration path is empty.")
.set_kind(ErrorKind::Configuration));
}
#[cfg(not(test))]
let ask = Ask {
@ -424,33 +446,34 @@ impl FileSettings {
#[cfg(not(test))]
if ask.run() {
create_config_file(&config_path)?;
return Err(Error::new(
"Edit the sample configuration and relaunch meli.",
));
return Err(
Error::new("Edit the sample configuration and relaunch meli.")
.set_kind(ErrorKind::Configuration),
);
}
#[cfg(test)]
return Ok(FileSettings::default());
return Ok(Self::default());
#[cfg(not(test))]
return Err(Error::new("No configuration file found."));
return Err(
Error::new("No configuration file found.").set_kind(ErrorKind::Configuration)
);
}
FileSettings::validate(config_path, true, false)
Self::validate(config_path, true, false)
}
pub fn validate(path: PathBuf, interactive: bool, clear_extras: bool) -> Result<Self> {
let s = pp::pp(&path)?;
let map: toml::map::Map<String, toml::value::Value> =
toml::from_str(&s).map_err(|err| {
Error::new(format!(
"{}:\nConfig file is invalid TOML: {}",
path.display(),
err
))
})?;
/*
* Check that a global composing option is set and return a user-friendly
* error message because the default serde one is confusing.
*/
let map: toml::value::Table = toml::from_str(&s).map_err(|err| {
Error::new(format!(
"{}: Config file is invalid TOML; {}",
path.display(),
err
))
})?;
// Check that a global composing option is set and return a user-friendly
// error message because the default serde one is confusing.
if !map.contains_key("composing") {
let err_msg = r#"You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows:
@ -469,25 +492,24 @@ This is required so that you don't accidentally start meli and find out later th
};
if ask.run() {
let mut file = OpenOptions::new().append(true).open(&path)?;
file.write_all("[composing]\nsend_mail = 'false'\n".as_bytes())
file.write_all(b"[composing]\nsend_mail = 'false'\n")
.map_err(|err| {
Error::new(format!("Could not append to {}: {}", path.display(), err))
})?;
return FileSettings::validate(path, interactive, clear_extras);
return Self::validate(path, interactive, clear_extras);
}
}
return Err(Error::new(format!(
"{}\n\nEdit the {} and relaunch meli.",
if interactive { "" } else { err_msg },
path.display()
)));
}
let mut s: FileSettings = toml::from_str(&s).map_err(|err| {
Error::new(format!(
"{}:\nConfig file contains errors: {}",
path.display(),
err
))
.set_kind(ErrorKind::Configuration));
}
let mut s: Self = toml::from_str(&s).map_err(|err| {
Error::new(format!("{}: Config file contains errors", path.display()))
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::Configuration)
})?;
let backends = melib::backends::Backends::new();
let Themes {
@ -513,10 +535,11 @@ This is required so that you don't accidentally start meli and find out later th
}
}
match s.terminal.theme.as_str() {
"dark" | "light" => {}
themes::DARK | themes::LIGHT => {}
t if s.terminal.themes.other_themes.contains_key(t) => {}
t => {
return Err(Error::new(format!("Theme `{}` was not found.", t)));
return Err(Error::new(format!("Theme `{}` was not found.", t))
.set_kind(ErrorKind::Configuration));
}
}
@ -534,6 +557,7 @@ This is required so that you don't accidentally start meli and find out later th
mailboxes,
extra,
manual_refresh,
default_mailbox: _,
refresh_command: _,
search_backend: _,
conf_override: _,
@ -557,12 +581,14 @@ This is required so that you don't accidentally start meli and find out later th
.collect(),
extra: extra.into_iter().collect(),
};
s.validate_config()?;
backends.validate_config(&lowercase_format, &mut s)?;
if !s.extra.is_empty() {
return Err(Error::new(format!(
"Unrecognised configuration values: {:?}",
s.extra
)));
))
.set_kind(ErrorKind::Configuration));
}
if clear_extras {
acc.extra.clear();
@ -586,17 +612,17 @@ pub struct Settings {
pub terminal: TerminalSettings,
pub log: LogSettings,
#[serde(skip)]
_logger: StderrLogger,
pub _logger: StderrLogger,
}
impl Settings {
pub fn new() -> Result<Settings> {
pub fn new() -> Result<Self> {
let fs = FileSettings::new()?;
let mut s: IndexMap<String, AccountConf> = IndexMap::new();
for (id, x) in fs.accounts {
let mut ac = AccountConf::from(x);
ac.account.name = id.clone();
ac.account.name.clone_from(&id);
s.insert(id, ac);
}
@ -607,7 +633,7 @@ impl Settings {
_logger.change_log_dest(log_path.into());
}
Ok(Settings {
Ok(Self {
accounts: s,
pager: fs.pager,
listing: fs.listing,
@ -622,7 +648,7 @@ impl Settings {
})
}
pub fn without_accounts() -> Result<Settings> {
pub fn without_accounts() -> Result<Self> {
let fs = FileSettings::new()?;
let mut _logger = StderrLogger::new(fs.log.maximum_level);
@ -630,7 +656,7 @@ impl Settings {
_logger.change_log_dest(log_path.into());
}
Ok(Settings {
Ok(Self {
accounts: IndexMap::new(),
pager: fs.pager,
listing: fs.listing,
@ -680,16 +706,29 @@ mod default_vals {
None
}
pub(in crate::conf) fn internal_value_false<T: std::convert::From<super::ToggleFlag>>() -> T {
super::ToggleFlag::InternalVal(false).into()
pub(in crate::conf) fn internal_value_false<T: std::convert::From<melib::conf::ToggleFlag>>(
) -> T {
melib::conf::ToggleFlag::InternalVal(false).into()
}
pub(in crate::conf) fn internal_value_true<T: std::convert::From<super::ToggleFlag>>() -> T {
super::ToggleFlag::InternalVal(true).into()
pub(in crate::conf) fn internal_value_true<T: std::convert::From<melib::conf::ToggleFlag>>() -> T
{
melib::conf::ToggleFlag::InternalVal(true).into()
}
pub(in crate::conf) fn ask<T: std::convert::From<super::ToggleFlag>>() -> T {
super::ToggleFlag::Ask.into()
pub(in crate::conf) fn action_internal_value_false<T: std::convert::From<melib::ActionFlag>>(
) -> T {
melib::conf::ActionFlag::InternalVal(false).into()
}
//pub(in crate::conf) fn action_internal_value_true<
// T: std::convert::From<melib::conf::ActionFlag>,
//>() -> T {
// melib::conf::ActionFlag::InternalVal(true).into()
//}
pub(in crate::conf) fn ask<T: std::convert::From<melib::conf::ActionFlag>>() -> T {
melib::conf::ActionFlag::Ask.into()
}
}
@ -763,10 +802,10 @@ impl<'de> Deserialize<'de> for IndexStyle {
{
let s = <String>::deserialize(deserializer)?;
match s.as_str() {
"Plain" | "plain" => Ok(IndexStyle::Plain),
"Threaded" | "threaded" => Ok(IndexStyle::Threaded),
"Compact" | "compact" => Ok(IndexStyle::Compact),
"Conversations" | "conversations" => Ok(IndexStyle::Conversations),
"Plain" | "plain" => Ok(Self::Plain),
"Threaded" | "threaded" => Ok(Self::Threaded),
"Compact" | "compact" => Ok(Self::Compact),
"Conversations" | "conversations" => Ok(Self::Conversations),
_ => Err(de::Error::custom("invalid `index_style` value")),
}
}
@ -778,10 +817,10 @@ impl Serialize for IndexStyle {
S: Serializer,
{
match self {
IndexStyle::Plain => serializer.serialize_str("plain"),
IndexStyle::Threaded => serializer.serialize_str("threaded"),
IndexStyle::Compact => serializer.serialize_str("compact"),
IndexStyle::Conversations => serializer.serialize_str("conversations"),
Self::Plain => serializer.serialize_str("plain"),
Self::Threaded => serializer.serialize_str("threaded"),
Self::Compact => serializer.serialize_str("compact"),
Self::Conversations => serializer.serialize_str("conversations"),
}
}
}
@ -807,15 +846,15 @@ impl<'de> Deserialize<'de> for SearchBackend {
if sqlite3.eq_ignore_ascii_case("sqlite3")
|| sqlite3.eq_ignore_ascii_case("sqlite") =>
{
Ok(SearchBackend::Sqlite3)
Ok(Self::Sqlite3)
}
none if none.eq_ignore_ascii_case("none")
|| none.eq_ignore_ascii_case("nothing")
|| none.is_empty() =>
{
Ok(SearchBackend::None)
Ok(Self::None)
}
auto if auto.eq_ignore_ascii_case("auto") => Ok(SearchBackend::Auto),
auto if auto.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
_ => Err(de::Error::custom("invalid `search_backend` value")),
}
}
@ -828,9 +867,9 @@ impl Serialize for SearchBackend {
{
match self {
#[cfg(feature = "sqlite3")]
SearchBackend::Sqlite3 => serializer.serialize_str("sqlite3"),
SearchBackend::None => serializer.serialize_str("none"),
SearchBackend::Auto => serializer.serialize_str("auto"),
Self::Sqlite3 => serializer.serialize_str("sqlite3"),
Self::None => serializer.serialize_str("none"),
Self::Auto => serializer.serialize_str("auto"),
}
}
}
@ -1042,6 +1081,7 @@ mod dotaddressable {
impl DotAddressable for melib::LogLevel {}
impl DotAddressable for PathBuf {}
impl DotAddressable for ToggleFlag {}
impl DotAddressable for ActionFlag {}
impl DotAddressable for SearchBackend {}
impl DotAddressable for melib::SpecialUsageMailbox {}
impl<T: DotAddressable> DotAddressable for Option<T> {}
@ -1263,7 +1303,7 @@ mod dotaddressable {
#[test]
fn test_config_parse() {
use std::{fmt::Write, fs, io::prelude::*, path::PathBuf};
use std::{fmt::Write, fs, path::PathBuf};
struct ConfigFile {
path: PathBuf,
@ -1320,11 +1360,8 @@ send_mail = 'false'
impl ConfigFile {
fn new(content: &str) -> std::result::Result<Self, std::io::Error> {
let mut f = fs::File::open("/dev/urandom")?;
let mut buf = [0u8; 16];
f.read_exact(&mut buf)?;
let mut filename = String::with_capacity(2 * 16);
for byte in &buf {
for byte in melib::utils::random::random_u64().to_be_bytes() {
write!(&mut filename, "{:02X}", byte).unwrap();
}
let mut path = std::env::temp_dir();
@ -1334,7 +1371,7 @@ send_mail = 'false'
.append(true)
.open(&path)?;
file.write_all(content.as_bytes())?;
Ok(ConfigFile { path, file })
Ok(Self { path, file })
}
}
@ -1352,7 +1389,7 @@ send_mail = 'false'
));
new_file
.file
.write_all("[composing]\nsend_mail = 'false'\n".as_bytes())
.write_all(b"[composing]\nsend_mail = 'false'\n")
.unwrap();
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
assert_eq!(

View File

@ -22,7 +22,8 @@
//! Configuration for composing email.
use std::collections::HashMap;
use melib::{email::HeaderName, ToggleFlag};
use melib::{conf::ActionFlag, email::HeaderName};
use serde::{de, Deserialize, Deserializer};
use super::{
default_vals::{ask, false_val, none, true_val},
@ -62,12 +63,12 @@ pub struct ComposingSettings {
/// Default: empty
#[serde(default, alias = "default-header-values")]
pub default_header_values: HashMap<HeaderName, String>,
/// Wrap header preample when editing a draft in an editor. This allows you
/// Wrap header preamble when editing a draft in an editor. This allows you
/// to write non-plain text email without the preamble creating syntax
/// errors. They are stripped when you return from the editor. The
/// values should be a two element array of strings, a prefix and suffix.
/// Default: None
#[serde(default, alias = "wrap-header-preample")]
#[serde(default, alias = "wrap-header-preamble")]
pub wrap_header_preamble: Option<(String, String)>,
/// Store sent mail after successful submission. This setting is meant to be
/// disabled for non-standard behaviour in gmail, which auto-saves sent
@ -91,7 +92,7 @@ pub struct ComposingSettings {
/// Forward emails as attachment? (Alternative is inline)
/// Default: ask
#[serde(default = "ask", alias = "forward-as-attachment")]
pub forward_as_attachment: ToggleFlag,
pub forward_as_attachment: ActionFlag,
/// Alternative lists of reply prefixes (etc. ["Re:", "RE:", ...]) to strip
/// Default: `["Re:", "RE:", "Fwd:", "Fw:", "回复:", "回覆:", "SV:", "Sv:",
/// "VS:", "Antw:", "Doorst:", "VS:", "VL:", "REF:", "TR:", "TR:", "AW:",
@ -114,7 +115,7 @@ pub struct ComposingSettings {
impl Default for ComposingSettings {
fn default() -> Self {
ComposingSettings {
Self {
send_mail: SendMail::ShellCommand("false".into()),
editor_command: None,
embedded_pty: false,
@ -125,7 +126,7 @@ impl Default for ComposingSettings {
wrap_header_preamble: None,
attribution_format_string: None,
attribution_use_posix_locale: true,
forward_as_attachment: ToggleFlag::Ask,
forward_as_attachment: ActionFlag::Ask,
reply_prefix_list_to_strip: None,
reply_prefix: res(),
custom_compose_hooks: vec![],
@ -176,7 +177,7 @@ pub mod strings {
named_unit_variant!(server_submission);
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum SendMail {
#[cfg(feature = "smtp")]
@ -202,3 +203,103 @@ impl From<ComposeHook> for crate::mail::hooks::Hook {
Self::new_shell_command(c.name.into(), c.command)
}
}
const SENDMAIL_ERR_HELP: &str = r#"Invalid `send_mail` value.
Here are some valid examples:
Use server submission in protocols that support it (JMAP, NNTP)
===============================================================
send_mail = "server_submission"
Using a shell script
====================
send_mail = "msmtp --read-recipients --read-envelope-from"
Direct SMTP connection
======================
send_mail = { hostname = "mail.example.com", port = 587, auth = { type = "auto", password = { type = "raw", value = "hunter2" } }, security = { type = "STARTTLS" } }
[composing.send_mail]
hostname = "mail.example.com"
port = 587
auth = { type = "auto", password = { type = "command_eval", value = "/path/to/password_script.sh" } }
security = { type = "TLS", danger_accept_invalid_certs = true } }
`send_mail` direct SMTP connection fields:
- hostname: text
- port: valid port number
- envelope_from: text (optional, default is empty),
- auth: ...
- security: ... (optional, default is "auto")
- extensions: ... (optional, default is PIPELINING, CHUNKING, PRDR, 8BITMIME, BINARYMIME, SMTPUTF8, AUTH and DSN_NOTIFY)
Possible values for `send_mail.auth`:
No authentication:
auth = { type = "none" }
Regular authentication:
Note: `require_auth` and `auth_type` are optional and can be skipped.
auth = { type = "auto", username = "...", password = "...", require_auth = true, auth_type = ... }
password can be:
password = { type = "raw", value = "..." }
password = { type = "command_eval", value = "/path/to/password_script.sh" }
XOAuth2 authentication:
Note: `require_auth` is optional and can be skipped.
auth = { type = "xoauth2", token_command = "...", require_auth = true }
Possible values for `send_mail.auth.auth_type` when `auth.type` is "auto":
auth_type = { plain = false, login = true }
Possible values for `send_mail.security`:
Note that in all cases field `danger_accept_invalid_certs` is optional and its default value is false.
security = "none"
security = { type = "auto", danger_accept_invalid_certs = false }
security = { type = "STARTTLS", danger_accept_invalid_certs = false }
security = { type = "TLS", danger_accept_invalid_certs = false }
Possible values for `send_mail.extensions` (All optional and have default values `true`:
pipelining
chunking
8bitmime
prdr
binarymime
smtputf8
auth
dsn_notify: Array of options e.g. ["FAILURE"]
"#;
impl<'de> Deserialize<'de> for SendMail {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum SendMailInner {
#[cfg(feature = "smtp")]
Smtp(melib::smtp::SmtpServerConf),
#[serde(with = "strings::server_submission")]
ServerSubmission,
ShellCommand(String),
}
match <SendMailInner>::deserialize(deserializer) {
#[cfg(feature = "smtp")]
Ok(SendMailInner::Smtp(v)) => Ok(Self::Smtp(v)),
Ok(SendMailInner::ServerSubmission) => Ok(Self::ServerSubmission),
Ok(SendMailInner::ShellCommand(v)) => Ok(Self::ShellCommand(v)),
Err(_err) => Err(de::Error::custom(SENDMAIL_ERR_HELP)),
}
}
}

View File

@ -19,7 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use melib::{search::Query, Error, Result};
use melib::{search::Query, Error, Result, ToggleFlag};
use super::{default_vals::*, DotAddressable, IndexStyle};
@ -130,6 +130,17 @@ pub struct ListingSettings {
#[serde(default)]
pub attachment_flag: Option<String>,
/// Flag to show if any thread entry contains your address as a receiver.
/// Useful to make mailing list threads that CC you stand out.
/// Default: "✸"
#[serde(default)]
pub highlight_self_flag: Option<String>,
/// Show `highlight_self_flag` or not.
/// Default: false
#[serde(default)]
pub highlight_self: ToggleFlag,
/// Should threads with different Subjects show a list of those
/// subjects on the entry title?
/// Default: "true"
@ -185,6 +196,8 @@ impl Default for ListingSettings {
thread_snoozed_flag: None,
selected_flag: None,
attachment_flag: None,
highlight_self_flag: None,
highlight_self: ToggleFlag::Unset,
thread_subject_pack: true,
threaded_repeat_identical_from_values: false,
relative_menu_indices: true,
@ -224,6 +237,8 @@ impl DotAddressable for ListingSettings {
"thread_snoozed_flag" => self.thread_snoozed_flag.lookup(field, tail),
"selected_flag" => self.selected_flag.lookup(field, tail),
"attachment_flag" => self.attachment_flag.lookup(field, tail),
"highlight_self_flag" => self.highlight_self_flag.lookup(field, tail),
"highlight_self" => self.highlight_self.lookup(field, tail),
"thread_subject_pack" => self.thread_subject_pack.lookup(field, tail),
"threaded_repeat_identical_from_values" => self
.threaded_repeat_identical_from_values

View File

@ -22,22 +22,22 @@
#![allow(clippy::derivable_impls)]
//! This module is automatically generated by config_macros.rs.
//! This module is automatically generated by `config_macros.rs`.
use super::*;
use melib::HeaderName;
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "sticky-headers" , alias = "headers-sticky" , alias = "headers_sticky")] # [serde (default)] pub sticky_headers : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > , # [doc = " Extra headers to display, if present, in the default header preamble."] # [doc = " Default: []"] # [serde (alias = "show-extra-headers")] # [serde (default)] pub show_extra_headers : Option < Vec < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , sticky_headers : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None , show_extra_headers : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "sticky-headers" , alias = "headers-sticky" , alias = "headers_sticky")] # [serde (default)] pub sticky_headers : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > , # [doc = " Extra headers to display, if present, in the default header preamble."] # [doc = " Default: []"] # [serde (alias = "show-extra-headers")] # [serde (default)] pub show_extra_headers : Option < Vec < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { Self { pager_context : None , pager_stop : None , sticky_headers : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None , show_extra_headers : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = " Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = " Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = " Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = " Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Should threads with different Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > , # [doc = " In threaded listing style, repeat identical From column values within a"] # [doc = " thread. Not repeating adds empty space in the From column which"] # [doc = " might result in less visual clutter."] # [doc = " Default: \"false\""] # [serde (default)] pub threaded_repeat_identical_from_values : Option < bool > , # [doc = " Show relative indices in menu mailboxes to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-menu-indices")] # [serde (default)] pub relative_menu_indices : Option < bool > , # [doc = " Show relative indices in listings to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-list-indices")] # [serde (default)] pub relative_list_indices : Option < bool > , # [doc = " Hide sidebar on launch. Default: \"false\""] # [serde (alias = "hide-sidebar-on-launch")] # [serde (default)] pub hide_sidebar_on_launch : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { ListingSettingsOverride { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , thread_subject_pack : None , threaded_repeat_identical_from_values : None , relative_menu_indices : None , relative_list_indices : None , hide_sidebar_on_launch : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = " Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = " Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = " Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = " Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Flag to show if any thread entry contains your address as a receiver."] # [doc = " Useful to make mailing list threads that CC you stand out."] # [doc = " Default: \"\""] # [serde (default)] pub highlight_self_flag : Option < Option < String > > , # [doc = " Show `highlight_self_flag` or not."] # [doc = " Default: false"] # [serde (default)] pub highlight_self : Option < ToggleFlag > , # [doc = " Should threads with different Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > , # [doc = " In threaded listing style, repeat identical From column values within a"] # [doc = " thread. Not repeating adds empty space in the From column which"] # [doc = " might result in less visual clutter."] # [doc = " Default: \"false\""] # [serde (default)] pub threaded_repeat_identical_from_values : Option < bool > , # [doc = " Show relative indices in menu mailboxes to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-menu-indices")] # [serde (default)] pub relative_menu_indices : Option < bool > , # [doc = " Show relative indices in listings to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-list-indices")] # [serde (default)] pub relative_list_indices : Option < bool > , # [doc = " Hide sidebar on launch. Default: \"false\""] # [serde (alias = "hide-sidebar-on-launch")] # [serde (default)] pub hide_sidebar_on_launch : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { Self { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , highlight_self_flag : None , highlight_self : None , thread_subject_pack : None , threaded_repeat_identical_from_values : None , relative_menu_indices : None , relative_list_indices : None , hide_sidebar_on_launch : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct NotificationsSettingsOverride { # [doc = " Enable notifications."] # [doc = " Default: True"] # [serde (default)] pub enable : Option < bool > , # [doc = " A command to pipe notifications through."] # [doc = " Default: None"] # [serde (default)] pub script : Option < Option < String > > , # [doc = " A command to pipe new mail notifications through (preferred over"] # [doc = " `script`). Default: None"] # [serde (default)] pub new_mail_script : Option < Option < String > > , # [doc = " A file location which has its size changed when new mail arrives (max"] # [doc = " 128 bytes). Can be used to trigger new mail notifications eg with"] # [doc = " `xbiff(1)`. Default: None"] # [serde (alias = "xbiff-file-path")] # [serde (default)] pub xbiff_file_path : Option < Option < String > > , # [serde (alias = "play-sound")] # [serde (default)] pub play_sound : Option < ToggleFlag > , # [serde (alias = "sound-file")] # [serde (default)] pub sound_file : Option < Option < String > > } impl Default for NotificationsSettingsOverride { fn default () -> Self { NotificationsSettingsOverride { enable : None , script : None , new_mail_script : None , xbiff_file_path : None , play_sound : None , sound_file : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct NotificationsSettingsOverride { # [doc = " Enable notifications."] # [doc = " Default: True"] # [serde (default)] pub enable : Option < bool > , # [doc = " A command to pipe notifications through."] # [doc = " Default: None"] # [serde (default)] pub script : Option < Option < String > > , # [doc = " A command to pipe new mail notifications through (preferred over"] # [doc = " `script`). Default: None"] # [serde (default)] pub new_mail_script : Option < Option < String > > , # [doc = " A file location which has its size changed when new mail arrives (max"] # [doc = " 128 bytes). Can be used to trigger new mail notifications eg with"] # [doc = " `xbiff(1)`. Default: None"] # [serde (alias = "xbiff-file-path")] # [serde (default)] pub xbiff_file_path : Option < Option < String > > , # [serde (alias = "play-sound")] # [serde (default)] pub play_sound : Option < ToggleFlag > , # [serde (alias = "sound-file")] # [serde (default)] pub sound_file : Option < Option < String > > } impl Default for NotificationsSettingsOverride { fn default () -> Self { Self { enable : None , script : None , new_mail_script : None , xbiff_file_path : None , play_sound : None , sound_file : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { ShortcutsOverride { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { Self { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embedded editor (for terminal interfaces) instead of forking and"] # [doc = " waiting."] # [serde (alias = "embed")] # [serde (default)] pub embedded_pty : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Set User-Agent"] # [doc = " Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < HeaderName , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embedded_pty : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embedded editor (for terminal interfaces) instead of forking and"] # [doc = " waiting."] # [serde (alias = "embed")] # [serde (default)] pub embedded_pty : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Set User-Agent"] # [doc = " Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < HeaderName , String > > , # [doc = " Wrap header preamble when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preamble")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ActionFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { Self { send_mail : None , editor_command : None , embedded_pty : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { TagsSettingsOverride { colors : None , ignore_tags : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { Self { colors : None , ignore_tags : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PGPSettingsOverride { # [doc = " auto verify signed e-mail according to RFC3156"] # [doc = " Default: true"] # [serde (alias = "auto-verify-signatures")] # [serde (default)] pub auto_verify_signatures : Option < bool > , # [doc = " auto decrypt encrypted e-mail"] # [doc = " Default: true"] # [serde (alias = "auto-decrypt")] # [serde (default)] pub auto_decrypt : Option < bool > , # [doc = " always sign sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-sign")] # [serde (default)] pub auto_sign : Option < bool > , # [doc = " Auto encrypt sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-encrypt")] # [serde (default)] pub auto_encrypt : Option < bool > , # [doc = " Default: None"] # [serde (alias = "sign-key")] # [serde (default)] pub sign_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "decrypt-key")] # [serde (default)] pub decrypt_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "encrypt-key")] # [serde (default)] pub encrypt_key : Option < Option < String > > , # [doc = " Allow remote lookups"] # [doc = " Default: None"] # [serde (alias = "allow-remote-lookups")] # [serde (default)] pub allow_remote_lookup : Option < ToggleFlag > , # [doc = " Remote lookup mechanisms."] # [doc = " Default: \"local,wkd\""] # [cfg_attr (feature = "gpgme" , serde (alias = "remote-lookup-mechanisms"))] # [cfg (feature = "gpgme")] # [serde (default)] pub remote_lookup_mechanisms : Option < melib :: gpgme :: LocateKey > , # [cfg (not (feature = "gpgme"))] # [cfg_attr (not (feature = "gpgme") , serde (alias = "remote-lookup-mechanisms"))] # [serde (default)] pub remote_lookup_mechanisms : Option < String > } impl Default for PGPSettingsOverride { fn default () -> Self { PGPSettingsOverride { auto_verify_signatures : None , auto_decrypt : None , auto_sign : None , auto_encrypt : None , sign_key : None , decrypt_key : None , encrypt_key : None , allow_remote_lookup : None , remote_lookup_mechanisms : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PGPSettingsOverride { # [doc = " auto verify signed e-mail according to RFC3156"] # [doc = " Default: true"] # [serde (alias = "auto-verify-signatures")] # [serde (default)] pub auto_verify_signatures : Option < ActionFlag > , # [doc = " auto decrypt encrypted e-mail"] # [doc = " Default: true"] # [serde (alias = "auto-decrypt")] # [serde (default)] pub auto_decrypt : Option < ActionFlag > , # [doc = " always sign sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-sign")] # [serde (default)] pub auto_sign : Option < ActionFlag > , # [doc = " Auto encrypt sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-encrypt")] # [serde (default)] pub auto_encrypt : Option < ActionFlag > , # [doc = " Default: None"] # [serde (alias = "sign-key")] # [serde (default)] pub sign_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "decrypt-key")] # [serde (default)] pub decrypt_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "encrypt-key")] # [serde (default)] pub encrypt_key : Option < Option < String > > , # [doc = " Allow remote lookups"] # [doc = " Default: False"] # [serde (alias = "allow-remote-lookups")] # [serde (default)] pub allow_remote_lookup : Option < ActionFlag > , # [doc = " Remote lookup mechanisms."] # [doc = " Default: \"local,wkd\""] # [cfg_attr (feature = "gpgme" , serde (alias = "remote-lookup-mechanisms"))] # [cfg (feature = "gpgme")] # [serde (default)] pub remote_lookup_mechanisms : Option < melib :: gpgme :: LocateKey > , # [cfg (not (feature = "gpgme"))] # [cfg_attr (not (feature = "gpgme") , serde (alias = "remote-lookup-mechanisms"))] # [serde (default)] pub remote_lookup_mechanisms : Option < String > } impl Default for PGPSettingsOverride { fn default () -> Self { Self { auto_verify_signatures : None , auto_decrypt : None , auto_sign : None , auto_encrypt : None , sign_key : None , decrypt_key : None , encrypt_key : None , allow_remote_lookup : None , remote_lookup_mechanisms : None } } }

View File

@ -19,7 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use melib::conf::ToggleFlag;
use melib::conf::ActionFlag;
use super::default_vals::*;
@ -30,22 +30,22 @@ pub struct PGPSettings {
/// auto verify signed e-mail according to RFC3156
/// Default: true
#[serde(default = "true_val", alias = "auto-verify-signatures")]
pub auto_verify_signatures: bool,
pub auto_verify_signatures: ActionFlag,
/// auto decrypt encrypted e-mail
/// Default: true
#[serde(default = "true_val", alias = "auto-decrypt")]
pub auto_decrypt: bool,
pub auto_decrypt: ActionFlag,
/// always sign sent e-mail
/// Default: false
#[serde(default = "false_val", alias = "auto-sign")]
pub auto_sign: bool,
pub auto_sign: ActionFlag,
/// Auto encrypt sent e-mail
/// Default: false
#[serde(default = "false_val", alias = "auto-encrypt")]
pub auto_encrypt: bool,
pub auto_encrypt: ActionFlag,
// https://tools.ietf.org/html/rfc4880#section-12.2
/// Default: None
@ -61,9 +61,12 @@ pub struct PGPSettings {
pub encrypt_key: Option<String>,
/// Allow remote lookups
/// Default: None
#[serde(default = "internal_value_false", alias = "allow-remote-lookups")]
pub allow_remote_lookup: ToggleFlag,
/// Default: False
#[serde(
default = "action_internal_value_false",
alias = "allow-remote-lookups"
)]
pub allow_remote_lookup: ActionFlag,
/// Remote lookup mechanisms.
/// Default: "local,wkd"
@ -91,15 +94,15 @@ fn default_lookup_mechanism() -> melib::gpgme::LocateKey {
impl Default for PGPSettings {
fn default() -> Self {
PGPSettings {
auto_verify_signatures: true,
auto_decrypt: true,
auto_sign: false,
auto_encrypt: false,
Self {
auto_verify_signatures: true.into(),
auto_decrypt: true.into(),
auto_sign: false.into(),
auto_encrypt: false.into(),
sign_key: None,
decrypt_key: None,
encrypt_key: None,
allow_remote_lookup: internal_value_false::<ToggleFlag>(),
allow_remote_lookup: action_internal_value_false::<ActionFlag>(),
#[cfg(feature = "gpgme")]
remote_lookup_mechanisms: default_lookup_mechanism(),
#[cfg(not(feature = "gpgme"))]

View File

@ -88,6 +88,12 @@ impl DotAddressable for Shortcuts {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandShortcut {
pub shortcut: Key,
pub command: Vec<String>,
}
/// Create a struct holding all of a Component's shortcuts.
#[macro_export]
macro_rules! shortcut_key_values {
@ -100,6 +106,7 @@ macro_rules! shortcut_key_values {
#[serde(default)]
#[serde(rename = $cname)]
pub struct $name {
pub commands: Vec<CommandShortcut>,
$(pub $fname : Key),*
}
@ -122,6 +129,7 @@ macro_rules! shortcut_key_values {
impl Default for $name {
fn default() -> Self {
Self {
commands : vec![],
$($fname: $default),*
}
}

View File

@ -55,7 +55,7 @@ pub struct TerminalSettings {
impl Default for TerminalSettings {
fn default() -> Self {
TerminalSettings {
Self {
theme: "dark".to_string(),
themes: Themes::default(),
ascii_drawing: false,

View File

@ -47,11 +47,14 @@ use crate::{
Context,
};
pub const LIGHT: &str = "light";
pub const DARK: &str = "dark";
#[inline(always)]
pub fn value(context: &Context, key: &'static str) -> ThemeAttribute {
let theme = match context.settings.terminal.theme.as_str() {
"light" => &context.settings.terminal.themes.light,
"dark" => &context.settings.terminal.themes.dark,
self::LIGHT => &context.settings.terminal.themes.light,
self::DARK => &context.settings.terminal.themes.dark,
t => context
.settings
.terminal
@ -66,8 +69,8 @@ pub fn value(context: &Context, key: &'static str) -> ThemeAttribute {
#[inline(always)]
pub fn fg_color(context: &Context, key: &'static str) -> Color {
let theme = match context.settings.terminal.theme.as_str() {
"light" => &context.settings.terminal.themes.light,
"dark" => &context.settings.terminal.themes.dark,
self::LIGHT => &context.settings.terminal.themes.light,
self::DARK => &context.settings.terminal.themes.dark,
t => context
.settings
.terminal
@ -82,8 +85,8 @@ pub fn fg_color(context: &Context, key: &'static str) -> Color {
#[inline(always)]
pub fn bg_color(context: &Context, key: &'static str) -> Color {
let theme = match context.settings.terminal.theme.as_str() {
"light" => &context.settings.terminal.themes.light,
"dark" => &context.settings.terminal.themes.dark,
self::LIGHT => &context.settings.terminal.themes.light,
self::DARK => &context.settings.terminal.themes.dark,
t => context
.settings
.terminal
@ -98,8 +101,8 @@ pub fn bg_color(context: &Context, key: &'static str) -> Color {
#[inline(always)]
pub fn attrs(context: &Context, key: &'static str) -> Attr {
let theme = match context.settings.terminal.theme.as_str() {
"light" => &context.settings.terminal.themes.light,
"dark" => &context.settings.terminal.themes.dark,
self::LIGHT => &context.settings.terminal.themes.light,
self::DARK => &context.settings.terminal.themes.dark,
t => context
.settings
.terminal
@ -318,6 +321,7 @@ const DEFAULT_KEYS: &[&str] = &[
"mail.listing.attachment_flag",
"mail.listing.thread_snooze_flag",
"mail.listing.tag_default",
"mail.listing.highlight_self",
"pager.highlight_search",
"pager.highlight_search_current",
];
@ -361,17 +365,17 @@ pub enum ColorField {
Bg,
}
/// The field a ThemeValue::Link refers to.
/// The field a `ThemeValue::Link` refers to.
trait ThemeLink {
type LinkType;
}
/// A color value that's a link can either refer to .fg or .bg field
/// A color value that's a link can either refer to `.fg` or `.bg` field
impl ThemeLink for Color {
type LinkType = ColorField;
}
/// An attr value that's a link can only refer to an .attr field
/// An `attr` value that's a link can only refer to an `.attr` field
impl ThemeLink for Attr {
type LinkType = ();
}
@ -387,37 +391,37 @@ enum ThemeValue<T: ThemeLink> {
impl From<&'static str> for ThemeValue<Color> {
fn from(from: &'static str) -> Self {
ThemeValue::Link(from.into(), ColorField::LikeSelf)
Self::Link(from.into(), ColorField::LikeSelf)
}
}
impl From<&'static str> for ThemeValue<Attr> {
fn from(from: &'static str) -> Self {
ThemeValue::Link(from.into(), ())
Self::Link(from.into(), ())
}
}
impl From<Color> for ThemeValue<Color> {
fn from(from: Color) -> Self {
ThemeValue::Value(from)
Self::Value(from)
}
}
impl From<Attr> for ThemeValue<Attr> {
fn from(from: Attr) -> Self {
ThemeValue::Value(from)
Self::Value(from)
}
}
impl Default for ThemeValue<Color> {
fn default() -> Self {
ThemeValue::Value(Color::Default)
Self::Value(Color::Default)
}
}
impl Default for ThemeValue<Attr> {
fn default() -> Self {
ThemeValue::Value(Attr::DEFAULT)
Self::Value(Attr::DEFAULT)
}
}
@ -428,11 +432,11 @@ impl<'de> Deserialize<'de> for ThemeValue<Attr> {
{
if let Ok(s) = <String>::deserialize(deserializer) {
if let Some(stripped) = s.strip_prefix('$') {
Ok(ThemeValue::Alias(stripped.to_string().into()))
Ok(Self::Alias(stripped.to_string().into()))
} else if let Ok(c) = Attr::from_string_de::<'de, D, String>(s.clone()) {
Ok(ThemeValue::Value(c))
Ok(Self::Value(c))
} else {
Ok(ThemeValue::Link(s.into(), ()))
Ok(Self::Link(s.into(), ()))
}
} else {
Err(de::Error::custom("invalid theme attribute value"))
@ -446,9 +450,9 @@ impl Serialize for ThemeValue<Attr> {
S: Serializer,
{
match self {
ThemeValue::Value(s) => s.serialize(serializer),
ThemeValue::Alias(s) => format!("${}", s).serialize(serializer),
ThemeValue::Link(s, ()) => serializer.serialize_str(s.as_ref()),
Self::Value(s) => s.serialize(serializer),
Self::Alias(s) => format!("${}", s).serialize(serializer),
Self::Link(s, ()) => serializer.serialize_str(s.as_ref()),
}
}
}
@ -459,15 +463,11 @@ impl Serialize for ThemeValue<Color> {
S: Serializer,
{
match self {
ThemeValue::Value(s) => s.serialize(serializer),
ThemeValue::Alias(s) => format!("${}", s).serialize(serializer),
ThemeValue::Link(s, ColorField::LikeSelf) => serializer.serialize_str(s.as_ref()),
ThemeValue::Link(s, ColorField::Fg) => {
serializer.serialize_str(format!("{}.fg", s).as_ref())
}
ThemeValue::Link(s, ColorField::Bg) => {
serializer.serialize_str(format!("{}.bg", s).as_ref())
}
Self::Value(s) => s.serialize(serializer),
Self::Alias(s) => format!("${}", s).serialize(serializer),
Self::Link(s, ColorField::LikeSelf) => serializer.serialize_str(s.as_ref()),
Self::Link(s, ColorField::Fg) => serializer.serialize_str(format!("{}.fg", s).as_ref()),
Self::Link(s, ColorField::Bg) => serializer.serialize_str(format!("{}.bg", s).as_ref()),
}
}
}
@ -479,21 +479,21 @@ impl<'de> Deserialize<'de> for ThemeValue<Color> {
{
if let Ok(s) = <String>::deserialize(deserializer) {
if let Some(stripped) = s.strip_prefix('$') {
Ok(ThemeValue::Alias(stripped.to_string().into()))
Ok(Self::Alias(stripped.to_string().into()))
} else if let Ok(c) = Color::from_string_de::<'de, D>(s.clone()) {
Ok(ThemeValue::Value(c))
Ok(Self::Value(c))
} else if s.ends_with(".fg") {
Ok(ThemeValue::Link(
Ok(Self::Link(
s[..s.len() - 3].to_string().into(),
ColorField::Fg,
))
} else if s.ends_with(".bg") {
Ok(ThemeValue::Link(
Ok(Self::Link(
s[..s.len() - 3].to_string().into(),
ColorField::Bg,
))
} else {
Ok(ThemeValue::Link(s.into(), ColorField::LikeSelf))
Ok(Self::Link(s.into(), ColorField::LikeSelf))
}
} else {
Err(de::Error::custom("invalid theme color value"))
@ -569,7 +569,7 @@ mod regexp {
impl Eq for RegexpWrapper {}
impl PartialEq for RegexpWrapper {
fn eq(&self, other: &RegexpWrapper) -> bool {
fn eq(&self, other: &Self) -> bool {
self.0.as_str().eq(other.0.as_str())
}
}
@ -652,8 +652,8 @@ mod regexp {
key: &'static str,
) -> SmallVec<[TextFormatter<'ctx>; 64]> {
let theme = match context.settings.terminal.theme.as_str() {
"light" => &context.settings.terminal.themes.light,
"dark" => &context.settings.terminal.themes.dark,
self::LIGHT => &context.settings.terminal.themes.light,
self::DARK => &context.settings.terminal.themes.dark,
t => context
.settings
.terminal
@ -827,7 +827,7 @@ impl<'de> Deserialize<'de> for Themes {
attrs: Option<ThemeValue<Attr>>,
}
let mut ret = Themes::default();
let mut ret = Self::default();
let mut s = <ThemesOptions>::deserialize(deserializer)?;
for tk in s.other_themes.keys() {
ret.other_themes.insert(tk.clone(), ret.dark.clone());
@ -1037,7 +1037,7 @@ impl Themes {
}
ThemeValue::Alias(ref ident) => {
if !theme.color_aliases.contains_key(ident.as_ref()) {
Some((Some(key), "alias", "nonexistant color alias", ident))
Some((Some(key), "alias", "nonexistent color alias", ident))
} else {
None
}
@ -1054,7 +1054,7 @@ impl Themes {
}
ThemeValue::Alias(ref ident) => {
if !theme.attr_aliases.contains_key(ident.as_ref()) {
Some((Some(key), "alias", "nonexistant color alias", ident))
Some((Some(key), "alias", "nonexistent color alias", ident))
} else {
None
}
@ -1070,7 +1070,7 @@ impl Themes {
}
} else if let ThemeValue::Alias(ref ident) = a.fg {
if !theme.color_aliases.contains_key(ident.as_ref()) {
Some((Some(key), "fg alias", "nonexistant color alias", ident))
Some((Some(key), "fg alias", "nonexistent color alias", ident))
} else {
None
}
@ -1087,7 +1087,7 @@ impl Themes {
}
} else if let ThemeValue::Alias(ref ident) = a.bg {
if !theme.color_aliases.contains_key(ident.as_ref()) {
Some((Some(key), "bg alias", "nonexistant color alias", ident))
Some((Some(key), "bg alias", "nonexistent color alias", ident))
} else {
None
}
@ -1107,7 +1107,7 @@ impl Themes {
Some((
Some(key),
"attrs alias",
"nonexistant text attribute alias",
"nonexistent text attribute alias",
ident,
))
} else {
@ -1145,7 +1145,7 @@ impl Themes {
keys.push((
Some(key),
"fg alias",
"nonexistant color alias in `text_format_regexps`",
"nonexistent color alias in `text_format_regexps`",
ident,
));
}
@ -1166,7 +1166,7 @@ impl Themes {
keys.push((
Some(key),
"bg alias",
"nonexistant color alias in `text_format_regexps`",
"nonexistent color alias in `text_format_regexps`",
ident,
));
}
@ -1187,7 +1187,7 @@ impl Themes {
keys.push((
Some(key),
"attrs alias",
"nonexistant text attribute alias in `text_format_regexps`",
"nonexistent text attribute alias in `text_format_regexps`",
ident,
));
}
@ -1217,10 +1217,10 @@ impl Themes {
}
pub fn validate(&self) -> Result<()> {
let hash_set: HashSet<&'static str> = DEFAULT_KEYS.iter().copied().collect();
Themes::validate_keys("light", &self.light, &hash_set)?;
Themes::validate_keys("dark", &self.dark, &hash_set)?;
Self::validate_keys(self::LIGHT, &self.light, &hash_set)?;
Self::validate_keys(self::DARK, &self.dark, &hash_set)?;
for (name, t) in self.other_themes.iter() {
Themes::validate_keys(name, t, &hash_set)?;
Self::validate_keys(name, t, &hash_set)?;
}
if let Err(err) = is_cyclic(&self.light) {
return Err(Error::new(format!("light theme contains a cycle: {}", err)));
@ -1238,8 +1238,8 @@ impl Themes {
pub fn key_to_string(&self, key: &str, unlink: bool) -> String {
let theme = match key {
"light" => &self.light,
"dark" => &self.dark,
self::LIGHT => &self.light,
self::DARK => &self.dark,
t => self.other_themes.get(t).unwrap_or(&self.dark),
};
let mut ret = String::new();
@ -1274,10 +1274,10 @@ impl Themes {
impl std::fmt::Display for Themes {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut ret = String::new();
ret.push_str(&self.key_to_string("dark", true));
ret.push_str(&self.key_to_string(self::DARK, true));
ret.push_str("\n\n");
ret.push_str(&self.key_to_string("light", true));
ret.push_str(&self.key_to_string(self::LIGHT, true));
for name in self.other_themes.keys() {
ret.push_str("\n\n");
ret.push_str(&self.key_to_string(name, true));
@ -1288,7 +1288,7 @@ impl std::fmt::Display for Themes {
impl Default for Themes {
#[allow(clippy::needless_update)]
fn default() -> Themes {
fn default() -> Self {
let mut light = IndexMap::default();
let mut dark = IndexMap::default();
let other_themes = IndexMap::default();
@ -1731,10 +1731,19 @@ impl Default for Themes {
attrs: Attr::BOLD
}
);
add!(
"mail.listing.highlight_self",
light = {
fg: Color::BLUE,
},
dark = {
fg: Color::BLUE,
}
);
add!("pager.highlight_search", light = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::BOLD }, dark = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::BOLD });
add!("pager.highlight_search_current", light = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::BOLD }, dark = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::BOLD });
Themes {
Self {
light: Theme {
keys: light,
attr_aliases: Default::default(),
@ -1807,8 +1816,8 @@ impl Serialize for Themes {
other_themes.insert(name.to_string(), new_map);
}
other_themes.insert("light".to_string(), light);
other_themes.insert("dark".to_string(), dark);
other_themes.insert(self::LIGHT.to_string(), light);
other_themes.insert(self::DARK.to_string(), dark);
other_themes.serialize(serializer)
}
}
@ -2085,7 +2094,7 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
fn test_theme_parsing() {
/* MUST SUCCEED: default themes should be valid */
let def = Themes::default();
assert!(def.validate().is_ok());
def.validate().unwrap();
/* MUST SUCCEED: new user theme `hunter2`, theme `dark` has user
* redefinitions */
const TEST_STR: &str = r#"[dark]
@ -2121,27 +2130,27 @@ fn test_theme_parsing() {
),
Color::Byte(15), // White
);
assert!(parsed.validate().is_ok());
parsed.validate().unwrap();
/* MUST FAIL: theme `dark` contains a cycle */
const HAS_CYCLE: &str = r#"[dark]
"mail.listing.compact.even" = { fg = "mail.listing.compact.odd" }
"mail.listing.compact.odd" = { fg = "mail.listing.compact.even" }
"#;
let parsed: Themes = toml::from_str(HAS_CYCLE).unwrap();
assert!(parsed.validate().is_err());
parsed.validate().unwrap_err();
/* MUST FAIL: theme `dark` contains an invalid key */
const HAS_INVALID_KEYS: &str = r#"[dark]
"asdfsafsa" = { fg = "Black" }
"#;
let parsed: std::result::Result<Themes, _> = toml::from_str(HAS_INVALID_KEYS);
assert!(parsed.is_err());
parsed.unwrap_err();
/* MUST SUCCEED: alias $Jebediah resolves to a valid color */
const TEST_ALIAS_STR: &str = r##"[dark]
color_aliases= { "Jebediah" = "#b4da55" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"##;
let parsed: Themes = toml::from_str(TEST_ALIAS_STR).unwrap();
assert!(parsed.validate().is_ok());
parsed.validate().unwrap();
assert_eq!(
unlink_fg(
&parsed.dark,
@ -2150,48 +2159,48 @@ color_aliases= { "Jebediah" = "#b4da55" }
),
Color::Rgb(180, 218, 85)
);
/* MUST FAIL: Mispell color alias $Jebediah as $Jebedia */
/* MUST FAIL: Misspell color alias $Jebediah as $Jebedia */
const TEST_INVALID_ALIAS_STR: &str = r##"[dark]
color_aliases= { "Jebediah" = "#b4da55" }
"mail.listing.tag_default" = { fg = "$Jebedia" }
"##;
let parsed: Themes = toml::from_str(TEST_INVALID_ALIAS_STR).unwrap();
assert!(parsed.validate().is_err());
parsed.validate().unwrap_err();
/* MUST FAIL: Color alias $Jebediah is defined as itself */
const TEST_CYCLIC_ALIAS_STR: &str = r#"[dark]
color_aliases= { "Jebediah" = "$Jebediah" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"#;
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR).unwrap();
assert!(parsed.validate().is_err());
parsed.validate().unwrap_err();
/* MUST FAIL: Attr alias $Jebediah is defined as itself */
const TEST_CYCLIC_ALIAS_ATTR_STR: &str = r#"[dark]
attr_aliases= { "Jebediah" = "$Jebediah" }
"mail.listing.tag_default" = { attrs = "$Jebediah" }
"#;
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_ATTR_STR).unwrap();
assert!(parsed.validate().is_err());
parsed.validate().unwrap_err();
/* MUST FAIL: alias $Jebediah resolves to a cycle */
const TEST_CYCLIC_ALIAS_STR_2: &str = r#"[dark]
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"#;
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_2).unwrap();
assert!(parsed.validate().is_err());
parsed.validate().unwrap_err();
/* MUST SUCCEED: alias $Jebediah resolves to a key's field */
const TEST_CYCLIC_ALIAS_STR_3: &str = r#"[dark]
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.bg" }
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
"#;
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_3).unwrap();
assert!(parsed.validate().is_ok());
parsed.validate().unwrap();
/* MUST FAIL: alias $Jebediah resolves to an invalid key */
const TEST_INVALID_LINK_KEY_FIELD_STR: &str = r#"[dark]
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.attrs" }
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
"#;
let parsed: Themes = toml::from_str(TEST_INVALID_LINK_KEY_FIELD_STR).unwrap();
assert!(parsed.validate().is_err());
parsed.validate().unwrap_err();
}
#[test]

View File

@ -19,8 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::collections::HashMap;
use indexmap::IndexMap;
use melib::Card;
use crate::{
@ -59,7 +58,7 @@ impl std::fmt::Display for ContactManager {
impl ContactManager {
pub fn new(context: &Context) -> Self {
let theme_default: ThemeAttribute = crate::conf::value(context, "theme_default");
ContactManager {
Self {
id: ComponentId::default(),
parent_id: None,
card: Card::new(),
@ -200,7 +199,7 @@ impl Component for ContactManager {
None => {}
Some(true) => {
let fields = std::mem::take(&mut self.form).collect().unwrap();
let fields: HashMap<String, String> = fields
let fields: IndexMap<String, String> = fields
.into_iter()
.map(|(s, v)| {
(
@ -227,14 +226,14 @@ impl Component for ContactManager {
}
}
self.set_dirty(true);
if let UIEvent::InsertInput(_) = event {
if matches!(event, UIEvent::InsertInput(_)) {
self.has_changes = true;
}
return true;
}
}
ViewMode::ReadOnly => {
if let &mut UIEvent::Input(Key::Esc) = event {
if matches!(event, UIEvent::Input(Key::Esc)) {
if self.can_quit_cleanly(context) {
self.unrealize(context);
}

View File

@ -21,7 +21,7 @@
use std::cmp;
use melib::{backends::AccountHash, text_processing::TextProcessing, Card, CardId, Draft};
use melib::{backends::AccountHash, text::TextProcessing, Card, CardId, Draft};
use crate::{
conf, contacts::editor::ContactManager, shortcut, terminal::*, Action::Tab, Component,
@ -88,7 +88,7 @@ impl ContactList {
index: i,
})
.collect();
ContactList {
Self {
accounts,
cursor_pos: 0,
new_cursor_pos: 0,
@ -112,13 +112,13 @@ impl ContactList {
}
pub fn for_account(pos: usize, context: &Context) -> Self {
ContactList {
Self {
account_pos: pos,
..Self::new(context)
}
}
fn initialize(&mut self, context: &mut Context) {
fn initialize(&mut self, context: &Context) {
self.data_columns.clear();
let account = &context.accounts[self.account_pos];
let book = &account.address_book;
@ -274,7 +274,7 @@ impl ContactList {
grid: &mut CellBuffer,
area: Area,
a: &AccountMenuEntry,
context: &mut Context,
context: &Context,
) {
let width = area.width();
let must_highlight_account: bool = self.account_pos == a.index;
@ -838,6 +838,25 @@ impl Component for ContactList {
self.movement = Some(PageMovement::End);
return true;
}
UIEvent::Input(ref key)
if context
.settings
.shortcuts
.contact_list
.commands
.iter()
.any(|cmd| {
if cmd.shortcut == *key {
for cmd in &cmd.command {
context.replies.push_back(UIEvent::Command(cmd.to_string()));
}
return true;
}
false
}) =>
{
return true;
}
_ => {}
}
}

View File

@ -176,7 +176,7 @@ impl JobExecutor {
/// A queue that holds scheduled tasks.
pub fn new(sender: Sender<ThreadEvent>) -> Self {
// Create a queue.
let mut ret = JobExecutor {
let mut ret = Self {
global_queue: Arc::new(Injector::new()),
workers: vec![],
parkers: vec![],
@ -185,7 +185,10 @@ impl JobExecutor {
jobs: Arc::new(Mutex::new(IndexMap::default())),
};
let mut workers = vec![];
for _ in 0..num_cpus::get().max(1) {
for _ in 0..std::thread::available_parallelism()
.map(Into::into)
.unwrap_or(1)
{
let new_worker = Worker::new_fifo();
ret.workers.push(new_worker.stealer());
let p = Parker::new();
@ -234,6 +237,7 @@ impl JobExecutor {
}
/// Spawns a future with a generic return value `R`
#[inline(always)]
pub fn spawn_specialized<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
where
F: Future<Output = R> + Send + 'static,
@ -295,6 +299,7 @@ impl JobExecutor {
/// Spawns a future with a generic return value `R` that might block on a
/// new thread
#[inline(always)]
pub fn spawn_blocking<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
where
F: Future<Output = R> + Send + 'static,
@ -306,7 +311,7 @@ impl JobExecutor {
)
}
pub fn create_timer(self: Arc<JobExecutor>, interval: Duration, value: Duration) -> Timer {
pub fn create_timer(self: Arc<Self>, interval: Duration, value: Duration) -> Timer {
let timer = TimerPrivate {
interval,
cancel: Arc::new(Mutex::new(false)),
@ -415,7 +420,7 @@ impl JobExecutor {
pub type JobChannel<T> = oneshot::Receiver<T>;
/// JoinHandle for the future that allows us to cancel the task.
/// `JoinHandle` for the future that allows us to cancel the task.
#[derive(Debug)]
pub struct JoinHandle<T> {
pub task: Arc<Mutex<Option<async_task::Task<()>>>>,

View File

@ -110,7 +110,7 @@ impl JobManager {
}
}
fn initialize(&mut self, context: &mut Context) {
fn initialize(&mut self, context: &Context) {
self.set_dirty(true);
let mut entries = (*context.main_loop_handler.job_executor.jobs.lock().unwrap()).clone();

View File

@ -19,6 +19,58 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
#![deny(
rustdoc::redundant_explicit_links,
/* groups */
clippy::correctness,
clippy::suspicious,
clippy::complexity,
clippy::perf,
clippy::cargo,
clippy::nursery,
clippy::style,
/* restriction */
clippy::dbg_macro,
clippy::rc_buffer,
clippy::as_underscore,
clippy::assertions_on_result_states,
/* rustdoc */
rustdoc::broken_intra_doc_links,
/* pedantic */
//clippy::cast_lossless,
//clippy::cast_possible_wrap,
//clippy::ptr_as_ptr,
clippy::doc_markdown,
clippy::expect_fun_call,
clippy::bool_to_int_with_if,
clippy::borrow_as_ptr,
clippy::cast_ptr_alignment,
clippy::large_futures,
clippy::waker_clone_wake,
clippy::unused_enumerate_index,
clippy::unnecessary_fallible_conversions,
clippy::struct_field_names,
clippy::manual_hash_one,
clippy::into_iter_without_iter,
)]
#![allow(
clippy::option_if_let_else,
clippy::missing_const_for_fn,
clippy::significant_drop_tightening,
clippy::multiple_crate_versions,
clippy::significant_drop_in_scrutinee,
clippy::cognitive_complexity,
clippy::manual_clamp
)]
/* Source Code Annotation Tags:
*
* Global tags (in tagref format <https://github.com/stepchowfun/tagref>) for source code
* annotation:
*
* - tags from melib/src/lib.rs.
* - [tag:hardcoded_color_value] Replace hardcoded color values with user configurable ones.
*/
//!
//! This crate contains the frontend stuff of the application. The application
//! entry way on `src/bin.rs` creates an event loop and passes input to a
@ -38,7 +90,6 @@ pub use melib::uuid;
pub extern crate bitflags;
pub extern crate serde_json;
#[macro_use]
pub extern crate smallvec;
pub extern crate termion;
@ -49,11 +100,13 @@ static GLOBAL: System = System;
pub extern crate melib;
pub use melib::{
error::*, log, AccountHash, Envelope, EnvelopeHash, EnvelopeRef, Flag, LogLevel, Mail, Mailbox,
MailboxHash, ThreadHash, ToggleFlag,
error::*, log, AccountHash, ActionFlag, Envelope, EnvelopeHash, EnvelopeRef, Flag, LogLevel,
Mail, Mailbox, MailboxHash, ThreadHash, ToggleFlag,
};
pub mod args;
#[cfg(feature = "cli-docs")]
pub mod manpages;
pub mod subcommands;
#[macro_use]

View File

@ -30,7 +30,7 @@ use melib::{
use super::*;
use crate::{
melib::text_processing::{TextProcessing, Truncate},
melib::text::{TextProcessing, Truncate},
uuid::Uuid,
};

View File

@ -134,12 +134,12 @@ enum ViewMode {
impl ViewMode {
#[inline]
fn is_edit(&self) -> bool {
matches!(self, ViewMode::Edit)
matches!(self, Self::Edit)
}
#[inline]
fn is_edit_attachments(&self) -> bool {
matches!(self, ViewMode::EditAttachments { .. })
matches!(self, Self::EditAttachments { .. })
}
}
@ -169,7 +169,7 @@ impl Composer {
pager.set_show_scrollbar(true);
let mut form = FormWidget::default();
form.set_cursor(2);
Composer {
Self {
reply_context: None,
account_hash: AccountHash::default(),
cursor: Cursor::Headers,
@ -195,9 +195,9 @@ impl Composer {
}
pub fn with_account(account_hash: AccountHash, context: &Context) -> Self {
let mut ret = Composer {
let mut ret = Self {
account_hash,
..Composer::new(context)
..Self::new(context)
};
// Add user's custom hooks.
@ -237,8 +237,7 @@ impl Composer {
);
}
if *account_settings!(context[account_hash].composing.format_flowed) {
ret.pager
.set_reflow(melib::text_processing::Reflow::FormatFlowed);
ret.pager.set_reflow(melib::text::Reflow::FormatFlowed);
}
ret
}
@ -249,7 +248,7 @@ impl Composer {
bytes: &[u8],
context: &Context,
) -> Result<Self> {
let mut ret = Composer::with_account(account_hash, context);
let mut ret = Self::with_account(account_hash, context);
// Add user's custom hooks.
for hook in account_settings!(context[account_hash].composing.custom_compose_hooks)
.iter()
@ -274,10 +273,10 @@ impl Composer {
pub fn reply_to(
coordinates @ (account_hash, _, _): (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
context: &Context,
reply_to_all: bool,
) -> Self {
let mut ret = Composer::with_account(account_hash, context);
let mut ret = Self::with_account(account_hash, context);
// Add user's custom hooks.
for hook in account_settings!(context[account_hash].composing.custom_compose_hooks)
.iter()
@ -388,15 +387,11 @@ impl Composer {
to.extend(envelope.from().iter().cloned());
}
to.extend(envelope.to().iter().cloned());
if let Ok(ours) = TryInto::<Address>::try_into(
context.accounts[&coordinates.0]
.settings
.account()
.make_display_name()
.as_str(),
) {
to.remove(&ours);
}
let ours = context.accounts[&coordinates.0]
.settings
.account()
.make_display_name();
to.remove(&ours);
ret.draft.set_header(HeaderName::TO, {
let mut ret: String =
to.into_iter()
@ -428,7 +423,7 @@ impl Composer {
)
.as_ref()
.map(|s| s.as_str()),
envelope.from().get(0),
envelope.from().first(),
envelope.date(),
*account_settings!(
context[ret.account_hash]
@ -452,9 +447,9 @@ impl Composer {
pub fn reply_to_select(
coordinates @ (account_hash, _, _): (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
context: &Context,
) -> Self {
let mut ret = Composer::reply_to(coordinates, reply_body, context, false);
let mut ret = Self::reply_to(coordinates, reply_body, context, false);
let account = &context.accounts[&account_hash];
let parent_message = account.collection.get_env(coordinates.2);
/* If message is from a mailing list and we detect a List-Post header, ask
@ -500,17 +495,17 @@ impl Composer {
pub fn reply_to_author(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
context: &Context,
) -> Self {
Composer::reply_to(coordinates, reply_body, context, false)
Self::reply_to(coordinates, reply_body, context, false)
}
pub fn reply_to_all(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
context: &Context,
) -> Self {
Composer::reply_to(coordinates, reply_body, context, true)
Self::reply_to(coordinates, reply_body, context, true)
}
pub fn forward(
@ -518,9 +513,9 @@ impl Composer {
bytes: &[u8],
env: &Envelope,
as_attachment: bool,
context: &mut Context,
context: &Context,
) -> Self {
let mut composer = Composer::with_account(coordinates.0, context);
let mut composer = Self::with_account(coordinates.0, context);
let mut draft: Draft = Draft::default();
draft.set_header(HeaderName::SUBJECT, format!("Fwd: {}", env.subject()));
let preamble = format!(
@ -672,7 +667,7 @@ To: {}
}
};
(addr, desc)
(addr.to_string(), desc)
})
.map(AutoCompleteEntry::from)
.collect::<Vec<AutoCompleteEntry>>()
@ -689,7 +684,12 @@ To: {}
let theme_default = crate::conf::value(context, "theme_default");
grid.clear_area(area, theme_default);
#[cfg(feature = "gpgme")]
if self.gpg_state.sign_mail.is_true() {
if self
.gpg_state
.sign_mail
.unwrap_or(ActionFlag::False)
.is_true()
{
let key_list = self
.gpg_state
.sign_keys
@ -732,7 +732,12 @@ To: {}
);
}
#[cfg(feature = "gpgme")]
if self.gpg_state.encrypt_mail.is_true() {
if self
.gpg_state
.encrypt_mail
.unwrap_or(ActionFlag::False)
.is_true()
{
let key_list = self
.gpg_state
.encrypt_keys
@ -868,10 +873,9 @@ impl Component for Composer {
if !self.initialized {
#[cfg(feature = "gpgme")]
if self.gpg_state.sign_mail.is_unset() {
self.gpg_state.sign_mail = ToggleFlag::InternalVal(*account_settings!(
context[self.account_hash].pgp.auto_sign
));
if self.gpg_state.sign_mail.is_none() {
self.gpg_state.sign_mail =
Some(*account_settings!(context[self.account_hash].pgp.auto_sign));
}
if !self.draft.headers().contains_key(HeaderName::FROM)
|| self.draft.headers()[HeaderName::FROM].is_empty()
@ -881,7 +885,8 @@ impl Component for Composer {
context.accounts[&self.account_hash]
.settings
.account()
.make_display_name(),
.make_display_name()
.to_string(),
);
}
self.pager.update_from_str(self.draft.body(), Some(77));
@ -1021,8 +1026,8 @@ impl Component for Composer {
guard.grid.set_dirty(false);
self.dirty = false;
}
return;
}
return;
} else {
self.embedded_dimensions = (area.width(), area.height());
}
@ -1172,7 +1177,7 @@ impl Component for Composer {
(ViewMode::Send(ref selector), UIEvent::FinishedUIDialog(id, result))
if selector.id() == *id =>
{
if let Some(true) = result.downcast_ref::<bool>() {
if matches!(result.downcast_ref::<bool>(), Some(true)) {
self.update_draft();
match send_draft_async(
#[cfg(feature = "gpgme")]
@ -1458,12 +1463,20 @@ impl Component for Composer {
#[cfg(feature = "gpgme")]
match self.cursor {
Cursor::Sign => {
let is_true = self.gpg_state.sign_mail.is_true();
self.gpg_state.sign_mail = ToggleFlag::from(!is_true);
let is_true = self
.gpg_state
.sign_mail
.unwrap_or(ActionFlag::False)
.is_true();
self.gpg_state.sign_mail = Some(ActionFlag::from(!is_true));
}
Cursor::Encrypt => {
let is_true = self.gpg_state.encrypt_mail.is_true();
self.gpg_state.encrypt_mail = ToggleFlag::from(!is_true);
let is_true = self
.gpg_state
.encrypt_mail
.unwrap_or(ActionFlag::False)
.is_true();
self.gpg_state.encrypt_mail = Some(ActionFlag::from(!is_true));
}
_ => {}
};
@ -1482,7 +1495,8 @@ impl Component for Composer {
..
} = self;
for err in hooks
// Collect errors in a vector because filter_map borrows context
let errors = hooks
.iter_mut()
.filter_map(|h| {
if let Err(err) = h(context, draft) {
@ -1491,8 +1505,8 @@ impl Component for Composer {
None
}
})
.collect::<Vec<_>>()
{
.collect::<Vec<_>>();
for err in errors {
context.replies.push_back(UIEvent::Notification {
title: None,
source: None,
@ -1671,7 +1685,7 @@ impl Component for Composer {
)
.map_err(|_err| -> Error { "No valid sender address in `From:`".into() })
.and_then(|(_, list)| {
list.get(0)
list.first()
.cloned()
.ok_or_else(|| "No valid sender address in `From:`".into())
})
@ -1686,7 +1700,7 @@ impl Component for Composer {
)
}) {
Ok(widget) => {
self.gpg_state.sign_mail = ToggleFlag::from(true);
self.gpg_state.sign_mail = Some(ActionFlag::from(true));
self.mode = ViewMode::SelectEncryptKey(false, widget);
}
Err(err) => {
@ -1712,7 +1726,7 @@ impl Component for Composer {
)
.map_err(|_err| -> Error { "No valid recipient addresses in `To:`".into() })
.and_then(|(_, list)| {
list.get(0)
list.first()
.cloned()
.ok_or_else(|| "No valid recipient addresses in `To:`".into())
})
@ -1727,7 +1741,7 @@ impl Component for Composer {
)
}) {
Ok(widget) => {
self.gpg_state.encrypt_mail = ToggleFlag::from(true);
self.gpg_state.encrypt_mail = Some(ActionFlag::from(true));
self.mode = ViewMode::SelectEncryptKey(true, widget);
}
Err(err) => {
@ -1862,10 +1876,11 @@ impl Component for Composer {
};
if *account_settings!(context[self.account_hash].composing.embedded_pty) {
let command = [editor, f.path().display().to_string()].join(" ");
match crate::terminal::embedded::create_pty(
self.embedded_dimensions.0,
self.embedded_dimensions.1,
[editor, f.path().display().to_string()].join(" "),
&command,
) {
Ok(terminal) => {
self.embedded_pty = Some(EmbeddedPty {
@ -1877,14 +1892,17 @@ impl Component for Composer {
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embedded));
context.replies.push_back(UIEvent::Fork(ForkType::Embedded(
self.embedded_pty
context.replies.push_back(UIEvent::Fork(ForkType::Embedded {
id: "editor".into(),
command: Some(command.into()),
pid: self
.embedded_pty
.as_ref()
.unwrap()
.lock()
.unwrap()
.child_pid,
)));
}));
self.mode = ViewMode::EmbeddedPty;
}
Err(err) => {
@ -1973,7 +1991,7 @@ impl Component for Composer {
});
return false;
}
match File::create_temp_file(&[], None, None, None, true)
let res = File::create_temp_file(&[], None, None, None, true)
.and_then(|f| {
let std_file = f.as_std_file()?;
Ok((
@ -1985,8 +2003,8 @@ impl Component for Composer {
.spawn()?,
))
})
.and_then(|(f, child)| Ok((f, child.wait_with_output()?.stderr)))
{
.and_then(|(f, child)| Ok((f, child.wait_with_output()?.stderr)));
match res {
Ok((f, stderr)) => {
if !stderr.is_empty() {
context.replies.push_back(UIEvent::StatusEvent(
@ -2166,20 +2184,47 @@ impl Component for Composer {
}
#[cfg(feature = "gpgme")]
Action::Compose(ComposeAction::ToggleSign) => {
let is_true = self.gpg_state.sign_mail.is_true();
self.gpg_state.sign_mail = ToggleFlag::from(!is_true);
let is_true = self
.gpg_state
.sign_mail
.unwrap_or(ActionFlag::False)
.is_true();
self.gpg_state.sign_mail = Some(ActionFlag::from(!is_true));
self.set_dirty(true);
return true;
}
#[cfg(feature = "gpgme")]
Action::Compose(ComposeAction::ToggleEncrypt) => {
let is_true = self.gpg_state.encrypt_mail.is_true();
self.gpg_state.encrypt_mail = ToggleFlag::from(!is_true);
let is_true = self
.gpg_state
.encrypt_mail
.unwrap_or(ActionFlag::False)
.is_true();
self.gpg_state.encrypt_mail = Some(ActionFlag::from(!is_true));
self.set_dirty(true);
return true;
}
_ => {}
},
UIEvent::Input(ref key)
if context
.settings
.shortcuts
.composing
.commands
.iter()
.any(|cmd| {
if cmd.shortcut == *key {
for cmd in &cmd.command {
context.replies.push_back(UIEvent::Command(cmd.to_string()));
}
return true;
}
false
}) =>
{
return true;
}
_ => {}
}
false
@ -2480,7 +2525,7 @@ pub fn save_draft(
pub fn send_draft_async(
#[cfg(feature = "gpgme")] gpg_state: gpg::GpgComposeState,
context: &mut Context,
context: &Context,
account_hash: AccountHash,
mut draft: Draft,
mailbox_type: SpecialUsageMailbox,
@ -2501,13 +2546,22 @@ pub fn send_draft_async(
>,
> = vec![];
#[cfg(feature = "gpgme")]
if gpg_state.sign_mail.is_true() && !gpg_state.encrypt_mail.is_true() {
if gpg_state.sign_mail.unwrap_or(ActionFlag::False).is_true()
&& !gpg_state
.encrypt_mail
.unwrap_or(ActionFlag::False)
.is_true()
{
filters_stack.push(Box::new(crate::mail::pgp::sign_filter(
gpg_state.sign_keys,
)?));
} else if gpg_state.encrypt_mail.is_true() {
} else if gpg_state
.encrypt_mail
.unwrap_or(ActionFlag::False)
.is_true()
{
filters_stack.push(Box::new(crate::mail::pgp::encrypt_filter(
if gpg_state.sign_mail.is_true() {
if gpg_state.sign_mail.unwrap_or(ActionFlag::False).is_true() {
Some(gpg_state.sign_keys.clone())
} else {
None
@ -2643,7 +2697,7 @@ hello world.
let envelope =
Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let tempdir = tempfile::tempdir().unwrap();
let mut context = Context::new_mock(&tempdir);
let context = Context::new_mock(&tempdir);
let account_hash = context.accounts[0].hash();
let mailbox_hash = MailboxHash::default();
let envelope_hash = envelope.hash();
@ -2653,7 +2707,7 @@ hello world.
let composer = Composer::reply_to(
(account_hash, mailbox_hash, envelope_hash),
String::new(),
&mut context,
&context,
false,
);
assert_eq!(
@ -2682,7 +2736,7 @@ hello world.
let composer = Composer::reply_to(
(account_hash, mailbox_hash, envelope_hash),
String::new(),
&mut context,
&context,
false,
);
assert_eq!(

View File

@ -54,7 +54,7 @@ impl EditAttachments {
let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonActions::Cancel));
buttons.set_focus(true);
buttons.set_cursor(1);
EditAttachments {
Self {
account_hash,
mode: EditAttachmentMode::Overview,
buttons,

View File

@ -29,7 +29,7 @@ pub enum KeySelection {
secret: bool,
local: bool,
pattern: String,
allow_remote_lookup: ToggleFlag,
allow_remote_lookup: ActionFlag,
},
Error {
id: ComponentId,
@ -52,8 +52,8 @@ impl KeySelection {
secret: bool,
local: bool,
pattern: String,
allow_remote_lookup: ToggleFlag,
context: &mut Context,
allow_remote_lookup: ActionFlag,
context: &Context,
) -> Result<Self> {
use melib::gpgme::*;
let mut ctx = Context::new()?;
@ -69,7 +69,7 @@ impl KeySelection {
.spawn_specialized("gpg::keylist".into(), job);
let mut progress_spinner = ProgressSpinner::new(8, context);
progress_spinner.start();
Ok(KeySelection::LoadingKeys {
Ok(Self::LoadingKeys {
handle,
secret,
local,
@ -83,11 +83,11 @@ impl KeySelection {
impl Component for KeySelection {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
match self {
KeySelection::LoadingKeys {
Self::LoadingKeys {
ref mut progress_spinner,
..
} => progress_spinner.draw(grid, area.center_inside((2, 2)), context),
KeySelection::Error { ref err, .. } => {
Self::Error { ref err, .. } => {
let theme_default = crate::conf::value(context, "theme_default");
grid.write_string(
&err.to_string(),
@ -98,13 +98,13 @@ impl Component for KeySelection {
Some(0),
);
}
KeySelection::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
Self::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
}
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
match self {
KeySelection::LoadingKeys {
Self::LoadingKeys {
ref mut progress_spinner,
ref mut handle,
secret,
@ -131,10 +131,10 @@ impl Component for KeySelection {
Ok(w) => {
*self = w;
}
Err(err) => *self = KeySelection::Error { err, id },
Err(err) => *self = Self::Error { err, id },
}
} else if !*local && allow_remote_lookup.is_ask() {
*self = KeySelection::Error {
*self = Self::Error {
err: Error::new(format!(
"No keys found for {}, perform remote lookup?",
pattern
@ -142,12 +142,12 @@ impl Component for KeySelection {
id,
}
} else {
*self = KeySelection::Error {
*self = Self::Error {
err: Error::new(format!("No keys found for {}.", pattern)),
id,
}
}
if let KeySelection::Error { ref err, .. } = self {
if let Self::Error { ref err, .. } = self {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
@ -177,17 +177,17 @@ impl Component for KeySelection {
move |id: ComponentId, results: &[melib::gpgme::Key]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.get(0).cloned()),
Box::new(results.first().cloned()),
))
},
)),
context,
));
widget.set_dirty(true);
*self = KeySelection::Loaded { widget, keys };
*self = Self::Loaded { widget, keys };
}
Ok(Some(Err(err))) => {
*self = KeySelection::Error {
*self = Self::Error {
err,
id: ComponentId::default(),
};
@ -197,30 +197,30 @@ impl Component for KeySelection {
}
_ => progress_spinner.process_event(event, context),
},
KeySelection::Error { .. } => false,
KeySelection::Loaded { ref mut widget, .. } => widget.process_event(event, context),
Self::Error { .. } => false,
Self::Loaded { ref mut widget, .. } => widget.process_event(event, context),
}
}
fn is_dirty(&self) -> bool {
match self {
KeySelection::LoadingKeys {
Self::LoadingKeys {
ref progress_spinner,
..
} => progress_spinner.is_dirty(),
KeySelection::Error { .. } => true,
KeySelection::Loaded { ref widget, .. } => widget.is_dirty(),
Self::Error { .. } => true,
Self::Loaded { ref widget, .. } => widget.is_dirty(),
}
}
fn set_dirty(&mut self, value: bool) {
match self {
KeySelection::LoadingKeys {
Self::LoadingKeys {
ref mut progress_spinner,
..
} => progress_spinner.set_dirty(value),
KeySelection::Error { .. } => {}
KeySelection::Loaded { ref mut widget, .. } => widget.set_dirty(value),
Self::Error { .. } => {}
Self::Loaded { ref mut widget, .. } => widget.set_dirty(value),
}
}
@ -228,29 +228,27 @@ impl Component for KeySelection {
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
match self {
KeySelection::LoadingKeys { .. } | KeySelection::Error { .. } => {
ShortcutMaps::default()
}
KeySelection::Loaded { ref widget, .. } => widget.shortcuts(context),
Self::LoadingKeys { .. } | Self::Error { .. } => ShortcutMaps::default(),
Self::Loaded { ref widget, .. } => widget.shortcuts(context),
}
}
fn id(&self) -> ComponentId {
match self {
KeySelection::LoadingKeys {
Self::LoadingKeys {
ref progress_spinner,
..
} => progress_spinner.id(),
KeySelection::Error { ref id, .. } => *id,
KeySelection::Loaded { ref widget, .. } => widget.id(),
Self::Error { ref id, .. } => *id,
Self::Loaded { ref widget, .. } => widget.id(),
}
}
}
#[derive(Clone, Debug)]
pub struct GpgComposeState {
pub sign_mail: ToggleFlag,
pub encrypt_mail: ToggleFlag,
pub sign_mail: Option<ActionFlag>,
pub encrypt_mail: Option<ActionFlag>,
pub encrypt_keys: Vec<melib::gpgme::Key>,
pub encrypt_for_self: bool,
pub sign_keys: Vec<melib::gpgme::Key>,
@ -258,9 +256,9 @@ pub struct GpgComposeState {
impl Default for GpgComposeState {
fn default() -> Self {
GpgComposeState {
sign_mail: ToggleFlag::Unset,
encrypt_mail: ToggleFlag::Unset,
Self {
sign_mail: None,
encrypt_mail: None,
encrypt_keys: vec![],
encrypt_for_self: true,
sign_keys: vec![],

View File

@ -31,7 +31,8 @@ use std::{
use futures::future::try_join_all;
use melib::{
backends::EnvelopeHashBatch, mbox::MboxMetadata, utils::datetime, FlagOp, UnixTimestamp,
backends::EnvelopeHashBatch, mbox::MboxMetadata, utils::datetime, Flag, FlagOp,
ShellExpandTrait, UnixTimestamp,
};
use smallvec::SmallVec;
@ -41,25 +42,11 @@ use crate::{
components::ExtendShortcutsMaps,
};
// [ref:TODO]: emoji_text_presentation_selector should be printed along with the chars
// before it but not as a separate Cell
//macro_rules! emoji_text_presentation_selector {
// () => {
// "\u{FE0E}"
// };
//}
//
//pub const DEFAULT_ATTACHMENT_FLAG: &str = concat!("📎",
// emoji_text_presentation_selector!()); pub const DEFAULT_SELECTED_FLAG: &str =
// concat!("☑️", emoji_text_presentation_selector!());
// pub const DEFAULT_UNSEEN_FLAG: &str = concat!("●",
// emoji_text_presentation_selector!()); pub const DEFAULT_SNOOZED_FLAG: &str =
// concat!("💤", emoji_text_presentation_selector!());
pub const DEFAULT_ATTACHMENT_FLAG: &str = "📎";
pub const DEFAULT_SELECTED_FLAG: &str = "☑️";
pub const DEFAULT_UNSEEN_FLAG: &str = "";
pub const DEFAULT_SNOOZED_FLAG: &str = "💤";
pub const DEFAULT_ATTACHMENT_FLAG: &str = concat!("📎", emoji_text_presentation_selector!());
pub const DEFAULT_SELECTED_FLAG: &str = concat!("☑️", emoji_text_presentation_selector!());
pub const DEFAULT_UNSEEN_FLAG: &str = concat!("", emoji_text_presentation_selector!());
pub const DEFAULT_SNOOZED_FLAG: &str = concat!("💤", emoji_text_presentation_selector!());
pub const DEFAULT_HIGHLIGHT_SELF_FLAG: &str = concat!("", emoji_text_presentation_selector!());
#[derive(Debug, Default)]
pub struct RowsState<T> {
@ -281,6 +268,7 @@ pub struct ColorCache {
pub even_highlighted_selected: ThemeAttribute,
pub odd_highlighted_selected: ThemeAttribute,
pub tag_default: ThemeAttribute,
pub highlight_self: ThemeAttribute,
// Conversations
pub subject: ThemeAttribute,
@ -290,6 +278,12 @@ pub struct ColorCache {
impl ColorCache {
pub fn new(context: &Context, style: IndexStyle) -> Self {
let default = Self {
theme_default: crate::conf::value(context, "theme_default"),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
highlight_self: crate::conf::value(context, "mail.listing.highlight_self"),
..Self::default()
};
let mut ret = match style {
IndexStyle::Plain => Self {
even: crate::conf::value(context, "mail.listing.plain.even"),
@ -311,9 +305,7 @@ impl ColorCache {
"mail.listing.plain.even_highlighted_selected",
),
odd_selected: crate::conf::value(context, "mail.listing.plain.odd_selected"),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
theme_default: crate::conf::value(context, "theme_default"),
..Self::default()
..default
},
IndexStyle::Threaded => Self {
even_unseen: crate::conf::value(context, "mail.listing.plain.even_unseen"),
@ -335,9 +327,7 @@ impl ColorCache {
),
even: crate::conf::value(context, "mail.listing.plain.even"),
odd: crate::conf::value(context, "mail.listing.plain.odd"),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
theme_default: crate::conf::value(context, "theme_default"),
..Self::default()
..default
},
IndexStyle::Compact => Self {
even_unseen: crate::conf::value(context, "mail.listing.compact.even_unseen"),
@ -362,12 +352,9 @@ impl ColorCache {
),
even: crate::conf::value(context, "mail.listing.compact.even"),
odd: crate::conf::value(context, "mail.listing.compact.odd"),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
theme_default: crate::conf::value(context, "theme_default"),
..Self::default()
..default
},
IndexStyle::Conversations => Self {
theme_default: crate::conf::value(context, "mail.listing.conversations"),
subject: crate::conf::value(context, "mail.listing.conversations.subject"),
from: crate::conf::value(context, "mail.listing.conversations.from"),
date: crate::conf::value(context, "mail.listing.conversations.date"),
@ -378,13 +365,13 @@ impl ColorCache {
context,
"mail.listing.conversations.highlighted_selected",
),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
..Self::default()
..default
},
};
if !context.settings.terminal.use_color() {
ret.highlighted.attrs |= Attr::REVERSE;
ret.tag_default.attrs |= Attr::REVERSE;
ret.highlight_self.attrs |= Attr::REVERSE;
ret.even_highlighted.attrs |= Attr::REVERSE;
ret.odd_highlighted.attrs |= Attr::REVERSE;
ret.even_highlighted_selected.attrs |= Attr::REVERSE | Attr::DIM;
@ -401,6 +388,8 @@ pub struct EntryStrings {
pub flag: FlagString,
pub from: FromString,
pub tags: TagString,
pub unseen: bool,
pub highlight_self: bool,
}
#[macro_export]
@ -469,6 +458,86 @@ column_str!(struct SubjectString(String));
column_str!(struct FlagString(String));
column_str!(struct TagString(String, SmallVec<[Option<Color>; 8]>));
impl FlagString {
pub(self) fn new(
flags: Flag,
is_selected: bool,
is_snoozed: bool,
is_unseen: bool,
has_attachments: bool,
context: &Context,
coordinates: (AccountHash, MailboxHash),
) -> Self {
Self(format!(
"{flag_passed}{flag_replied}{flag_seen}{flag_trashed}{flag_draft}{flag_flagged} \
{selected}{snoozed}{unseen}{attachments}{whitespace}",
flag_passed = Some("P")
.filter(|_| flags.contains(Flag::PASSED))
.unwrap_or_default(),
flag_replied = Some("R")
.filter(|_| flags.contains(Flag::REPLIED))
.unwrap_or_default(),
flag_seen = Some("S")
.filter(|_| flags.contains(Flag::SEEN))
.unwrap_or_default(),
flag_trashed = Some("T")
.filter(|_| flags.contains(Flag::TRASHED))
.unwrap_or_default(),
flag_draft = Some("D")
.filter(|_| flags.contains(Flag::DRAFT))
.unwrap_or_default(),
flag_flagged = Some("F")
.filter(|_| flags.contains(Flag::FLAGGED))
.unwrap_or_default(),
selected = if is_selected {
mailbox_settings!(context[coordinates.0][&coordinates.1].listing.selected_flag)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(DEFAULT_SELECTED_FLAG)
} else {
""
},
snoozed = if is_snoozed {
mailbox_settings!(
context[coordinates.0][&coordinates.1]
.listing
.thread_snoozed_flag
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(DEFAULT_SNOOZED_FLAG)
} else {
""
},
unseen = if is_unseen {
mailbox_settings!(context[coordinates.0][&coordinates.1].listing.unseen_flag)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(DEFAULT_UNSEEN_FLAG)
} else {
""
},
attachments = if has_attachments {
mailbox_settings!(
context[coordinates.0][&coordinates.1]
.listing
.attachment_flag
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(DEFAULT_ATTACHMENT_FLAG)
} else {
""
},
whitespace = if is_selected || is_unseen || is_snoozed || has_attachments {
" "
} else {
""
},
))
}
}
#[derive(Clone, Copy, Debug)]
struct MailboxMenuEntry {
depth: usize,
@ -488,6 +557,18 @@ struct AccountMenuEntry {
entries: SmallVec<[MailboxMenuEntry; 16]>,
}
impl AccountMenuEntry {
fn entry_by_hash(&self, needle: MailboxHash) -> Option<usize> {
self.entries.iter().enumerate().find_map(|(i, e)| {
if e.mailbox_hash == needle {
Some(i)
} else {
None
}
})
}
}
pub trait MailListingTrait: ListingTrait {
fn as_component(&self) -> &dyn Component
where
@ -524,9 +605,9 @@ pub trait MailListingTrait: ListingTrait {
};
let account = &mut context.accounts[&account_hash];
match a {
ListingAction::SetSeen => {
ListingAction::Flag(FlagAction::Set(Flag::SEEN)) | ListingAction::SetSeen => {
if let Err(err) = account.set_flags(
env_hashes.clone(),
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::Set(Flag::SEEN)],
) {
@ -535,9 +616,9 @@ pub trait MailListingTrait: ListingTrait {
));
}
}
ListingAction::SetUnseen => {
ListingAction::Flag(FlagAction::Unset(Flag::SEEN)) | ListingAction::SetUnseen => {
if let Err(err) = account.set_flags(
env_hashes.clone(),
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::UnSet(Flag::SEEN)],
) {
@ -546,9 +627,31 @@ pub trait MailListingTrait: ListingTrait {
));
}
}
ListingAction::Tag(Add(ref tag_str)) => {
ListingAction::Flag(FlagAction::Set(flag)) => {
if let Err(err) = account.set_flags(
env_hashes.clone(),
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::Set(*flag)],
) {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
}
ListingAction::Flag(FlagAction::Unset(flag)) => {
if let Err(err) = account.set_flags(
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::UnSet(*flag)],
) {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
}
ListingAction::Tag(TagAction::Add(ref tag_str)) => {
if let Err(err) = account.set_flags(
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::SetTag(tag_str.into())],
) {
@ -557,9 +660,9 @@ pub trait MailListingTrait: ListingTrait {
));
}
}
ListingAction::Tag(Remove(ref tag_str)) => {
ListingAction::Tag(TagAction::Remove(ref tag_str)) => {
if let Err(err) = account.set_flags(
env_hashes.clone(),
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::UnSetTag(tag_str.into())],
) {
@ -676,6 +779,7 @@ pub trait MailListingTrait: ListingTrait {
if path.is_relative() {
path = context.current_dir().join(&path);
}
path = path.expand();
let account = &mut context.accounts[&account_hash];
let format = (*format).unwrap_or_default();
let collection = account.collection.clone();
@ -719,7 +823,7 @@ pub trait MailListingTrait: ListingTrait {
format.append(
&mut file,
bytes.as_slice(),
env.from().get(0),
env.from().first(),
Some(env.date()),
(env.flags(), tags),
MboxMetadata::CClient,
@ -736,7 +840,7 @@ pub trait MailListingTrait: ListingTrait {
format.append(
&mut file,
bytes.as_slice(),
env.from().get(0),
env.from().first(),
Some(env.date()),
(env.flags(), tags),
MboxMetadata::CClient,
@ -775,7 +879,7 @@ pub trait MailListingTrait: ListingTrait {
kind: Some(NotificationType::Error(err.kind)),
},
Ok(Some(Ok(path))) => UIEvent::Notification {
title: Some("Succesfully exported mbox".into()),
title: Some("Successfully exported mbox".into()),
source: None,
body: format!("Wrote to file {}", path.display()).into(),
kind: Some(NotificationType::Info),
@ -972,11 +1076,11 @@ enum MenuEntryCursor {
}
impl std::ops::Sub<MenuEntryCursor> for isize {
type Output = isize;
type Output = Self;
fn sub(self, other: MenuEntryCursor) -> isize {
fn sub(self, other: MenuEntryCursor) -> Self {
if let MenuEntryCursor::Mailbox(v) = other {
v as isize - self
v as Self - self
} else {
self - 1
}
@ -1015,7 +1119,7 @@ pub struct Listing {
prev_ratio: usize,
menu_width: WidgetWidth,
focus: ListingFocus,
view: Box<ThreadView>,
view: Option<Box<ThreadView>>,
}
impl std::fmt::Display for Listing {
@ -1093,8 +1197,9 @@ impl Component for Listing {
} else {
self.component.draw(grid, area, context);
if self.component.unfocused() {
self.view
.draw(grid, self.component.view_area().unwrap_or(area), context);
if let Some(ref mut view) = self.view {
view.draw(grid, self.component.view_area().unwrap_or(area), context);
}
}
}
} else if right_component_width == 0 {
@ -1117,8 +1222,9 @@ impl Component for Listing {
let area = area.skip_cols(mid + 1);
self.component.draw(grid, area, context);
if self.component.unfocused() {
self.view
.draw(grid, self.component.view_area().unwrap_or(area), context);
if let Some(ref mut view) = self.view {
view.draw(grid, self.component.view_area().unwrap_or(area), context);
}
}
}
}
@ -1320,17 +1426,21 @@ impl Component for Listing {
match content.downcast_ref::<ListingMessage>().copied() {
None => {}
Some(ListingMessage::FocusUpdate { new_value }) => {
self.view.process_event(
&mut UIEvent::VisibilityChange(!matches!(new_value, Focus::None)),
context,
);
if let Some(ref mut view) = self.view {
view.process_event(
&mut UIEvent::VisibilityChange(!matches!(new_value, Focus::None)),
context,
);
}
if matches!(new_value, Focus::Entry) {
// Need to clear gap between sidebar and listing component, if any.
self.dirty = true;
}
}
Some(ListingMessage::UpdateView) => {
self.view.set_dirty(true);
if let Some(ref mut view) = self.view {
view.set_dirty(true);
}
}
Some(ListingMessage::OpenEntryUnderCursor {
env_hash,
@ -1339,8 +1449,10 @@ impl Component for Listing {
go_to_first_unread,
}) => {
let (a, m) = self.component.coordinates();
self.view.unrealize(context);
self.view = Box::new(ThreadView::new(
if let Some(view) = self.view.take() {
view.unrealize(context);
}
self.view = Some(Box::new(ThreadView::new(
(a, m, env_hash),
thread_hash,
Some(env_hash),
@ -1351,7 +1463,7 @@ impl Component for Listing {
Some(ThreadViewFocus::MailView)
},
context,
));
)));
}
}
return true;
@ -1378,7 +1490,13 @@ impl Component for Listing {
_ => {}
}
if self.component.unfocused() && self.view.process_event(event, context) {
if self.component.unfocused()
&& self
.view
.as_mut()
.map(|v| v.process_event(event, context))
.unwrap_or(false)
{
return true;
}
@ -1390,7 +1508,12 @@ impl Component for Listing {
}
}
if (self.focus == ListingFocus::Mailbox && self.status.is_none())
&& ((self.component.unfocused() && self.view.process_event(event, context))
&& ((self.component.unfocused()
&& self
.view
.as_mut()
.map(|v| v.process_event(event, context))
.unwrap_or(false))
|| self.component.process_event(event, context))
{
return true;
@ -1540,7 +1663,15 @@ impl Component for Listing {
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_account"]) => {
if self.cursor_pos.account + amount < self.accounts.len() {
self.cursor_pos.account += amount;
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
let _new_val = self.cursor_pos.account;
self.cursor_pos.menu = if let Some(idx) = context.accounts[_new_val]
.default_mailbox()
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else {
return true;
}
@ -1548,7 +1679,15 @@ impl Component for Listing {
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_account"]) => {
if self.cursor_pos.account >= amount {
self.cursor_pos.account -= amount;
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
let _new_val = self.cursor_pos.account;
self.cursor_pos.menu = if let Some(idx) = context.accounts[_new_val]
.default_mailbox()
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else {
return true;
}
@ -1606,12 +1745,13 @@ impl Component for Listing {
return true;
}
Action::Listing(ListingAction::Import(file_path, mailbox_path)) => {
let file_path = file_path.expand();
let account = &mut context.accounts[self.cursor_pos.account];
if let Err(err) = account
.mailbox_by_path(mailbox_path)
.and_then(|mailbox_hash| {
Ok((
std::fs::read(file_path).chain_err_summary(|| {
std::fs::read(&file_path).chain_err_summary(|| {
format!("Could not read {}", file_path.display())
})?,
mailbox_hash,
@ -1635,17 +1775,38 @@ impl Component for Listing {
| Action::Listing(a @ ListingAction::CopyToOtherAccount(_, _))
| Action::Listing(a @ ListingAction::MoveToOtherAccount(_, _))
| Action::Listing(a @ ListingAction::ExportMbox(_, _))
| Action::Listing(a @ ListingAction::Flag(_))
| Action::Listing(a @ ListingAction::Tag(_)) => {
let focused = self.component.get_focused_items(context);
self.component.perform_action(context, focused, a);
let should_be_unselected: bool = matches!(
a,
ListingAction::Delete
| ListingAction::MoveTo(_)
| ListingAction::MoveToOtherAccount(_, _)
);
let mut row_updates: SmallVec<[EnvelopeHash; 8]> = SmallVec::new();
for (k, v) in self.component.selection().iter_mut() {
if *v {
*v = false;
*v = !should_be_unselected;
row_updates.push(*k);
}
}
self.component.row_updates().extend(row_updates);
return true;
}
Action::Listing(ListingAction::ClearSelection) => {
// Clear selection.
let row_updates: SmallVec<[EnvelopeHash; 8]> =
self.component.get_focused_items(context);
for h in &row_updates {
if let Some(val) = self.component.selection().get_mut(h) {
*val = false;
}
}
self.component.row_updates().extend(row_updates);
self.component.set_dirty(true);
return true;
}
_ => {}
},
@ -1844,6 +2005,7 @@ impl Component for Listing {
&& self.component.modifier_command().is_some() =>
{
self.component.set_modifier_command(Some(Modifier::Union));
return true;
}
UIEvent::Input(ref key)
if !self.component.unfocused()
@ -1852,6 +2014,7 @@ impl Component for Listing {
{
self.component
.set_modifier_command(Some(Modifier::Difference));
return true;
}
UIEvent::Input(ref key)
if !self.component.unfocused()
@ -1862,12 +2025,14 @@ impl Component for Listing {
{
self.component
.set_modifier_command(Some(Modifier::Intersection));
return true;
}
UIEvent::Input(ref key)
if self.component.unfocused()
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["next_entry"]) =>
{
self.component.next_entry(context);
return true;
}
UIEvent::Input(ref key)
if self.component.unfocused()
@ -1876,6 +2041,42 @@ impl Component for Listing {
) =>
{
self.component.prev_entry(context);
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
if !self.component.unfocused() =>
{
// Clear selection.
let row_updates: SmallVec<[EnvelopeHash; 8]> =
self.component.get_focused_items(context);
for h in &row_updates {
if let Some(val) = self.component.selection().get_mut(h) {
*val = false;
}
}
self.component.row_updates().extend(row_updates);
self.component.set_dirty(true);
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
if !self.cmd_buf.is_empty() =>
{
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
}
UIEvent::Input(Key::Char(c)) if c.is_ascii_digit() => {
self.cmd_buf.push(*c);
self.component.set_modifier_active(true);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
self.cmd_buf.clone(),
)));
return true;
}
_ => {}
}
@ -1900,7 +2101,7 @@ impl Component for Listing {
&& self.menu_cursor_pos.menu == MenuEntryCursor::Status =>
{
self.cursor_pos = self.menu_cursor_pos;
self.open_status(self.menu_cursor_pos.account, context);
self.change_account(context);
self.set_dirty(true);
self.focus = ListingFocus::Mailbox;
self.ratio = self.prev_ratio;
@ -1987,9 +2188,17 @@ impl Component for Listing {
} => {
if *account > 0 {
*account -= 1;
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(
self.accounts[*account].entries.len().saturating_sub(1),
);
self.menu_cursor_pos.menu =
if self.accounts[*account].entries.is_empty() {
MenuEntryCursor::Status
} else {
MenuEntryCursor::Mailbox(
self.accounts[*account]
.entries
.len()
.saturating_sub(1),
)
};
} else {
return true;
}
@ -2022,7 +2231,12 @@ impl Component for Listing {
} if !self.accounts[*account].entries.is_empty()
&& *menu == MenuEntryCursor::Status =>
{
*menu = MenuEntryCursor::Mailbox(0);
if let Some(idx) = context.accounts[*account]
.default_mailbox()
.and_then(|h| self.accounts[*account].entry_by_hash(h))
{
*menu = MenuEntryCursor::Mailbox(idx);
}
}
// If current account has no mailboxes, go to next account
CursorPos {
@ -2161,15 +2375,32 @@ impl Component for Listing {
{
if self.menu_cursor_pos.account + amount >= self.accounts.len() {
// Go to last mailbox.
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(
self.accounts[self.menu_cursor_pos.account]
.entries
.len()
.saturating_sub(1),
);
self.menu_cursor_pos.menu = if self.accounts
[self.menu_cursor_pos.account]
.entries
.is_empty()
{
MenuEntryCursor::Status
} else {
MenuEntryCursor::Mailbox(
self.accounts[self.menu_cursor_pos.account]
.entries
.len()
.saturating_sub(1),
)
};
} else if self.menu_cursor_pos.account + amount < self.accounts.len() {
self.menu_cursor_pos.account += amount;
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(0);
let _new_val = self.menu_cursor_pos.account;
self.menu_cursor_pos.menu = if let Some(idx) = context.accounts
[_new_val]
.default_mailbox()
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else {
return true;
}
@ -2179,7 +2410,16 @@ impl Component for Listing {
{
if self.menu_cursor_pos.account >= amount {
self.menu_cursor_pos.account -= amount;
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(0);
let _new_val = self.menu_cursor_pos.account;
self.menu_cursor_pos.menu = if let Some(idx) = context.accounts
[_new_val]
.default_mailbox()
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else {
return true;
}
@ -2205,12 +2445,24 @@ impl Component for Listing {
menu: MenuEntryCursor::Mailbox(0)
}
) {
// Can't go anywhere upwards, we're on top already.
return true;
}
if self.menu_cursor_pos.menu == MenuEntryCursor::Mailbox(0) {
self.menu_cursor_pos.account = 0;
} else {
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(0);
match (
self.menu_cursor_pos.menu,
context.accounts[self.menu_cursor_pos.account]
.default_mailbox()
.and_then(|h| {
self.accounts[self.menu_cursor_pos.account].entry_by_hash(h)
}),
) {
(MenuEntryCursor::Mailbox(0), _) => {
self.menu_cursor_pos.account = 0;
}
(MenuEntryCursor::Mailbox(_), Some(v)) => {
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(v);
}
_ => return true,
}
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
@ -2248,11 +2500,20 @@ impl Component for Listing {
self.accounts[*account].entries.len().saturating_sub(1)
) {
*account = self.accounts.len().saturating_sub(1);
*menu = MenuEntryCursor::Mailbox(0);
} else {
*menu = if let Some(idx) = context.accounts[*account]
.default_mailbox()
.and_then(|h| self.accounts[*account].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else if !self.accounts[*account].entries.is_empty() {
*menu = MenuEntryCursor::Mailbox(
self.accounts[*account].entries.len().saturating_sub(1),
);
} else {
*menu = MenuEntryCursor::Status;
}
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
@ -2339,6 +2600,25 @@ impl Component for Listing {
)));
return true;
}
UIEvent::Input(ref key)
if context
.settings
.shortcuts
.listing
.commands
.iter()
.any(|cmd| {
if cmd.shortcut == *key {
for cmd in &cmd.command {
context.replies.push_back(UIEvent::Command(cmd.to_string()));
}
return true;
}
false
}) =>
{
return true;
}
_ => {}
}
false
@ -2352,7 +2632,7 @@ impl Component for Listing {
.map(Component::is_dirty)
.unwrap_or_else(|| self.component.is_dirty())
|| if self.component.unfocused() {
self.view.is_dirty()
self.view.as_ref().map(|v| v.is_dirty()).unwrap_or(false)
} else {
self.component.is_dirty()
}
@ -2365,7 +2645,9 @@ impl Component for Listing {
} else {
self.component.set_dirty(value);
if self.component.unfocused() {
self.view.set_dirty(value);
if let Some(ref mut view) = self.view {
view.set_dirty(value);
}
}
}
}
@ -2373,7 +2655,9 @@ impl Component for Listing {
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = ShortcutMaps::default();
if self.focus != ListingFocus::Menu && self.component.unfocused() {
map.extend_shortcuts(self.view.shortcuts(context));
if let Some(ref view) = self.view {
map.extend_shortcuts(view.shortcuts(context));
}
}
map.extend_shortcuts(if let Some(s) = self.status.as_ref() {
s.shortcuts(context)
@ -2497,12 +2781,12 @@ impl Listing {
})
.collect();
let first_account_hash = account_entries[0].hash;
let mut ret = Listing {
let mut ret = Self {
component: Offline(OfflineListing::new((
first_account_hash,
MailboxHash::default(),
))),
view: Box::<ThreadView>::default(),
view: None,
accounts: account_entries,
status: None,
dirty: true,
@ -2553,14 +2837,14 @@ impl Listing {
.iter()
.map(|entry| entry.entries.len() + 1)
.sum::<usize>();
let min_width: usize = 2 * area.width();
let min_width: usize = area.width();
let (width, height) = self.menu.grid().size();
let cursor = match self.focus {
ListingFocus::Mailbox => self.cursor_pos,
ListingFocus::Menu => self.menu_cursor_pos,
};
if min_width > width || height < total_height || self.dirty {
let _ = self.menu.resize(min_width * 2, total_height);
let _ = self.menu.resize(min_width, total_height);
let mut y = 0;
for a in 0..self.accounts.len() {
let menu_area = self.menu.area().skip_rows(y);
@ -2635,7 +2919,7 @@ impl Listing {
}
/// Print a single account in the menu area.
fn print_account(&mut self, mut area: Area, aidx: usize, context: &mut Context) -> usize {
fn print_account(&mut self, mut area: Area, aidx: usize, context: &Context) -> usize {
let account_y = self.menu.area().height() - area.height();
#[derive(Clone, Copy, Debug)]
struct Line {
@ -3072,9 +3356,24 @@ impl Listing {
self.status(context),
)));
}
MenuEntryCursor::Status => {
MenuEntryCursor::Status if context.is_online(account_hash).is_ok() => {
self.open_status(self.cursor_pos.account, context);
}
MenuEntryCursor::Status => {
self.component.unrealize(context);
self.component =
Offline(OfflineListing::new((account_hash, MailboxHash::default())));
self.component.realize(self.id().into(), context);
self.component
.process_event(&mut UIEvent::VisibilityChange(true), context);
self.status = None;
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.status(context),
)));
}
}
self.sidebar_divider = *account_settings!(context[account_hash].listing.sidebar_divider);
self.set_dirty(true);

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@ use super::*;
use crate::{components::PageMovement, jobs::JoinHandle};
macro_rules! row_attr {
($field:ident, $color_cache:expr, $unseen:expr, $highlighted:expr, $selected:expr $(,)*) => {{
($field:ident, $color_cache:expr, unseen: $unseen:expr, highlighted: $highlighted:expr, selected: $selected:expr $(,)*) => {{
let color_cache = &$color_cache;
let unseen = $unseen;
let highlighted = $highlighted;
@ -65,7 +65,7 @@ macro_rules! row_attr {
},
}
}};
($color_cache:expr, $unseen:expr, $highlighted:expr, $selected:expr $(,)*) => {{
($color_cache:expr, unseen: $unseen:expr, highlighted: $highlighted:expr, selected: $selected:expr $(,)*) => {{
let color_cache = &$color_cache;
let unseen = $unseen;
let highlighted = $highlighted;
@ -227,23 +227,20 @@ impl MailListingTrait for ConversationsListing {
if !force && old_cursor_pos == self.new_cursor_pos && old_mailbox_hash == self.cursor_pos.1
{
self.kick_parent(self.parent, ListingMessage::UpdateView, context);
} else if self.unfocused() {
if let Some((thread_hash, env_hash)) = self
} else if self.unfocused()
&& self
.get_thread_under_cursor(self.cursor_pos.2)
.and_then(|thread| self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0])))
{
self.kick_parent(
self.parent,
ListingMessage::OpenEntryUnderCursor {
thread_hash,
env_hash,
show_thread: true,
go_to_first_unread: true,
},
context,
);
self.set_focus(Focus::Entry, context);
}
.and_then(|thread| {
self.rows
.thread_to_env
.get(&thread)
.and_then(|e| Some((thread, e.first()?)))
})
.is_some()
{
self.force_draw = true;
self.dirty = true;
self.set_focus(Focus::Entry, context);
}
}
@ -412,38 +409,32 @@ impl ListingTrait for ConversationsListing {
fn next_entry(&mut self, context: &mut Context) {
if self
.get_thread_under_cursor(self.cursor_pos.2 + 1)
.get_thread_under_cursor(self.new_cursor_pos.2 + 1)
.is_some()
{
// [ref:TODO]: makes this less ugly.
self.movement = Some(PageMovement::Down(1));
self.perform_movement(None);
self.force_draw = true;
self.dirty = true;
self.cursor_pos.2 += 1;
self.new_cursor_pos.2 += 1;
self.set_focus(Focus::Entry, context);
self.cursor_pos.2 -= 1;
self.new_cursor_pos.2 -= 1;
}
}
fn prev_entry(&mut self, context: &mut Context) {
if self.cursor_pos.2 == 0 {
if self.new_cursor_pos.2 == 0 {
return;
}
if self
.get_thread_under_cursor(self.cursor_pos.2 - 1)
.get_thread_under_cursor(self.new_cursor_pos.2 - 1)
.is_some()
{
// [ref:TODO]: makes this less ugly.
self.movement = Some(PageMovement::Up(1));
self.perform_movement(None);
self.force_draw = true;
self.dirty = true;
self.cursor_pos.2 -= 1;
self.new_cursor_pos.2 -= 1;
self.set_focus(Focus::Entry, context);
self.cursor_pos.2 += 1;
self.new_cursor_pos.2 += 1;
}
}
@ -474,42 +465,12 @@ impl ListingTrait for ConversationsListing {
return;
}
let rows = area.height() / 3;
if rows == 0 {
return;
}
if let Some(mvm) = self.movement.take() {
match mvm {
PageMovement::Up(amount) => {
self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(amount);
}
PageMovement::PageUp(multiplier) => {
self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(rows * multiplier);
}
PageMovement::Down(amount) => {
if self.new_cursor_pos.2 + amount + 1 < self.length {
self.new_cursor_pos.2 += amount;
} else {
self.new_cursor_pos.2 = self.length.saturating_sub(1);
}
}
PageMovement::PageDown(multiplier) => {
if self.new_cursor_pos.2 + rows * multiplier + 1 < self.length {
self.new_cursor_pos.2 += rows * multiplier;
} else if self.new_cursor_pos.2 + rows * multiplier > self.length {
self.new_cursor_pos.2 = self.length.saturating_sub(1);
} else {
self.new_cursor_pos.2 = (self.length.saturating_sub(1) / rows) * rows;
}
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
PageMovement::Home => {
self.new_cursor_pos.2 = 0;
}
PageMovement::End => {
self.new_cursor_pos.2 = self.length.saturating_sub(1);
}
}
}
self.perform_movement(Some(rows));
let prev_page_no = (self.cursor_pos.2).wrapping_div(rows);
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
@ -551,6 +512,7 @@ impl ListingTrait for ConversationsListing {
context,
);
self.force_draw = false;
context.dirty_areas.push_back(area);
}
@ -643,8 +605,13 @@ impl ListingTrait for ConversationsListing {
}
Focus::Entry => {
if let Some((thread_hash, env_hash)) = self
.get_thread_under_cursor(self.cursor_pos.2)
.and_then(|thread| self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0])))
.get_thread_under_cursor(self.new_cursor_pos.2)
.and_then(|thread| {
self.rows
.thread_to_env
.get(&thread)
.and_then(|e| Some((thread, *e.first()?)))
})
{
self.force_draw = true;
self.dirty = true;
@ -658,6 +625,7 @@ impl ListingTrait for ConversationsListing {
},
context,
);
self.cursor_pos.2 = self.new_cursor_pos.2;
} else {
return;
}
@ -713,7 +681,7 @@ impl ConversationsListing {
}
#[allow(clippy::too_many_arguments)]
pub(super) fn make_entry_string(
fn make_entry_string(
&self,
root_envelope: &Envelope,
context: &Context,
@ -788,13 +756,23 @@ impl ConversationsListing {
} else {
subject
}),
flag: FlagString(format!(
"{}{}",
if thread.has_attachments() { "📎" } else { "" },
if thread.snoozed() { "💤" } else { "" }
)),
from: FromString(Address::display_name_slice(from).to_string()),
flag: FlagString::new(
root_envelope.flags(),
self.rows
.selection
.get(&root_envelope.hash())
.cloned()
.unwrap_or(false),
thread.snoozed(),
thread.unseen() > 0,
thread.has_attachments(),
context,
(self.cursor_pos.0, self.cursor_pos.1),
),
from: FromString(Address::display_name_slice(from)),
tags: TagString(tags_string, colors),
unseen: thread.unseen() > 0,
highlight_self: false,
}
}
@ -886,9 +864,9 @@ impl ConversationsListing {
let row_attr = row_attr!(
self.color_cache,
thread.unseen() > 0,
self.cursor_pos.2 == idx,
self.rows.is_thread_selected(*thread_hash)
unseen: thread.unseen() > 0,
highlighted: self.cursor_pos.2 == idx,
selected: self.rows.is_thread_selected(*thread_hash)
);
// draw flags
let (mut x, _) = grid.write_string(
@ -900,17 +878,17 @@ impl ConversationsListing {
None,
);
if !strings.flag.is_empty() {
for c in grid.row_iter(area, x..(x + 3), 0) {
for c in grid.row_iter(area, x..(x + 1), 0) {
grid[c].set_bg(row_attr.bg);
}
x += 3;
x += 1;
}
let subject_attr = row_attr!(
subject,
self.color_cache,
thread.unseen() > 0,
self.cursor_pos.2 == idx,
self.rows.is_thread_selected(*thread_hash)
unseen: thread.unseen() > 0,
highlighted: self.cursor_pos.2 == idx,
selected: self.rows.is_thread_selected(*thread_hash)
);
// draw subject
let (x_, subject_overflowed) = grid.write_string(
@ -959,9 +937,9 @@ impl ConversationsListing {
let date_attr = row_attr!(
date,
self.color_cache,
thread.unseen() > 0,
self.cursor_pos.2 == idx,
self.rows.is_thread_selected(*thread_hash)
unseen: thread.unseen() > 0,
highlighted: self.cursor_pos.2 == idx,
selected: self.rows.is_thread_selected(*thread_hash)
);
x = 0;
x += grid
@ -981,9 +959,9 @@ impl ConversationsListing {
let from_attr = row_attr!(
from,
self.color_cache,
thread.unseen() > 0,
self.cursor_pos.2 == idx,
self.rows.is_thread_selected(*thread_hash)
unseen: thread.unseen() > 0,
highlighted: self.cursor_pos.2 == idx,
selected: self.rows.is_thread_selected(*thread_hash)
);
// draw from
x += grid
@ -1002,6 +980,43 @@ impl ConversationsListing {
}
}
}
fn perform_movement(&mut self, height: Option<usize>) {
let rows = height.unwrap_or(1);
if let Some(mvm) = self.movement.take() {
match mvm {
PageMovement::Up(amount) => {
self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(amount);
}
PageMovement::PageUp(multiplier) => {
self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(rows * multiplier);
}
PageMovement::Down(amount) => {
if self.new_cursor_pos.2 + amount + 1 < self.length {
self.new_cursor_pos.2 += amount;
} else {
self.new_cursor_pos.2 = self.length.saturating_sub(1);
}
}
PageMovement::PageDown(multiplier) => {
if self.new_cursor_pos.2 + rows * multiplier + 1 < self.length {
self.new_cursor_pos.2 += rows * multiplier;
} else if self.new_cursor_pos.2 + rows * multiplier > self.length {
self.new_cursor_pos.2 = self.length.saturating_sub(1);
} else {
self.new_cursor_pos.2 = (self.length.saturating_sub(1) / rows) * rows;
}
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
PageMovement::Home => {
self.new_cursor_pos.2 = 0;
}
PageMovement::End => {
self.new_cursor_pos.2 = self.length.saturating_sub(1);
}
}
}
}
}
impl Component for ConversationsListing {
@ -1045,7 +1060,9 @@ impl Component for ConversationsListing {
if let Some(mvm) = self.movement.as_ref() {
match mvm {
PageMovement::Up(amount) => {
for c in self.cursor_pos.2.saturating_sub(*amount)..=self.cursor_pos.2 {
for c in self.new_cursor_pos.2.saturating_sub(*amount)
..=self.new_cursor_pos.2
{
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
thread,
@ -1061,8 +1078,8 @@ impl Component for ConversationsListing {
}
}
if modifier == Modifier::Intersection {
for c in (0..self.cursor_pos.2.saturating_sub(*amount))
.chain((self.cursor_pos.2 + 2)..self.length)
for c in (0..self.new_cursor_pos.2.saturating_sub(*amount))
.chain((self.new_cursor_pos.2 + 2)..self.length)
{
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows
@ -1072,8 +1089,8 @@ impl Component for ConversationsListing {
}
}
PageMovement::PageUp(multiplier) => {
for c in self.cursor_pos.2.saturating_sub(rows * multiplier)
..=self.cursor_pos.2
for c in self.new_cursor_pos.2.saturating_sub(rows * multiplier)
..=self.new_cursor_pos.2
{
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
@ -1091,8 +1108,8 @@ impl Component for ConversationsListing {
}
}
PageMovement::Down(amount) => {
for c in self.cursor_pos.2
..std::cmp::min(self.length, self.cursor_pos.2 + amount + 1)
for c in self.new_cursor_pos.2
..std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
{
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
@ -1109,9 +1126,9 @@ impl Component for ConversationsListing {
}
}
if modifier == Modifier::Intersection {
for c in (0..self.cursor_pos.2).chain(
(std::cmp::min(self.length, self.cursor_pos.2 + amount + 1) + 1)
..self.length,
for c in (0..self.new_cursor_pos.2).chain(
(std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
+ 1)..self.length,
) {
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows
@ -1121,9 +1138,9 @@ impl Component for ConversationsListing {
}
}
PageMovement::PageDown(multiplier) => {
for c in self.cursor_pos.2
for c in self.new_cursor_pos.2
..std::cmp::min(
self.cursor_pos.2 + rows * multiplier + 1,
self.new_cursor_pos.2 + rows * multiplier + 1,
self.length,
)
{
@ -1142,9 +1159,9 @@ impl Component for ConversationsListing {
}
}
if modifier == Modifier::Intersection {
for c in (0..self.cursor_pos.2).chain(
for c in (0..self.new_cursor_pos.2).chain(
(std::cmp::min(
self.cursor_pos.2 + rows * multiplier + 1,
self.new_cursor_pos.2 + rows * multiplier + 1,
self.length,
) + 1)..self.length,
) {
@ -1157,7 +1174,7 @@ impl Component for ConversationsListing {
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
PageMovement::Home => {
for c in 0..=self.cursor_pos.2 {
for c in 0..=self.new_cursor_pos.2 {
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
thread,
@ -1173,7 +1190,7 @@ impl Component for ConversationsListing {
}
}
if modifier == Modifier::Intersection {
for c in (self.cursor_pos.2 + 1)..self.length {
for c in (self.new_cursor_pos.2 + 1)..self.length {
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows
.update_selection_with_thread(thread, |e| *e = false);
@ -1182,7 +1199,7 @@ impl Component for ConversationsListing {
}
}
PageMovement::End => {
for c in self.cursor_pos.2..self.length {
for c in self.new_cursor_pos.2..self.length {
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
thread,
@ -1198,7 +1215,7 @@ impl Component for ConversationsListing {
}
}
if modifier == Modifier::Intersection {
for c in 0..self.cursor_pos.2 {
for c in 0..self.new_cursor_pos.2 {
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows
.update_selection_with_thread(thread, |e| *e = false);
@ -1217,7 +1234,7 @@ impl Component for ConversationsListing {
self.update_line(context, row);
let row: usize = self.rows.env_order[&row];
let page_no = (self.cursor_pos.2).wrapping_div(rows);
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
let top_idx = page_no * rows;
// Update row only if it's currently visible
@ -1229,11 +1246,21 @@ impl Component for ConversationsListing {
}
if self.force_draw {
// Draw the entire list
let area = if matches!(self.focus, Focus::Entry) {
area.take_cols(area.width() / 3)
} else {
area
};
self.draw_list(grid, area, context);
self.force_draw = false;
}
} else {
// Draw the entire list
let area = if matches!(self.focus, Focus::Entry) {
area.take_cols(area.width() / 3)
} else {
area
};
self.draw_list(grid, area, context);
}
}
@ -1257,6 +1284,11 @@ impl Component for ConversationsListing {
let shortcuts = self.shortcuts(context);
match (&event, self.focus) {
(UIEvent::VisibilityChange(true), _) => {
self.force_draw = true;
self.set_dirty(true);
return true;
}
(UIEvent::Input(ref k), Focus::Entry)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"]) =>
{
@ -1458,19 +1490,6 @@ impl Component for ConversationsListing {
}
_ => {}
},
UIEvent::Input(Key::Esc)
if !self.unfocused()
&& self
.rows
.selection
.values()
.cloned()
.any(std::convert::identity) =>
{
self.rows.clear_selection();
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char(''))
if !self.unfocused() && !&self.filter_term.is_empty() =>
{
@ -1502,6 +1521,25 @@ impl Component for ConversationsListing {
}
self.set_dirty(true);
}
UIEvent::Input(ref key)
if context
.settings
.shortcuts
.listing
.commands
.iter()
.any(|cmd| {
if cmd.shortcut == *key {
for cmd in &cmd.command {
context.replies.push_back(UIEvent::Command(cmd.to_string()));
}
return true;
}
false
}) =>
{
return true;
}
_ => {}
}
@ -1510,8 +1548,9 @@ impl Component for ConversationsListing {
fn is_dirty(&self) -> bool {
match self.focus {
Focus::None => self.dirty,
Focus::Entry => self.dirty,
Focus::None | Focus::Entry => {
self.dirty || self.force_draw || !self.rows.row_updates.is_empty()
}
Focus::EntryFullscreen => false,
}
}

View File

@ -119,7 +119,7 @@ impl std::fmt::Display for OfflineListing {
impl OfflineListing {
pub fn new(cursor_pos: (AccountHash, MailboxHash)) -> Box<Self> {
Box::new(OfflineListing {
Box::new(Self {
cursor_pos,
_row_updates: SmallVec::new(),
_selection: HashMap::default(),
@ -220,7 +220,13 @@ impl Component for OfflineListing {
if let Some(msg) = msg.clone() {
self.messages.push(msg);
}
self.dirty = true
self.set_dirty(true);
}
UIEvent::ChangeMode(UIMode::Normal)
| UIEvent::Resize
| UIEvent::ConfigReload { old_settings: _ }
| UIEvent::VisibilityChange(_) => {
self.set_dirty(true);
}
_ => {}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -141,13 +141,13 @@ pub fn encrypt_filter(
Default::default(),
ctx.encrypt(sign_keys, encrypt_keys, data)?.await?,
);
a.content_disposition = ContentDisposition::from(r#"attachment; filename="msg.asc""#.as_bytes());
a.content_disposition = ContentDisposition::from(br#"attachment; filename="msg.asc""#);
a
};
let mut a: AttachmentBuilder = AttachmentBuilder::new("Version: 1\n".as_bytes());
let mut a: AttachmentBuilder = AttachmentBuilder::new(b"Version: 1\n");
a.set_content_type_from_bytes("application/pgp-encrypted".as_bytes());
a.set_content_disposition(ContentDisposition::from("attachment".as_bytes()));
a.set_content_type_from_bytes(b"application/pgp-encrypted");
a.set_content_disposition(ContentDisposition::from(b"attachment"));
let parts = vec![a, sig_attachment.into()];
let boundary = ContentType::make_boundary(&parts);
Ok(Attachment::new(

View File

@ -19,6 +19,8 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::borrow::Cow;
use melib::{MailBackendExtensionStatus, SpecialUsageMailbox};
use super::*;
@ -41,7 +43,7 @@ impl std::fmt::Display for AccountStatus {
}
impl AccountStatus {
pub fn new(account_pos: usize, theme_default: ThemeAttribute) -> AccountStatus {
pub fn new(account_pos: usize, theme_default: ThemeAttribute) -> Self {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(theme_default.fg)
@ -54,7 +56,7 @@ impl AccountStatus {
content.grid_mut().set_growable(true);
_ = content.resize(80, 20);
AccountStatus {
Self {
cursor: (0, 0),
account_pos,
content,
@ -171,17 +173,19 @@ impl AccountStatus {
a.backend_capabilities.supports_search,
) {
(SearchBackend::Auto, true) | (SearchBackend::None, true) => {
"backend-side search".to_string()
Cow::Borrowed("backend-side search")
}
(SearchBackend::Auto, false) | (SearchBackend::None, false) => {
"none (search will be slow)".to_string()
Cow::Borrowed("none (search will be slow)")
}
#[cfg(feature = "sqlite3")]
(SearchBackend::Sqlite3, _) => {
if let Ok(path) = crate::sqlite3::db_path() {
format!("sqlite3 database {}", path.display())
} else {
"sqlite3 database".to_string()
match crate::sqlite3::AccountCache::db_path(&a.name) {
Ok(Some(path)) => {
Cow::Owned(format!("sqlite3 database: {}", path.display()))
}
Ok(None) => Cow::Borrowed("sqlite3 database: uninitialized"),
Err(err) => Cow::Owned(format!("sqlite3 error: {err}")),
}
}
},

View File

@ -94,7 +94,7 @@ impl MailView {
initialize_now: bool,
context: &mut Context,
) -> Self {
let mut ret = MailView {
let mut ret = Self {
coordinates,
dirty: true,
contact_selector: None,
@ -260,7 +260,7 @@ impl MailView {
}
let envelope: EnvelopeRef = account.collection.get_env(coordinates.2);
let mut entries = Vec::new();
let mut entries: IndexMap<Card, (Card, String)> = IndexMap::default();
for addr in envelope.from().iter().chain(envelope.to().iter()) {
let mut new_card: Card = Card::new();
new_card
@ -269,12 +269,12 @@ impl MailView {
if let Some(display_name) = addr.get_display_name() {
new_card.set_name(display_name);
}
entries.push((new_card, format!("{}", addr)));
entries.insert(new_card.clone(), (new_card, format!("{}", addr)));
}
drop(envelope);
self.contact_selector = Some(Box::new(Selector::new(
"select contacts to add",
entries,
entries.into_iter().map(|(_, v)| v).collect(),
false,
Some(Box::new(move |id: ComponentId, results: &[Card]| {
Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec())))
@ -356,6 +356,7 @@ impl Component for MailView {
fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool {
if let Some(ref mut s) = self.contact_selector {
// [ref:FIXME]: contact_selector should not forward navigation events and return true
if s.process_event(event, context) {
return true;
}
@ -499,7 +500,7 @@ impl Component for MailView {
move |id: ComponentId, result: &[Option<PendingReplyAction>]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(result.get(0).cloned().flatten()),
Box::new(result.first().cloned().flatten()),
))
},
)),
@ -665,7 +666,7 @@ impl Component for MailView {
/* autosend or open unsubscribe option */
let unsubscribe = actions.unsubscribe.as_ref().unwrap();
for option in unsubscribe.iter() {
/* [ref:TODO]: Ask for confirmation before proceding with an action */
/* [ref:TODO]: Ask for confirmation before proceeding with an action */
match option {
list_management::ListAction::Email(email) => {
if let Ok(mailto) = Mailto::try_from(*email) {
@ -675,7 +676,8 @@ impl Component for MailView {
context.accounts[&coordinates.0]
.settings
.account()
.make_display_name(),
.make_display_name()
.to_string(),
);
/* Manually drop stuff because borrowck doesn't do it
* on its own */
@ -725,12 +727,16 @@ impl Component for MailView {
.spawn()
{
Ok(child) => {
context.children.push(child);
context
.children
.entry(url_launcher.to_string().into())
.or_default()
.push(child);
}
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Couldn't launch {:?}: {}",
"Couldn't launch {}: {}",
url_launcher, err
)),
));
@ -765,11 +771,15 @@ impl Component for MailView {
.stdout(Stdio::piped())
.spawn()
{
Ok(child) => context.children.push(child),
Ok(child) => context
.children
.entry(url_launcher.to_string().into())
.or_default()
.push(child),
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Couldn't launch {:?}: {}",
"Couldn't launch {}: {}",
url_launcher, err
)),
));
@ -789,6 +799,25 @@ impl Component for MailView {
.push_back(UIEvent::Action(Tab(New(Some(Box::new(new_tab))))));
return true;
}
UIEvent::Input(ref key)
if context
.settings
.shortcuts
.envelope_view
.commands
.iter()
.any(|cmd| {
if cmd.shortcut == *key {
for cmd in &cmd.command {
context.replies.push_back(UIEvent::Command(cmd.to_string()));
}
return true;
}
false
}) =>
{
return true;
}
_ => {}
}
false

View File

@ -87,7 +87,7 @@ impl EnvelopeView {
) -> Self {
let view_settings = view_settings.unwrap_or_default();
let body = Box::new(AttachmentBuilder::new(&mail.bytes).build());
let mut ret = EnvelopeView {
let mut ret = Self {
pager: pager.unwrap_or_default(),
subview,
dirty: true,
@ -152,8 +152,8 @@ impl EnvelopeView {
format!("Failed to start html filter process: {}", filter_invocation,)
.into(),
),
source: None,
body: err.to_string().into(),
source: Some(err.into()),
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
}));
// [ref:FIXME]: add `v` configurable shortcut
@ -221,7 +221,7 @@ impl EnvelopeView {
}
}
for a in parts {
EnvelopeView::attachment_to_display_helper(
Self::attachment_to_display_helper(
a,
main_loop_handler,
active_jobs,
@ -257,7 +257,7 @@ impl EnvelopeView {
}
#[cfg(feature = "gpgme")]
{
if view_settings.auto_verify_signatures {
if view_settings.auto_verify_signatures.is_true() {
let verify_fut = crate::mail::pgp::verify(a.clone());
let handle = main_loop_handler
.job_executor
@ -271,7 +271,7 @@ impl EnvelopeView {
job_id: handle.job_id,
display: {
let mut v = vec![];
EnvelopeView::attachment_to_display_helper(
Self::attachment_to_display_helper(
&parts[0],
main_loop_handler,
active_jobs,
@ -288,7 +288,7 @@ impl EnvelopeView {
inner: Box::new(a.clone()),
display: {
let mut v = vec![];
EnvelopeView::attachment_to_display_helper(
Self::attachment_to_display_helper(
&parts[0],
main_loop_handler,
active_jobs,
@ -317,7 +317,7 @@ impl EnvelopeView {
}
#[cfg(feature = "gpgme")]
{
if view_settings.auto_decrypt {
if view_settings.auto_decrypt.is_true() {
let decrypt_fut = crate::mail::pgp::decrypt(a.raw().to_vec());
let handle = main_loop_handler
.job_executor
@ -342,7 +342,7 @@ impl EnvelopeView {
}
_ => {
for a in parts {
EnvelopeView::attachment_to_display_helper(
Self::attachment_to_display_helper(
a,
main_loop_handler,
active_jobs,
@ -475,7 +475,7 @@ impl EnvelopeView {
} => {
if show_comments {
if description.is_empty() {
acc.push_str("Succesfully decrypted.\n\n");
acc.push_str("Successfully decrypted.\n\n");
} else {
acc.push_str(description);
acc.push_str("\n\n");
@ -1081,13 +1081,13 @@ impl Component for EnvelopeView {
self.view_settings.body_theme = crate::conf::value(context, "mail.view.body");
self.pager = Pager::from_string(
text,
Some(context),
context,
Some(cursor_pos),
None,
self.view_settings.body_theme,
);
if let Some(ref filter) = self.view_settings.pager_filter {
self.pager.filter(filter);
self.pager.filter(filter, context);
}
}
@ -1269,7 +1269,7 @@ impl Component for EnvelopeView {
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
if let Some(attachment) = self.open_attachment(lidx, context) {
if let Ok(()) = crate::mailcap::MailcapEntry::execute(attachment, context) {
if crate::mailcap::MailcapEntry::execute(attachment, context).is_ok() {
self.set_dirty(true);
} else {
context.replies.push_back(UIEvent::StatusEvent(
@ -1294,15 +1294,15 @@ impl Component for EnvelopeView {
}
match save_attachment(&path, &self.mail.bytes) {
Err(err) => {
log::error!("Failed to create file at {}: {err}", path.display());
context.replies.push_back(UIEvent::Notification {
title: Some(
format!("Failed to create file at {}", path.display()).into(),
),
source: None,
body: err.to_string().into(),
source: Some(err),
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
});
log::error!("Failed to create file at {}: {err}", path.display());
return true;
}
Ok(()) => {
@ -1336,15 +1336,15 @@ impl Component for EnvelopeView {
}
match save_attachment(&path, &u.decode(Default::default())) {
Err(err) => {
log::error!("Failed to create file at {}: {err}", path.display());
context.replies.push_back(UIEvent::Notification {
title: Some(
format!("Failed to create file at {}", path.display()).into(),
),
source: None,
body: err.to_string().into(),
source: Some(err),
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
});
log::error!("Failed to create file at {}: {err}", path.display());
}
Ok(()) => {
context.replies.push_back(UIEvent::Notification {
@ -1365,15 +1365,15 @@ impl Component for EnvelopeView {
}
match save_attachment(&path, &self.mail.bytes) {
Err(err) => {
log::error!("Failed to create file at {}: {err}", path.display());
context.replies.push_back(UIEvent::Notification {
title: Some(
format!("Failed to create file at {}", path.display()).into(),
),
source: None,
body: err.to_string().into(),
source: Some(err),
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
});
log::error!("Failed to create file at {}: {err}", path.display());
return true;
}
Ok(()) => {
@ -1411,7 +1411,7 @@ impl Component for EnvelopeView {
ContentType::MessageRfc822 => {
match Mail::new(attachment.body().to_vec(), Some(Flag::SEEN)) {
Ok(wrapper) => {
self.subview = Some(Box::new(EnvelopeView::new(
self.subview = Some(Box::new(Self::new(
wrapper,
None,
None,
@ -1441,7 +1441,7 @@ impl Component for EnvelopeView {
let attachment_type = attachment.mime_type();
let filename = attachment.filename();
if let Ok(command) = query_default_app(&attachment_type) {
match File::create_temp_file(
let res = File::create_temp_file(
&attachment.decode(Default::default()),
filename.as_deref(),
None,
@ -1462,10 +1462,15 @@ impl Component for EnvelopeView {
.stdout(Stdio::piped())
.spawn()?,
))
}) {
});
match res {
Ok((p, child)) => {
context.temp_files.push(p);
context.children.push(child);
context
.children
.entry(command.into())
.or_default()
.push(child);
}
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
@ -1559,13 +1564,17 @@ impl Component for EnvelopeView {
.spawn()
{
Ok(child) => {
context.children.push(child);
context
.children
.entry(url_launcher.to_string().into())
.or_default()
.push(child);
}
Err(err) => {
context.replies.push_back(UIEvent::Notification {
title: Some(format!("Failed to launch {:?}", url_launcher).into()),
source: None,
body: err.to_string().into(),
source: Some(err.into()),
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
});
}

View File

@ -32,7 +32,7 @@ use melib::{
attachment_types::{ContentType, MultipartType, Text},
error::*,
parser::BytesExt,
text_processing::Truncate,
text::Truncate,
utils::xdg::query_default_app,
Attachment, Result,
};
@ -76,7 +76,7 @@ impl std::fmt::Display for ViewFilter {
}
impl ViewFilter {
pub fn new_html(body: &Attachment, context: &mut Context) -> Result<Self> {
pub fn new_html(body: &Attachment, context: &Context) -> Result<Self> {
fn run(cmd: &str, args: &[&str], bytes: &[u8]) -> Result<String> {
let mut html_filter = Command::new(cmd)
.args(args)
@ -306,11 +306,7 @@ impl ViewFilter {
})
}
fn html_process_event(
_self: &mut ViewFilter,
event: &mut UIEvent,
context: &mut Context,
) -> bool {
fn html_process_event(_self: &mut Self, event: &mut UIEvent, context: &mut Context) -> bool {
if matches!(event, UIEvent::Input(key) if *key == context.settings.shortcuts.envelope_view.open_html)
{
let command = context
@ -328,7 +324,7 @@ impl ViewFilter {
command
};
if let Some(command) = command {
match File::create_temp_file(&_self.unfiltered, None, None, Some("html"), true)
let res = File::create_temp_file(&_self.unfiltered, None, None, Some("html"), true)
.and_then(|p| {
let exec_cmd = desktop_exec_to_command(
&command,
@ -344,13 +340,18 @@ impl ViewFilter {
.stdout(Stdio::piped())
.spawn()?,
))
}) {
});
match res {
Ok((p, child)) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::UpdateSubStatus(command.to_string()),
StatusEvent::UpdateSubStatus(command.clone()),
));
context.temp_files.push(p);
context.children.push(child);
context
.children
.entry(command.into())
.or_default()
.push(child);
}
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(

View File

@ -19,12 +19,12 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use melib::{text_processing::Truncate, Envelope, Error, Mail, Result};
use melib::{text::Truncate, Envelope, Error, Mail, Result};
use super::{EnvelopeView, MailView, ViewSettings};
use crate::{jobs::JoinHandle, mailbox_settings, Component, Context, ShortcutMaps, UIEvent};
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PendingReplyAction {
Reply,
ReplyToAuthor,
@ -149,7 +149,7 @@ impl MailViewState {
}),
context.main_loop_handler.clone(),
));
self_.state = MailViewState::Loaded {
self_.state = Self::Loaded {
env,
bytes,
env_view,
@ -192,7 +192,7 @@ impl MailViewState {
impl Default for MailViewState {
fn default() -> Self {
MailViewState::Init {
Self::Init {
pending_action: None,
}
}

View File

@ -32,7 +32,7 @@ use crate::components::PageMovement;
#[derive(Debug)]
struct ThreadEntry {
index: (usize, ThreadNodeHash, usize),
/// (indentation, thread_node index, line number in listing)
/// (indentation, `thread_node` index, line number in listing)
indentation: usize,
msg_hash: EnvelopeHash,
seen: bool,
@ -51,7 +51,7 @@ pub enum ThreadViewFocus {
MailView,
}
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct ThreadView {
new_cursor_pos: usize,
cursor_pos: usize,
@ -73,11 +73,11 @@ pub struct ThreadView {
}
impl ThreadView {
/// @coordinates: (account index, mailbox_hash, root set thread_node index)
/// @expanded_hash: optional position of expanded entry when we render the
/// ThreadView.
/// default: expanded message is the last one.
/// @context: current context
/// @`coordinates`: (account index, `mailbox_hash`, root set `thread_node`
/// index)
/// @`expanded_hash`: optional position of expanded entry when we
/// render the `ThreadView`.
/// default: expanded message is the last one.
pub fn new(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
thread_group: ThreadHash,
@ -86,7 +86,7 @@ impl ThreadView {
focus: Option<ThreadViewFocus>,
context: &mut Context,
) -> Self {
let mut view = ThreadView {
let mut view = Self {
reversed: false,
coordinates,
thread_group,
@ -106,7 +106,11 @@ impl ThreadView {
//],
use_color: context.settings.terminal.use_color(),
horizontal: None,
..Default::default()
expanded_pos: 0,
new_expanded_pos: 0,
visible_entries: vec![],
movement: None,
content: Screen::<Virtual>::default(),
};
view.initiate(expanded_hash, go_to_first_unread, context);
view.new_cursor_pos = view.new_expanded_pos;
@ -147,10 +151,8 @@ impl ThreadView {
{
self.entries[new_cursor].hidden = old_entries[old_cursor].hidden;
old_cursor += 1;
new_cursor += 1;
} else {
new_cursor += 1;
}
new_cursor += 1;
self.recalc_visible_entries();
}
@ -636,162 +638,68 @@ impl ThreadView {
context.dirty_areas.push_back(area);
}
//fn draw_horz(&mut self, grid: &mut CellBuffer, area: Area, context: &mut
// Context) { if self.entries.is_empty() {
// return;
// }
// let upper_left = area.upper_left();
// let bottom_right = area.bottom_right();
// let total_rows = area.height();
fn draw_horz(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.entries.is_empty() {
return;
}
// let pager_ratio = *mailbox_settings!(
// context[self.coordinates.0][&self.coordinates.1]
// .pager
// .pager_ratio
// );
// let mut bottom_entity_rows = (pager_ratio * total_rows) / 100;
let mid = self.content.area().width().min(area.height() / 2);
// if bottom_entity_rows > total_rows {
// bottom_entity_rows = total_rows.saturating_sub(1);
// }
let theme_default = crate::conf::value(context, "theme_default");
// First draw the thread subject on the first row
if self.dirty {
grid.clear_area(area, theme_default);
let account = &context.accounts[&self.coordinates.0];
let threads = account.collection.get_threads(self.coordinates.1);
let thread_root = threads.thread_iter(self.thread_group).next().unwrap().1;
let thread_node = &threads.thread_nodes()[&thread_root];
let i = thread_node.message().unwrap_or_else(|| {
let mut iter_ptr = thread_node.children()[0];
while threads.thread_nodes()[&iter_ptr].message().is_none() {
iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
}
threads.thread_nodes()[&iter_ptr].message().unwrap()
});
let envelope: EnvelopeRef = account.collection.get_env(i);
// let mut mid = get_y(upper_left) + total_rows - bottom_entity_rows;
// if mid >= get_y(bottom_right) {
// mid = get_y(bottom_right) / 2;
// }
// let mid = mid;
grid.write_string(
&envelope.subject(),
theme_default.fg,
theme_default.bg,
theme_default.attrs,
area,
Some(0),
);
context.dirty_areas.push_back(area);
};
// let theme_default = crate::conf::value(context, "theme_default");
// // First draw the thread subject on the first row
// let y = {
// grid.clear_area(area, theme_default);
// let account = &context.accounts[&self.coordinates.0];
// let threads = account.collection.get_threads(self.coordinates.1);
// let thread_root =
// threads.thread_iter(self.thread_group).next().unwrap().1; let
// thread_node = &threads.thread_nodes()[&thread_root]; let i =
// thread_node.message().unwrap_or_else(|| { let mut iter_ptr =
// thread_node.children()[0]; while
// threads.thread_nodes()[&iter_ptr].message().is_none() {
// iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0]; }
// threads.thread_nodes()[&iter_ptr].message().unwrap()
// });
// let envelope: EnvelopeRef = account.collection.get_env(i);
let area = area.skip_rows(2);
let (width, height) = self.content.area().size();
if height == 0 || height == self.cursor_pos || width == 0 {
return;
}
// let (x, y) = grid.write_string(
// &envelope.subject(),
// theme_default.fg,
// theme_default.bg,
// theme_default.attrs,
// area,
// Some(get_x(upper_left)),
// );
// for x in x..=get_x(bottom_right) {
// grid[(x, y)]
// .set_ch(' ')
// .set_fg(theme_default.fg)
// .set_bg(theme_default.bg);
// }
// context.dirty_areas.push_back(area);
// y + 2
// };
// for x in get_x(upper_left)..=get_x(bottom_right) {
// set_and_join_box(grid, (x, y - 1), BoxBoundary::Horizontal);
// grid[(x, y - 1)]
// .set_fg(theme_default.fg)
// .set_bg(theme_default.bg);
// }
// let (width, height) = self.content.area().size();
// if height == 0 || height == self.cursor_pos || width == 0 {
// return;
// }
// grid.clear_area(area.skip_rows(y).take_rows(mid + 1), theme_default);
// match self.focus {
// ThreadViewFocus::None => {
// let area = area.skip_rows(y).take_rows(mid);
// let rows = area.height() / 2;
// if rows == 0 {
// return;
// }
// let page_no = (self.new_cursor_pos).wrapping_div(rows);
// let top_idx = page_no * rows;
// grid.copy_area(
// self.content.grid(),
// area,
// self.content.area().skip_rows(top_idx),
// );
// context.dirty_areas.push_back(area);
// }
// ThreadViewFocus::Thread => {
// let area = {
// let val = area.skip_rows(y);
// if val.height() < 20 {
// area
// } else {
// val
// }
// };
// let rows = area.height() / 2;
// if rows == 0 {
// return;
// }
// let page_no = (self.new_cursor_pos).wrapping_div(rows);
// let top_idx = page_no * rows;
// grid.copy_area(
// self.content.grid(),
// area,
// self.content.area().skip_rows(top_idx),
// );
// context.dirty_areas.push_back(area);
// }
// ThreadViewFocus::MailView => { /* show only envelope */ }
// }
// match self.focus {
// ThreadViewFocus::None => {
// {
// let area = {
// let val = area.skip_rows(mid);
// if val.height() < 20 {
// area
// } else {
// val
// }
// };
// context.dirty_areas.push_back(area);
// for x in get_x(area.upper_left())..=get_x(area.bottom_right())
// { set_and_join_box(grid, (x, mid),
// BoxBoundary::Horizontal); grid[(x, mid)]
// .set_fg(theme_default.fg)
// .set_bg(theme_default.bg);
// }
// }
// {
// let area = area.skip_rows(y).take_rows(mid - 1);
// self.draw_list(grid, area, context);
// }
// let area = area.take_rows(mid);
// self.entries[self.new_expanded_pos]
// .mailview
// .draw(grid, area, context);
// }
// ThreadViewFocus::Thread => {
// self.dirty = true;
// self.draw_list(grid, area.skip_rows(y), context);
// }
// ThreadViewFocus::MailView => {
// self.entries[self.new_expanded_pos]
// .mailview
// .draw(grid, area, context);
// }
// }
//}
match self.focus {
ThreadViewFocus::None => {
self.draw_list(grid, area.take_rows(mid), context);
self.entries[self.new_expanded_pos].mailview.draw(
grid,
area.skip_rows(mid + 1),
context,
);
}
ThreadViewFocus::Thread => {
self.dirty = true;
self.draw_list(grid, area.skip_rows(0), context);
}
ThreadViewFocus::MailView => {
self.entries[self.new_expanded_pos]
.mailview
.draw(grid, area, context);
}
}
context.dirty_areas.push_back(area);
}
fn recalc_visible_entries(&mut self) {
if self.entries.is_empty() {
@ -865,7 +773,7 @@ impl ThreadView {
impl std::fmt::Display for ThreadView {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(e) = self.entries.get(0) {
if let Some(e) = self.entries.first() {
e.mailview.fmt(fmt)
} else {
write!(fmt, "view thread")
@ -891,6 +799,8 @@ impl Component for ThreadView {
self.entries[self.new_expanded_pos]
.mailview
.draw(grid, area, context);
} else if Some(true) == self.horizontal {
self.draw_horz(grid, area, context);
} else {
self.draw_vert(grid, area, context);
}
@ -898,7 +808,10 @@ impl Component for ThreadView {
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if let (UIEvent::Action(Listing(OpenInNewTab)), false) = (&event, self.entries.is_empty()) {
if matches!(
(&event, self.entries.is_empty()),
(UIEvent::Action(Listing(OpenInNewTab)), false)
) {
// Handle this before self.mailview does
let mut new_tab = Self::new(
self.coordinates,
@ -934,7 +847,7 @@ impl Component for ThreadView {
if let Some(ref mut v) = self.horizontal {
*v = !*v;
} else {
self.horizontal = Some(false);
self.horizontal = Some(true);
}
self.set_dirty(true);
true
@ -1102,7 +1015,28 @@ impl Component for ThreadView {
}
false
}
UIEvent::Input(ref key)
if context
.settings
.shortcuts
.thread_view
.commands
.iter()
.any(|cmd| {
if cmd.shortcut == *key {
for cmd in &cmd.command {
context.replies.push_back(UIEvent::Command(cmd.to_string()));
}
return true;
}
false
}) =>
{
true
}
_ => {
// [ref:VERIFY]: In what case do we need to forward a handled UIEvent to all
// entries?
if self
.entries
.iter_mut()

View File

@ -21,7 +21,10 @@
use std::fmt::Write as IoWrite;
use melib::{attachment_types::Charset, error::*, pgp::DecryptionMetadata, Attachment, Result};
use melib::{
attachment_types::Charset, conf::ActionFlag, error::*, pgp::DecryptionMetadata, Attachment,
Result,
};
use crate::{
conf::shortcuts::EnvelopeViewShortcuts,
@ -43,8 +46,8 @@ pub struct ViewSettings {
pub sticky_headers: bool,
pub show_date_in_my_timezone: bool,
pub show_extra_headers: Vec<String>,
pub auto_verify_signatures: bool,
pub auto_decrypt: bool,
pub auto_verify_signatures: ActionFlag,
pub auto_decrypt: ActionFlag,
}
impl Default for ViewSettings {
@ -61,8 +64,8 @@ impl Default for ViewSettings {
sticky_headers: false,
show_date_in_my_timezone: false,
show_extra_headers: vec![],
auto_verify_signatures: true,
auto_decrypt: true,
auto_verify_signatures: ActionFlag::InternalVal(true),
auto_decrypt: ActionFlag::InternalVal(true),
}
}
}
@ -184,7 +187,13 @@ impl ViewOptions {
.collect::<Vec<Link>>();
}
for (lidx, l) in links.iter().enumerate().rev() {
text.insert_str(l.start, &format!("[{}]", lidx));
let mut start = l.start;
while start < text.len() && !text.is_char_boundary(start) {
start += 1;
}
if start < text.len() {
text.insert_str(start, &format!("[{}]", lidx));
}
}
}

View File

@ -21,10 +21,10 @@
use std::{fs::File, io::Write, os::unix::fs::PermissionsExt, path::Path};
use melib::Result;
use melib::{Result, ShellExpandTrait};
pub fn save_attachment(path: &Path, bytes: &[u8]) -> Result<()> {
let mut f = File::create(path)?;
let mut f = File::create(path.expand())?;
let mut permissions = f.metadata()?.permissions();
permissions.set_mode(0o600); // Read/write for owner only.
f.set_permissions(permissions)?;

View File

@ -24,7 +24,7 @@ use indexmap::IndexMap;
use melib::{backends::AccountHash, SortOrder};
use super::*;
use crate::{accounts::MailboxEntry, melib::text_processing::TextProcessing};
use crate::{accounts::MailboxEntry, melib::text::TextProcessing};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MailboxAction {
@ -96,7 +96,7 @@ impl MailboxManager {
let theme_default = crate::conf::value(context, "theme_default");
let mut data_columns = DataColumns::default();
data_columns.theme_config.set_single_theme(theme_default);
MailboxManager {
Self {
cursor_pos: 0,
new_cursor_pos: 0,
account_hash,
@ -117,7 +117,7 @@ impl MailboxManager {
}
}
fn initialize(&mut self, context: &mut Context) {
fn initialize(&mut self, context: &Context) {
let account = &context.accounts[self.account_pos];
self.length = account.mailbox_entries.len();
let mut entries = account.mailbox_entries.clone();

View File

@ -24,13 +24,12 @@
//! Implements [RFC 1524 A User Agent Configuration Mechanism For Multimedia
//! Mail Format Information](https://www.rfc-editor.org/rfc/inline-errata/rfc1524.html)
use std::{
collections::HashMap,
io::{Read, Write},
path::PathBuf,
process::{Command, Stdio},
};
use melib::{email::Attachment, log, text_processing::GlobMatch, Error, Result};
use melib::{email::Attachment, log, text::GlobMatch, Error, Result};
use crate::{
state::Context,
@ -77,7 +76,6 @@ impl MailcapEntry {
}
}
let mut hash_map = HashMap::new();
let mut content = String::new();
std::fs::File::open(mailcap_path.as_path())?.read_to_string(&mut content)?;
@ -111,13 +109,12 @@ impl MailcapEntry {
}
}
result = Some(MailcapEntry {
result = Some(Self {
command: cmd.to_string(),
copiousoutput,
});
break;
}
hash_map.insert(key.to_string(), cmd.to_string());
} else {
let mut parts_iter = l.split(';');
let key = parts_iter.next().unwrap();
@ -134,25 +131,23 @@ impl MailcapEntry {
}
}
result = Some(MailcapEntry {
result = Some(Self {
command: cmd.to_string(),
copiousoutput,
});
break;
}
hash_map.insert(key.to_string(), cmd.to_string());
}
}
match result {
None => Err(Error::new("Not found")),
Some(MailcapEntry {
Some(Self {
command,
copiousoutput,
}) => {
let parts = split_command!(command);
let (cmd, args) = (parts[0], &parts[1..]);
let mut f = None;
let mut needs_stdin = true;
let params = a.parameters();
/* [ref:TODO]: See mailcap(5)
@ -173,7 +168,6 @@ impl MailcapEntry {
true,
)?;
let p = _f.path().display().to_string();
f = Some(_f);
Ok(p)
}
"%t" => Ok(a.content_type().to_string()),

View File

@ -101,6 +101,16 @@ fn run_app(opt: Opt) -> Result<()> {
Some(SubCommand::EditConfig) => {
return subcommands::edit_config();
}
Some(SubCommand::PrintConfigPath) => {
let config_path = crate::conf::get_config_file()?;
println!("{}", config_path.display());
return Ok(());
}
#[cfg(not(feature = "cli-docs"))]
Some(SubCommand::Man(ManOpt {})) => {
return Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"));
}
#[cfg(feature = "cli-docs")]
Some(SubCommand::Man(ManOpt { page, no_raw })) => {
return subcommands::man(page, false).and_then(|s| subcommands::pager(s, no_raw));
}
@ -117,7 +127,7 @@ fn run_app(opt: Opt) -> Result<()> {
return Ok(());
}
Some(SubCommand::View { .. }) => {}
Some(SubCommand::PrintUsedPaths) => {
Some(SubCommand::PrintAppDirectories) => {
println!(
"{}",
xdg::BaseDirectories::with_prefix("meli")
@ -133,13 +143,25 @@ fn run_app(opt: Opt) -> Result<()> {
println!("{}", temp_dir.display());
return Ok(());
}
#[cfg(not(feature = "cli-docs"))]
Some(SubCommand::InstallMan {
destination_path: _,
}) => {
return Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"));
}
#[cfg(feature = "cli-docs")]
Some(SubCommand::InstallMan { destination_path }) => {
match args::manpages::ManPages::install(destination_path) {
match crate::manpages::ManPages::install(destination_path) {
Ok(p) => println!("Installed at {}.", p.display()),
Err(err) => return Err(err),
}
return Ok(());
}
Some(SubCommand::PrintLogPath) => {
let settings = crate::conf::Settings::new()?;
println!("{}", settings._logger.log_dest().display());
return Ok(());
}
None => {}
}

View File

@ -0,0 +1,193 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use std::{
env, fs,
io::Read,
path::{Path, PathBuf},
sync::Arc,
};
use flate2::bufread::GzDecoder;
use melib::{log, ShellExpandTrait};
use crate::{Error, Result};
pub const POSSIBLE_VALUES: &[&str] = &[
"meli",
"meli.1",
"conf",
"meli.conf",
"meli.conf.5",
"themes",
"meli-themes",
"meli-themes.5",
"guide",
"meli.7",
];
pub fn parse_manpage(src: &str) -> Result<ManPages> {
match src {
"" | "meli" | "meli.1" | "main" => Ok(ManPages::Main),
"meli.7" | "guide" => Ok(ManPages::Guide),
"meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
"meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => Ok(ManPages::Themes),
_ => Err(Error::new(format!("Invalid documentation page: {src}",))),
}
}
#[derive(Clone, Copy, Debug)]
/// Choose manpage
pub enum ManPages {
/// meli(1)
Main = 0,
/// meli.conf(5)
Conf = 1,
/// meli-themes(5)
Themes = 2,
/// meli(7)
Guide = 3,
}
impl std::fmt::Display for ManPages {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
fmt,
"{}",
match self {
Self::Main => "meli.1",
Self::Conf => "meli.conf.5",
Self::Themes => "meli-themes.5",
Self::Guide => "meli.7",
}
)
}
}
impl ManPages {
pub fn install(destination: Option<PathBuf>) -> Result<PathBuf> {
fn path_valid(p: &Path, tries: &mut Vec<PathBuf>) -> bool {
tries.push(p.into());
let p = p.expand();
p.exists()
&& p.is_dir()
&& fs::metadata(p)
.ok()
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
}
let mut tries = vec![];
let Some(mut path) = destination
.filter(|p| path_valid(p, &mut tries))
.or_else(|| {
if let Some(paths) = env::var_os("MANPATH") {
if let Some(path) = env::split_paths(&paths).find(|p| path_valid(p, &mut tries))
{
return Some(path);
}
}
None
})
.or_else(|| {
#[allow(deprecated)]
env::home_dir()
.map(|p| p.join(".local").join("share").join("man"))
.filter(|p| path_valid(p, &mut tries))
})
else {
return Err(format!("Could not write to any of these paths: {:?}", tries).into());
};
path = path.expand();
for (p, dir) in [
(Self::Main, "man1"),
(Self::Conf, "man5"),
(Self::Themes, "man5"),
(Self::Guide, "man7"),
] {
let text = crate::subcommands::man(p, true)?;
path.push(dir);
std::fs::create_dir_all(&path).map_err(|err| {
Error::new(format!("Could not create {} directory.", path.display()))
.set_source(Some(Arc::new(err)))
})?;
path.push(&p.to_string());
fs::write(&path, text.as_bytes()).map_err(|err| {
Error::new(format!("Could not write to {}", path.display()))
.set_source(Some(Arc::new(err)))
})?;
log::trace!("Installed {} to {}", p, path.display());
path.pop();
path.pop();
}
Ok(path)
}
pub fn read(self, source: bool) -> Result<String> {
const MANPAGES: [&[u8]; 4] = [
include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.txt.gz")),
];
const MANPAGES_MDOC: [&[u8]; 4] = [
include_bytes!(concat!(env!("OUT_DIR"), "/meli.mdoc.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.mdoc.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.mdoc.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.mdoc.gz")),
];
let mut gz = GzDecoder::new(if source {
MANPAGES_MDOC[self as usize]
} else {
MANPAGES[self as usize]
});
let mut v = String::with_capacity(
str::parse::<usize>(unsafe {
std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap())
})
.unwrap_or_else(|_| panic!("{:?} was not compressed with size comment header", self)),
);
gz.read_to_string(&mut v)?;
Ok(v)
}
/// Helper function to remove backspace markup from mandoc output.
pub fn remove_markup(input: &str) -> Result<String> {
use std::{
io::Write,
process::{Command, Stdio},
};
let mut child = Command::new("col")
.arg("-b")
.arg("-x")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.as_mut().unwrap().write_all(input.as_bytes())?;
Ok(String::from_utf8_lossy(&child.wait_with_output()?.stdout).to_string())
}
}

View File

@ -49,7 +49,7 @@ mod dbus {
impl DbusNotifications {
pub fn new(context: &Context) -> Self {
DbusNotifications {
Self {
rate_limit: RateLimit::new(
1000,
1000,
@ -183,7 +183,7 @@ pub struct NotificationCommand {
impl NotificationCommand {
pub fn new() -> Self {
NotificationCommand::default()
Self::default()
}
}
@ -213,12 +213,13 @@ impl Component for NotificationCommand {
}
}
let mut script = context.settings.notifications.script.as_ref();
if *kind == Some(NotificationType::NewMail)
let script = if *kind == Some(NotificationType::NewMail)
&& context.settings.notifications.new_mail_script.is_some()
{
script = context.settings.notifications.new_mail_script.as_ref();
}
context.settings.notifications.new_mail_script.as_ref()
} else {
context.settings.notifications.script.as_ref()
};
if let Some(ref bin) = script {
match Command::new(bin)
@ -230,7 +231,11 @@ impl Component for NotificationCommand {
.spawn()
{
Ok(child) => {
context.children.push(child);
context
.children
.entry(stringify!(NotificationCommand).into())
.or_default()
.push(child);
}
Err(err) => {
log::error!("Could not run notification script: {err}.");
@ -261,7 +266,11 @@ impl Component for NotificationCommand {
.spawn()
{
Ok(child) => {
context.children.push(child);
context
.children
.entry(stringify!(NotificationCommand).into())
.or_default()
.push(child);
return false;
}
Err(err) => {
@ -378,7 +387,7 @@ impl Component for DisplayMessageBox {
}) = self.messages.get(self.pos)
{
let noto_colors = crate::conf::value(context, "status.notification");
use crate::melib::text_processing::{Reflow, TextProcessing};
use crate::melib::text::{Reflow, TextProcessing};
let box_width = area.width() / 3;
if box_width < 10 {

View File

@ -250,7 +250,7 @@ impl PluginBackend {
plugin,
channel: Arc::new(Mutex::new(channel)),
collection: Default::default(),
is_online: Arc::new(Mutex::new((now, Err(Error::new("Unitialized"))))),
is_online: Arc::new(Mutex::new((now, Err(Error::new("Uninitialized"))))),
}))
}

View File

@ -26,22 +26,23 @@ use std::{
};
use melib::{
backends::{MailBackend, ResultFuture},
backends::MailBackend,
email::{Envelope, EnvelopeHash},
log,
search::{
escape_double_quote,
Query::{self, *},
},
utils::sqlite3::{self as melib_sqlite3, rusqlite::params, DatabaseDescription},
Error, Result, SortField, SortOrder,
smol,
utils::sqlite3::{rusqlite::params, DatabaseDescription},
Error, Result, ResultIntoError, SortField, SortOrder,
};
use smallvec::SmallVec;
use crate::melib::ResultIntoError;
const DB: DatabaseDescription = DatabaseDescription {
name: "index.db",
identifier: None,
application_prefix: "meli",
init_script: Some(
"CREATE TABLE IF NOT EXISTS envelopes (
id INTEGER PRIMARY KEY,
@ -113,10 +114,6 @@ END; ",
version: 1,
};
pub fn db_path() -> Result<PathBuf> {
melib_sqlite3::db_path(DB.name)
}
//#[inline(always)]
//fn fts5_bareword(w: &str) -> Cow<str> {
// if w == "AND" || w == "OR" || w == "NOT" {
@ -140,152 +137,194 @@ pub fn db_path() -> Result<PathBuf> {
// }
//}
//
//
pub async fn insert(
envelope: Envelope,
backend: Arc<RwLock<Box<dyn MailBackend>>>,
acc_name: String,
) -> Result<()> {
let db_path = db_path()?;
if !db_path.exists() {
return Err(Error::new(
"Database hasn't been initialised. Run `reindex` command",
));
}
let conn = melib_sqlite3::open_db(db_path)?;
pub struct AccountCache;
let op = backend
.read()
.unwrap()
.operation(envelope.hash())?
.as_bytes()?;
impl AccountCache {
pub async fn insert(
envelope: Envelope,
backend: Arc<RwLock<Box<dyn MailBackend>>>,
acc_name: String,
) -> Result<()> {
let db_desc = DatabaseDescription {
identifier: Some(acc_name.clone().into()),
..DB.clone()
};
let body = match op.await.map(|bytes| envelope.body_bytes(&bytes)) {
Ok(body) => body.text(),
Err(err) => {
log::error!(
"Failed to open envelope {}: {err}",
envelope.message_id_display(),
);
return Err(err);
if !db_desc.exists().unwrap_or(false) {
return Err(Error::new(format!(
"Database hasn't been initialised. Run `reindex {acc_name}` command"
)));
}
};
if let Err(err) = conn.execute(
"INSERT OR IGNORE INTO accounts (name) VALUES (?1)",
params![acc_name,],
) {
log::error!(
"Failed to insert envelope {}: {err}",
envelope.message_id_display(),
);
return Err(Error::new(err.to_string()));
}
let account_id: i32 = {
let mut stmt = conn
.prepare("SELECT id FROM accounts WHERE name = ?")
.unwrap();
let x = stmt
.query_map(params![acc_name], |row| row.get(0))
let op = backend
.read()
.unwrap()
.next()
.unwrap()
.unwrap();
x
};
if let Err(err) = conn
.execute(
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, cc, bcc, \
subject, message_id, in_reply_to, _references, flags, has_attachments, body_text, \
timestamp)
.operation(envelope.hash())?
.as_bytes()?;
let body = match op.await.map(|bytes| envelope.body_bytes(&bytes)) {
Ok(body) => body.text(),
Err(err) => {
log::error!(
"Failed to open envelope {}: {err}",
envelope.message_id_display(),
);
return Err(err);
}
};
smol::unblock(move || {
let mut conn = db_desc.open_or_create_db()?;
let tx =
conn.transaction_with_behavior(melib::rusqlite::TransactionBehavior::Immediate)?;
if let Err(err) = tx.execute(
"INSERT OR IGNORE INTO accounts (name) VALUES (?1)",
params![acc_name,],
) {
log::error!(
"Failed to insert envelope {}: {err}",
envelope.message_id_display(),
);
return Err(Error::new(format!(
"Failed to insert envelope {}: {err}",
envelope.message_id_display(),
)));
}
let account_id: i32 = {
let mut stmt = tx
.prepare("SELECT id FROM accounts WHERE name = ?")
.unwrap();
let x = stmt
.query_map(params![acc_name], |row| row.get(0))
.unwrap()
.next()
.unwrap()
.unwrap();
x
};
if let Err(err) = tx
.execute(
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, cc, \
bcc, subject, message_id, in_reply_to, _references, flags, has_attachments, \
body_text, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
params![
account_id,
envelope.hash().to_be_bytes().to_vec(),
envelope.date_as_str(),
envelope.field_from_to_string(),
envelope.field_to_to_string(),
envelope.field_cc_to_string(),
envelope.field_bcc_to_string(),
envelope.subject().into_owned().trim_end_matches('\u{0}'),
envelope.message_id_display().to_string(),
envelope
.in_reply_to_display()
.map(|f| f.to_string())
.unwrap_or_default(),
envelope.field_references_to_string(),
i64::from(envelope.flags().bits()),
if envelope.has_attachments() { 1 } else { 0 },
body,
envelope.date().to_be_bytes().to_vec()
],
)
.map_err(|e| Error::new(e.to_string()))
{
log::error!(
"Failed to insert envelope {}: {err}",
envelope.message_id_display(),
);
}
Ok(())
}
pub fn remove(env_hash: EnvelopeHash) -> Result<()> {
let db_path = db_path()?;
if !db_path.exists() {
return Err(Error::new(
"Database hasn't been initialised. Run `reindex` command",
));
params![
account_id,
envelope.hash().to_be_bytes().to_vec(),
envelope.date_as_str(),
envelope.field_from_to_string(),
envelope.field_to_to_string(),
envelope.field_cc_to_string(),
envelope.field_bcc_to_string(),
envelope.subject().into_owned().trim_end_matches('\u{0}'),
envelope.message_id_display().to_string(),
envelope
.in_reply_to_display()
.map(|f| f.to_string())
.unwrap_or_default(),
envelope.field_references_to_string(),
i64::from(envelope.flags().bits()),
i32::from(envelope.has_attachments()),
body,
envelope.date().to_be_bytes().to_vec()
],
)
.map_err(|e| Error::new(e.to_string()))
{
drop(tx);
log::error!(
"Failed to insert envelope {}: {err}",
envelope.message_id_display(),
);
} else {
tx.commit()?;
}
Ok(())
})
.await?;
Ok(())
}
let conn = melib_sqlite3::open_db(db_path)?;
if let Err(err) = conn
.execute(
"DELETE FROM envelopes WHERE hash = ?",
params![env_hash.to_be_bytes().to_vec(),],
)
.map_err(|e| Error::new(e.to_string()))
{
log::error!("Failed to remove envelope {env_hash}: {err}");
return Err(err);
pub async fn remove(acc_name: String, env_hash: EnvelopeHash) -> Result<()> {
let db_desc = DatabaseDescription {
identifier: Some(acc_name.clone().into()),
..DB.clone()
};
let db_path = db_desc.db_path()?;
if !db_path.exists() {
return Err(Error::new(format!(
"Database hasn't been initialised. Run `reindex {acc_name}` command"
)));
}
smol::unblock(move || {
let mut conn = db_desc.open_or_create_db()?;
let tx =
conn.transaction_with_behavior(melib::rusqlite::TransactionBehavior::Immediate)?;
if let Err(err) = tx.execute(
"DELETE FROM envelopes WHERE hash = ?",
params![env_hash.to_be_bytes().to_vec(),],
) {
drop(tx);
log::error!("Failed to remove envelope {env_hash}: {err}");
return Err(Error::new(format!(
"Failed to remove envelope {env_hash}: {err}"
)));
}
tx.commit()?;
Ok(())
})
.await?;
Ok(())
}
Ok(())
}
pub fn index(context: &mut crate::state::Context, account_index: usize) -> ResultFuture<()> {
let account = &context.accounts[account_index];
let (acc_name, acc_mutex, backend_mutex): (String, Arc<RwLock<_>>, Arc<_>) = (
account.name().to_string(),
account.collection.envelopes.clone(),
account.backend.clone(),
);
let conn = melib_sqlite3::open_or_create_db(&DB, Some(acc_name.as_str()))?;
let env_hashes = acc_mutex
.read()
.unwrap()
.keys()
.cloned()
.collect::<Vec<_>>();
pub async fn index(
acc_name: Arc<String>,
collection: melib::Collection,
backend_mutex: Arc<RwLock<Box<dyn MailBackend>>>,
) -> Result<()> {
let acc_mutex = collection.envelopes.clone();
let db_desc = Arc::new(DatabaseDescription {
identifier: Some(acc_name.to_string().into()),
..DB.clone()
});
let env_hashes = acc_mutex
.read()
.unwrap()
.keys()
.cloned()
.collect::<Vec<_>>();
/* Sleep, index and repeat in order not to block the main process */
Ok(Box::pin(async move {
conn.execute(
"INSERT OR REPLACE INTO accounts (name) VALUES (?1)",
params![acc_name.as_str(),],
)
.chain_err_summary(|| "Failed to update index:")?;
/* Sleep, index and repeat in order not to block the main process */
let account_id: i32 = {
let mut stmt = conn
.prepare("SELECT id FROM accounts WHERE name = ?")
.unwrap();
let x = stmt
.query_map(params![acc_name.as_str()], |row| row.get(0))
.unwrap()
.next()
.unwrap()
.unwrap();
x
let acc_name = Arc::clone(&acc_name);
let db_desc = Arc::clone(&db_desc);
smol::unblock(move || {
let mut conn = db_desc.open_or_create_db()?;
let tx = conn
.transaction_with_behavior(melib::rusqlite::TransactionBehavior::Immediate)?;
tx.execute(
"INSERT OR REPLACE INTO accounts (name) VALUES (?1)",
params![acc_name.as_str(),],
)
.chain_err_summary(|| "Failed to update index:")?;
let account_id = {
let mut stmt = tx
.prepare("SELECT id FROM accounts WHERE name = ?")
.unwrap();
let x = stmt
.query_map(params![acc_name.as_str()], |row| row.get(0))
.unwrap()
.next()
.unwrap()
.unwrap();
x
};
tx.commit()?;
Ok::<i32, Error>(account_id)
})
.await?
};
let mut ctr = 0;
log::trace!(
@ -296,90 +335,131 @@ pub fn index(context: &mut crate::state::Context, account_index: usize) -> Resul
);
for chunk in env_hashes.chunks(200) {
ctr += chunk.len();
for env_hash in chunk {
let mut op = backend_mutex.read().unwrap().operation(*env_hash)?;
let mut chunk_bytes = Vec::with_capacity(chunk.len());
for &env_hash in chunk {
let mut op = backend_mutex.read().unwrap().operation(env_hash)?;
let bytes = op
.as_bytes()?
.await
.chain_err_summary(|| format!("Failed to open envelope {}", env_hash))?;
let envelopes_lck = acc_mutex.read().unwrap();
if let Some(e) = envelopes_lck.get(env_hash) {
let body = e.body_bytes(&bytes).text().replace('\0', "");
conn.execute(
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, \
cc, bcc, subject, message_id, in_reply_to, _references, flags, \
has_attachments, body_text, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
params![
account_id,
e.hash().to_be_bytes().to_vec(),
e.date_as_str(),
e.field_from_to_string(),
e.field_to_to_string(),
e.field_cc_to_string(),
e.field_bcc_to_string(),
e.subject().into_owned().trim_end_matches('\u{0}'),
e.message_id_display().to_string(),
e.in_reply_to_display()
.map(|f| f.to_string())
.unwrap_or_default(),
e.field_references_to_string(),
i64::from(e.flags().bits()),
if e.has_attachments() { 1 } else { 0 },
body,
e.date().to_be_bytes().to_vec()
],
)
.chain_err_summary(|| {
format!("Failed to insert envelope {}", e.message_id_display())
})?;
}
chunk_bytes.push((env_hash, bytes));
}
let sleep_dur = std::time::Duration::from_millis(20);
std::thread::sleep(sleep_dur);
{
let acc_mutex = acc_mutex.clone();
let db_desc = Arc::clone(&db_desc);
smol::unblock(move || {
let mut conn = db_desc.open_or_create_db()?;
let tx = conn.transaction_with_behavior(
melib::rusqlite::TransactionBehavior::Immediate,
)?;
let envelopes_lck = acc_mutex.read().unwrap();
for (env_hash, bytes) in chunk_bytes {
if let Some(e) = envelopes_lck.get(&env_hash) {
let body = e.body_bytes(&bytes).text().replace('\0', "");
tx.execute(
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, \
_to, cc, bcc, subject, message_id, in_reply_to, _references, \
flags, has_attachments, body_text, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
params![
account_id,
e.hash().to_be_bytes().to_vec(),
e.date_as_str(),
e.field_from_to_string(),
e.field_to_to_string(),
e.field_cc_to_string(),
e.field_bcc_to_string(),
e.subject().into_owned().trim_end_matches('\u{0}'),
e.message_id_display().to_string(),
e.in_reply_to_display()
.map(|f| f.to_string())
.unwrap_or_default(),
e.field_references_to_string(),
i64::from(e.flags().bits()),
i32::from(e.has_attachments()),
body,
e.date().to_be_bytes().to_vec()
],
)
.chain_err_summary(|| {
format!("Failed to insert envelope {}", e.message_id_display())
})?;
}
}
tx.commit()?;
Ok::<(), Error>(())
})
.await?;
}
let sleep_dur = std::time::Duration::from_millis(50);
smol::Timer::after(sleep_dur).await;
}
Ok(())
}))
}
pub fn search(
query: &Query,
(sort_field, sort_order): (SortField, SortOrder),
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
let db_path = db_path()?;
if !db_path.exists() {
return Err(Error::new(
"Database hasn't been initialised. Run `reindex` command",
));
}
let conn = melib_sqlite3::open_db(db_path)?;
pub async fn search(
acc_name: String,
query: Query,
(sort_field, sort_order): (SortField, SortOrder),
) -> Result<SmallVec<[EnvelopeHash; 512]>> {
let db_desc = DatabaseDescription {
identifier: Some(acc_name.clone().into()),
..DB.clone()
};
let sort_field = match sort_field {
SortField::Subject => "subject",
SortField::Date => "timestamp",
};
if !db_desc.exists().unwrap_or(false) {
return Err(Error::new(format!(
"Database hasn't been initialised for account `{}`. Run `reindex` command to \
build an index.",
acc_name
)));
}
let query = query_to_sql(&query);
let sort_order = match sort_order {
SortOrder::Asc => "ASC",
SortOrder::Desc => "DESC",
};
smol::unblock(move || {
let mut conn = db_desc.open_or_create_db()?;
let mut stmt = conn
.prepare(&format!(
"SELECT hash FROM envelopes WHERE {} ORDER BY {} {};",
query_to_sql(query),
sort_field,
sort_order
))
.map_err(|e| Error::new(e.to_string()))?;
let sort_field = match sort_field {
SortField::Subject => "subject",
SortField::Date => "timestamp",
};
let results = stmt
.query_map([], |row| row.get::<_, EnvelopeHash>(0))
.map_err(Error::from)?
.map(|item| item.map_err(Error::from))
.collect::<Result<SmallVec<[EnvelopeHash; 512]>>>();
Ok(Box::pin(async { results }))
let sort_order = match sort_order {
SortOrder::Asc => "ASC",
SortOrder::Desc => "DESC",
};
let tx = conn.transaction()?;
let mut stmt = tx
.prepare(&format!(
"SELECT hash FROM envelopes WHERE {} ORDER BY {} {};",
query, sort_field, sort_order
))
.map_err(|e| Error::new(e.to_string()))?;
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
// for the temporary to live long enough
let x = stmt
.query_map([], |row| row.get::<_, EnvelopeHash>(0))
.map_err(Error::from)?
.map(|item| item.map_err(Error::from))
.collect::<Result<SmallVec<[EnvelopeHash; 512]>>>();
x
})
.await
}
pub fn db_path(acc_name: &str) -> Result<Option<PathBuf>> {
let db_desc = DatabaseDescription {
identifier: Some(acc_name.to_string().into()),
..DB.clone()
};
let db_path = db_desc.db_path()?;
if !db_path.exists() {
return Ok(None);
}
Ok(Some(db_path))
}
}
/// Translates a `Query` to an Sqlite3 expression in a `String`.

View File

@ -35,9 +35,10 @@
//! [`ThreadEvent`].
use std::{
borrow::Cow,
collections::BTreeSet,
env,
os::unix::io::RawFd,
os::fd::{AsRawFd, FromRawFd, OwnedFd},
path::{Path, PathBuf},
sync::Arc,
thread,
@ -59,7 +60,7 @@ use crate::{
};
struct InputHandler {
pipe: (RawFd, RawFd),
pipe: (OwnedFd, OwnedFd),
rx: Receiver<InputCommand>,
tx: Sender<InputCommand>,
state_tx: Sender<ThreadEvent>,
@ -76,17 +77,19 @@ impl InputHandler {
* thread will receive it and die. */
//let _ = self.rx.try_iter().count();
let rx = self.rx.clone();
let pipe = self.pipe.0;
let pipe = nix::unistd::dup(self.pipe.0.as_raw_fd())
.expect("Fatal: Could not dup() input pipe file descriptor");
let tx = self.state_tx.clone();
thread::Builder::new()
.name("input-thread".to_string())
.spawn(move || {
let pipe = unsafe { OwnedFd::from_raw_fd(pipe) };
get_events(
|i| {
tx.send(ThreadEvent::Input(i)).unwrap();
},
&rx,
pipe,
&pipe,
working,
)
})
@ -95,7 +98,7 @@ impl InputHandler {
}
fn kill(&self) {
let _ = nix::unistd::write(self.pipe.1, &[1]);
let _ = nix::unistd::write(self.pipe.1.as_raw_fd(), &[1]);
self.tx.send(InputCommand::Kill).unwrap();
}
@ -141,8 +144,8 @@ pub struct Context {
receiver: Receiver<ThreadEvent>,
input_thread: InputHandler,
current_dir: PathBuf,
pub children: Vec<std::process::Child>,
/// Children processes
pub children: IndexMap<Cow<'static, str>, Vec<std::process::Child>>,
pub temp_files: Vec<File>,
}
@ -160,7 +163,7 @@ impl Context {
}
pub fn is_online_idx(&mut self, account_pos: usize) -> Result<()> {
let Context {
let Self {
ref mut accounts,
ref mut replies,
..
@ -203,9 +206,7 @@ impl Context {
crossbeam::channel::bounded(32 * ::std::mem::size_of::<ThreadEvent>());
let job_executor = Arc::new(JobExecutor::new(sender.clone()));
let input_thread = unbounded();
let input_thread_pipe = nix::unistd::pipe()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)
.unwrap();
let input_thread_pipe = crate::types::pipe().unwrap();
let backends = Backends::new();
let settings = Box::new(Settings::new().unwrap());
let accounts = vec![{
@ -241,7 +242,7 @@ impl Context {
let accounts = accounts.into_iter().map(|acc| (acc.hash(), acc)).collect();
let working = Arc::new(());
let control = Arc::downgrade(&working);
Context {
Self {
accounts,
settings,
dirty_areas: VecDeque::with_capacity(0),
@ -250,7 +251,7 @@ impl Context {
unrealized: IndexSet::default(),
temp_files: Vec::new(),
current_dir: std::env::current_dir().unwrap(),
children: vec![],
children: IndexMap::default(),
input_thread: InputHandler {
pipe: input_thread_pipe,
@ -293,18 +294,38 @@ impl Drop for State {
// When done, restore the defaults to avoid messing with the terminal.
self.screen.switch_to_main_screen();
use nix::sys::wait::{waitpid, WaitPidFlag};
for child in self.context.children.iter_mut() {
if let Err(err) = waitpid(
nix::unistd::Pid::from_raw(child.id() as i32),
Some(WaitPidFlag::WNOHANG),
) {
log::warn!("Failed to wait on subprocess {}: {}", child.id(), err);
}
for (id, pid, err) in self
.context
.children
.iter_mut()
.flat_map(|(i, v)| v.drain(..).map(move |v| (i, v)))
.filter_map(|(id, child)| {
if let Err(err) = waitpid(
nix::unistd::Pid::from_raw(child.id() as i32),
Some(WaitPidFlag::WNOHANG),
) {
Some((id, child.id(), err))
} else {
None
}
})
{
log::trace!("Failed to wait on subprocess {} ({}): {}", id, pid, err);
}
if let Some(ForkType::Embedded(child_pid)) = self.child.take() {
if let Some(ForkType::Embedded { id, command, pid }) = self.child.take() {
/* Try wait, we don't want to block */
if let Err(e) = waitpid(child_pid, Some(WaitPidFlag::WNOHANG)) {
log::warn!("Failed to wait on subprocess {}: {}", child_pid, e);
if let Err(err) = waitpid(pid, Some(WaitPidFlag::WNOHANG)) {
log::trace!(
"Failed to wait on embedded process {} {} ({}): {}",
id,
if let Some(v) = command.as_ref() {
v.as_ref()
} else {
""
},
pid,
err
);
}
}
}
@ -321,8 +342,7 @@ impl State {
* it from reading stdin, see get_events() for details
*/
let input_thread = unbounded();
let input_thread_pipe = nix::unistd::pipe()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)?;
let input_thread_pipe = crate::types::pipe()?;
let backends = Backends::new();
let settings = Box::new(if let Some(settings) = settings {
settings
@ -410,7 +430,7 @@ impl State {
Screen::draw_horizontal_segment_no_color
});
let message_box = DisplayMessageBox::new(&screen);
let mut s = State {
let mut s = Self {
screen,
child: None,
mode: UIMode::Normal,
@ -429,7 +449,7 @@ impl State {
unrealized: IndexSet::default(),
temp_files: Vec::new(),
current_dir: std::env::current_dir()?,
children: vec![],
children: IndexMap::default(),
input_thread: InputHandler {
pipe: input_thread_pipe,
@ -536,6 +556,8 @@ impl State {
}
let mut areas: smallvec::SmallVec<[Area; 8]> =
self.context.dirty_areas.drain(0..).collect();
let can_draw_above_screen: bool = !matches!(self.mode, UIMode::Embedded | UIMode::Fork);
if self.message_box.active {
let now = datetime::now();
if self
@ -555,7 +577,7 @@ impl State {
/* Sort by x_start, ie upper_left corner's x coordinate */
areas.sort_by(|a, b| a.upper_left().0.partial_cmp(&b.upper_left().0).unwrap());
if self.message_box.active {
if self.message_box.active && can_draw_above_screen {
/* Check if any dirty area intersects with the area occupied by
* floating notification box */
let displ = self.message_box.cached_area();
@ -606,7 +628,7 @@ impl State {
}
}
if self.message_box.is_dirty() && self.message_box.active {
if self.message_box.is_dirty() && self.message_box.active && can_draw_above_screen {
if !self.message_box.is_empty() {
if !self.message_box.initialised {
{
@ -639,7 +661,7 @@ impl State {
}
}
self.message_box.set_dirty(false);
} else if self.message_box.is_dirty() {
} else if self.message_box.is_dirty() && can_draw_above_screen {
/* Clear area previously occupied by floating notification box */
if self.message_box.cached_area().generation() == self.screen.area().generation() {
for row in self
@ -654,7 +676,7 @@ impl State {
self.message_box.set_dirty(false);
}
if !self.overlay.is_empty() {
if !self.overlay.is_empty() && can_draw_above_screen {
let area: Area = self.screen.area();
let overlay_area = area.center_inside((
if self.screen.cols() / 3 > 30 {
@ -703,7 +725,7 @@ impl State {
}
pub fn can_quit_cleanly(&mut self) -> bool {
let State {
let Self {
ref mut components,
ref context,
..
@ -800,38 +822,36 @@ impl State {
});
return;
}
match crate::sqlite3::index(&mut self.context, account_index) {
Ok(job) => {
let handle = self
.context
.main_loop_handler
.job_executor
.spawn_blocking("sqlite3::index".into(), job);
self.context.accounts[account_index].active_jobs.insert(
handle.job_id,
crate::accounts::JobRequest::Generic {
name: "Message index rebuild".into(),
handle,
on_finish: None,
log_level: LogLevel::INFO,
},
);
self.context.replies.push_back(UIEvent::Notification {
title: None,
source: None,
body: "Message index rebuild started.".into(),
kind: Some(NotificationType::Info),
});
}
Err(err) => {
self.context.replies.push_back(UIEvent::Notification {
title: Some("Message index rebuild failed".into()),
source: None,
body: err.to_string().into(),
kind: Some(NotificationType::Error(err.kind)),
});
}
}
let account = &self.context.accounts[account_index];
let (acc_name, backend_mutex): (Arc<String>, Arc<_>) = (
Arc::new(account.name().to_string()),
account.backend.clone(),
);
let job = crate::sqlite3::AccountCache::index(
acc_name,
account.collection.clone(),
backend_mutex,
);
let handle = self
.context
.main_loop_handler
.job_executor
.spawn_specialized("sqlite3::index".into(), job);
self.context.accounts[account_index].active_jobs.insert(
handle.job_id,
crate::accounts::JobRequest::Generic {
name: "Message index rebuild".into(),
handle,
on_finish: None,
log_level: LogLevel::INFO,
},
);
self.context.replies.push_back(UIEvent::Notification {
title: None,
source: None,
body: "Message index rebuild started.".into(),
kind: Some(NotificationType::Info),
});
}
#[cfg(not(feature = "sqlite3"))]
AccountAction(_, ReIndex) => {
@ -839,7 +859,7 @@ impl State {
title: None,
source: None,
body: "Message index rebuild failed: meli is not built with sqlite3 support."
.to_string(),
.into(),
kind: Some(NotificationType::Error(ErrorKind::None)),
});
}
@ -894,6 +914,27 @@ impl State {
)))
.unwrap();
}
#[cfg(feature = "cli-docs")]
Tab(Man(manpage)) => match manpage
.read(false)
.map(|text| crate::manpages::ManPages::remove_markup(&text).unwrap_or(text))
{
Ok(m) => self.rcv_event(UIEvent::Action(Tab(New(Some(Box::new(
Pager::from_string(
m,
&self.context,
None,
None,
crate::conf::value(&self.context, "theme_default"),
),
)))))),
Err(err) => self.context.replies.push_back(UIEvent::Notification {
title: None,
body: "Encountered an error while retrieving manual page.".into(),
source: Some(err),
kind: Some(NotificationType::Error(ErrorKind::Bug)),
}),
},
v => {
self.rcv_event(UIEvent::Action(v));
}
@ -928,8 +969,8 @@ impl State {
));
self.overlay.insert(new.id(), new);
} else if let Action::ReloadConfiguration = action {
match Settings::new().and_then(|new_settings| {
} else if matches!(action, Action::ReloadConfiguration) {
let res = Settings::new().and_then(|new_settings| {
let old_accounts = self
.context
.settings
@ -962,7 +1003,8 @@ impl State {
return Err("No changes detected.".into());
}
Ok(Box::new(new_settings))
}) {
});
match res {
Ok(new_settings) => {
let old_settings =
std::mem::replace(&mut self.context.settings, new_settings);
@ -986,7 +1028,7 @@ impl State {
}
Err(err) => {
self.context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!("invalid command: {err}")),
StatusEvent::DisplayMessage(format!("Invalid command `{cmd}`: {err}")),
));
}
}
@ -1005,8 +1047,12 @@ impl State {
self.context.restore_input();
return;
}
UIEvent::Fork(ForkType::Generic(child)) => {
self.context.children.push(child);
UIEvent::Fork(ForkType::Generic {
id,
command: _,
child,
}) => {
self.context.children.entry(id).or_default().push(child);
return;
}
UIEvent::Fork(child) => {
@ -1194,24 +1240,27 @@ impl State {
pub fn try_wait_on_child(&mut self) -> Option<bool> {
let should_return_flag = match self.child {
Some(ForkType::NewDraft(_, ref mut c)) => {
let w = c.try_wait();
Some(ForkType::Generic {
ref id,
ref command,
ref mut child,
}) => {
let w = child.try_wait();
match w {
Ok(Some(_)) => true,
Ok(None) => false,
Err(err) => {
log::error!("Failed to wait on editor process: {err}");
return None;
}
}
}
Some(ForkType::Generic(ref mut c)) => {
let w = c.try_wait();
match w {
Ok(Some(_)) => true,
Ok(None) => false,
Err(err) => {
log::error!("Failed to wait on child process: {err}");
log::error!(
"Failed to wait on child process {} {} ({}): {}",
id,
if let Some(v) = command.as_ref() {
v.as_ref()
} else {
""
},
child.id(),
err
);
return None;
}
}

View File

@ -27,15 +27,13 @@ use std::{
};
use crossbeam::channel::{Receiver, Sender};
#[cfg(feature = "cli-docs")]
use flate2::bufread::GzDecoder;
use melib::Result;
use melib::{Result, ShellExpandTrait};
use crate::{args::*, *};
use crate::*;
pub fn create_config(path: Option<PathBuf>) -> Result<()> {
let config_path = if let Some(path) = path {
path
path.expand()
} else {
conf::get_config_file()?
};
@ -74,33 +72,7 @@ pub fn edit_config() -> Result<()> {
#[cfg(feature = "cli-docs")]
pub fn man(page: manpages::ManPages, source: bool) -> Result<String> {
const MANPAGES: [&[u8]; 4] = [
include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.txt.gz")),
];
const MANPAGES_MDOC: [&[u8]; 4] = [
include_bytes!(concat!(env!("OUT_DIR"), "/meli.mdoc.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.mdoc.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.mdoc.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.mdoc.gz")),
];
let mut gz = GzDecoder::new(if source {
MANPAGES_MDOC[page as usize]
} else {
MANPAGES[page as usize]
});
let mut v = String::with_capacity(
str::parse::<usize>(unsafe {
std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap())
})
.unwrap_or_else(|_| panic!("{:?} was not compressed with size comment header", page)),
);
gz.read_to_string(&mut v)?;
Ok(v)
page.read(source)
}
#[cfg(feature = "cli-docs")]
@ -133,8 +105,8 @@ pub fn pager(v: String, no_raw: Option<Option<bool>>) -> Result<()> {
}
#[cfg(not(feature = "cli-docs"))]
pub fn man(_: ManOpt) -> Result<()> {
Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"))
pub fn man(_: crate::args::ManOpt) -> Result<()> {
Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"))
}
pub fn compiled_with() -> Result<()> {
@ -159,7 +131,7 @@ pub fn compiled_with() -> Result<()> {
pub fn test_config(path: Option<PathBuf>) -> Result<()> {
let config_path = if let Some(path) = path {
path
path.expand()
} else {
crate::conf::get_config_file()?
};
@ -172,6 +144,7 @@ pub fn view(
sender: Sender<ThreadEvent>,
receiver: Receiver<ThreadEvent>,
) -> Result<State> {
let path = path.expand();
if !path.exists() {
return Err(Error::new(format!(
"`{}` is not a valid path",

View File

@ -21,7 +21,7 @@
use std::{collections::BTreeMap, io::Write};
use super::*;
use crate::melib::text_processing::TextProcessing;
use crate::melib::text::TextProcessing;
#[derive(Debug)]
pub struct SVGScreenshotFilter {
@ -37,7 +37,7 @@ impl std::fmt::Display for SVGScreenshotFilter {
impl SVGScreenshotFilter {
pub fn new() -> Self {
SVGScreenshotFilter {
Self {
save_screenshot: false,
id: ComponentId::default(),
}
@ -76,7 +76,7 @@ impl Component for SVGScreenshotFilter {
* rectangle element
* inserted along with the `use` elements
*
* Each row is arbritarily set at 17px high, and each character cell is 8
* Each row is arbitrarily set at 17px high, and each character cell is 8
* pixels wide. Rectangle cells each have one extra pixel (so 18px *
* 9px) in their dimensions in order to cover the spacing between
* cells.
@ -98,7 +98,7 @@ impl Component for SVGScreenshotFilter {
* - Whenever the foreground color changes, emit a text element with the
* accumulated
* text in the specific foreground color.
* - Whenever the backgrund color changes, emit a rectangle element filled
* - Whenever the background color changes, emit a rectangle element filled
* with the
* specific background color.
*/
@ -431,20 +431,22 @@ impl Component for SVGScreenshotFilter {
kind: None,
});
}
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
if let UIEvent::Input(Key::F(6)) = event {
if matches!(
event,
UIEvent::Input(Key::F(6)) | UIEvent::CmdInput(Key::F(6))
) {
self.save_screenshot = true;
true
} else if let UIEvent::CmdInput(Key::F(6)) = event {
self.save_screenshot = true;
true
} else if let UIEvent::EmbeddedInput((Key::F(6), _)) = event {
} else if matches!(event, UIEvent::EmbeddedInput((Key::F(6), _))) {
self.save_screenshot = true;
false
} else {
false
}
}
fn set_dirty(&mut self, _value: bool) {}
fn is_dirty(&self) -> bool {

View File

@ -28,8 +28,6 @@ mod color;
mod screen;
pub use color::*;
#[macro_use]
pub mod position;
#[macro_use]
pub mod cells;
#[macro_use]
pub mod keys;
@ -41,7 +39,31 @@ use std::io::{BufRead, Write};
pub use braille::BraillePixelIter;
pub use screen::{Area, Screen, ScreenGeneration, StateStdout, Tty, Virtual};
pub use self::{cells::*, keys::*, position::*, text_editing::*};
pub use self::{cells::*, keys::*, text_editing::*};
/// A type alias for a `(x, y)` position on screen.
pub type Pos = (usize, usize);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Alignment {
/// Stretch to fill all space if possible, center if no meaningful way to
/// stretch.
Fill,
/// Snap to left or top side, leaving space on right or bottom.
Start,
/// Snap to right or bottom side, leaving space on left or top.
End,
/// Center natural width of widget inside the allocation.
#[default]
Center,
}
#[macro_export]
macro_rules! emoji_text_presentation_selector {
() => {
'\u{FE0E}'
};
}
/*
* CSI events we use

View File

@ -145,7 +145,7 @@ pub struct BraillePixelIter {
impl From<&[u16]> for BraillePixelIter {
fn from(from: &[u16]) -> Self {
BraillePixelIter {
Self {
columns: [
Braille16bitColumn {
bitmaps: (

View File

@ -30,12 +30,12 @@ use std::{
use melib::{
log,
text_processing::{search::KMP, wcwidth},
text::{search::KMP, wcwidth},
};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use smallvec::SmallVec;
use super::{position::*, Area, Color, ScreenGeneration};
use super::{Area, Color, Pos, ScreenGeneration};
use crate::{state::Context, ThemeAttribute};
/// In a scroll region up and down cursor movements shift the region vertically.
@ -204,7 +204,13 @@ impl CellBuffer {
}
let blank = blank.unwrap_or(self.default_cell);
self.buf = vec![blank; newlen];
let oldbuf = std::mem::replace(&mut self.buf, vec![blank; newlen]);
let (oldcols, oldrows) = (self.cols, self.rows);
for y in 0..oldrows.min(newrows) {
let row_length = oldcols.min(newcols);
self.buf[y * newcols..(y * newcols + row_length)]
.copy_from_slice(&oldbuf[y * oldcols..(y * oldcols + row_length)]);
}
self.cols = newcols;
self.rows = newrows;
true
@ -393,6 +399,16 @@ impl CellBuffer {
return BoundsIterator::empty(self.generation());
}
#[inline(always)]
fn get_x(p: Pos) -> usize {
p.0
}
#[inline(always)]
fn get_y(p: Pos) -> usize {
p.1
}
BoundsIterator {
width: area.width(),
height: area.height(),
@ -499,20 +515,6 @@ impl CellBuffer {
/// Change foreground and background colors in an `Area`
pub fn change_colors(&mut self, area: Area, fg_color: Color, bg_color: Color) {
if cfg!(feature = "debug-tracing") {
let bounds = self.size();
let upper_left = area.upper_left();
let bottom_right = area.bottom_right();
let (x, y) = upper_left;
if y > (get_y(bottom_right))
|| x > get_x(bottom_right)
|| y >= get_y(bounds)
|| x >= get_x(bounds)
{
log::debug!("BUG: Invalid area in change_colors:\n area: {:?}", area);
return;
}
}
for row in self.bounds_iter(area) {
for c in row {
self[c].set_fg(fg_color).set_bg(bg_color);
@ -522,20 +524,6 @@ impl CellBuffer {
/// Change [`ThemeAttribute`] in an `Area`
pub fn change_theme(&mut self, area: Area, theme: ThemeAttribute) {
if cfg!(feature = "debug-tracing") {
let bounds = self.size();
let upper_left = area.upper_left();
let bottom_right = area.bottom_right();
let (x, y) = upper_left;
if y > (get_y(bottom_right))
|| x > get_x(bottom_right)
|| y >= get_y(bounds)
|| x >= get_x(bounds)
{
log::debug!("BUG: Invalid area in change_theme:\n area: {:?}", area);
return;
}
}
for row in self.bounds_iter(area) {
for c in row {
self[c]
@ -563,6 +551,16 @@ impl CellBuffer {
return dest.upper_left();
}
#[inline(always)]
fn get_x(p: Pos) -> usize {
p.0
}
#[inline(always)]
fn get_y(p: Pos) -> usize {
p.1
}
let mut ret = dest.bottom_right();
let mut src_x = get_x(src.upper_left());
let mut src_y = get_y(src.upper_left());
@ -643,10 +641,22 @@ impl CellBuffer {
if area.is_empty() {
return (0, 0);
}
#[inline(always)]
fn get_x(p: Pos) -> usize {
p.0
}
#[inline(always)]
fn get_y(p: Pos) -> usize {
p.1
}
let mut bounds = self.size();
let upper_left = area.upper_left();
let bottom_right = area.bottom_right();
let (mut x, mut y) = upper_left;
let mut prev_coords = upper_left;
if y == get_y(bounds) || x == get_x(bounds) {
if self.growable {
if !self.resize(
@ -681,10 +691,17 @@ impl CellBuffer {
}
}
for c in s.chars() {
if c == crate::emoji_text_presentation_selector!() {
let prev_attrs = self[prev_coords].attrs();
self[prev_coords].set_attrs(prev_attrs | Attr::FORCE_TEXT);
continue;
}
if c == '\r' {
continue;
}
if c == '\n' {
prev_coords = (x, y);
y += 1;
if let Some(_x) = line_break {
x = _x + get_x(upper_left);
@ -707,6 +724,7 @@ impl CellBuffer {
}
break;
}
prev_coords = (x, y);
if c == '\t' {
self[(x, y)].set_ch(' ');
x += 1;
@ -993,7 +1011,10 @@ impl Cell {
self.attrs
}
pub fn set_attrs(&mut self, newattrs: Attr) -> &mut Self {
pub fn set_attrs(&mut self, mut newattrs: Attr) -> &mut Self {
if self.attrs.intersects(Attr::FORCE_TEXT) {
newattrs |= Attr::FORCE_TEXT;
}
if !self.keep_attrs {
self.attrs = newattrs;
}
@ -1076,14 +1097,15 @@ bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Attr: u8 {
/// Terminal default.
const DEFAULT = 0b000_0000;
const BOLD = 0b000_0001;
const DIM = 0b000_0010;
const ITALICS = 0b000_0100;
const UNDERLINE = 0b000_1000;
const BLINK = 0b001_0000;
const REVERSE = 0b010_0000;
const HIDDEN = 0b100_0000;
const DEFAULT = 0;
const BOLD = 1;
const DIM = Self::BOLD.bits() << 1;
const ITALICS = Self::DIM.bits() << 1;
const UNDERLINE = Self::ITALICS.bits() << 1;
const BLINK = Self::UNDERLINE.bits() << 1;
const REVERSE = Self::BLINK.bits() << 1;
const HIDDEN = Self::REVERSE.bits() << 1;
const FORCE_TEXT = Self::HIDDEN.bits() << 1;
}
}
@ -1104,6 +1126,7 @@ impl std::fmt::Display for Attr {
Self::BLINK => write!(f, "Blink"),
Self::REVERSE => write!(f, "Reverse"),
Self::HIDDEN => write!(f, "Hidden"),
Self::FORCE_TEXT => write!(f, "ForceTextRepresentation"),
combination => {
let mut ctr = 0;
if combination.intersects(Self::BOLD) {
@ -1151,6 +1174,12 @@ impl std::fmt::Display for Attr {
}
Self::HIDDEN.fmt(f)?;
}
if combination.intersects(Self::FORCE_TEXT) {
if ctr > 0 {
write!(f, "|")?;
}
Self::FORCE_TEXT.fmt(f)?;
}
write!(f, "")
}
}
@ -1193,6 +1222,7 @@ impl Attr {
"Blink" => Ok(Self::BLINK),
"Reverse" => Ok(Self::REVERSE),
"Hidden" => Ok(Self::HIDDEN),
"ForceTextRepresentation" => Ok(Self::FORCE_TEXT),
combination if combination.contains('|') => {
let mut ret = Self::DEFAULT;
for c in combination.trim().split('|') {
@ -1706,31 +1736,30 @@ pub mod boundaries {
/// Returns the inner area of the created box.
pub fn create_box(grid: &mut CellBuffer, area: Area) -> Area {
debug_assert_eq!(grid.generation(), area.generation());
let upper_left = area.upper_left();
let bottom_right = area.bottom_right();
if !grid.ascii_drawing {
for x in get_x(upper_left)..get_x(bottom_right) {
grid[(x, get_y(upper_left))].set_ch(HORZ_BOUNDARY);
grid[(x, get_y(bottom_right))].set_ch(HORZ_BOUNDARY);
for (top, bottom) in grid
.bounds_iter(area.nth_row(0))
.zip(grid.bounds_iter(area.nth_row(area.height().saturating_sub(1))))
{
for c in top.chain(bottom) {
grid[c].set_ch(HORZ_BOUNDARY);
}
}
for y in get_y(upper_left)..get_y(bottom_right) {
grid[(get_x(upper_left), y)].set_ch(VERT_BOUNDARY);
grid[(get_x(bottom_right), y)].set_ch(VERT_BOUNDARY);
for (left, right) in grid
.bounds_iter(area.nth_col(0))
.zip(grid.bounds_iter(area.nth_col(area.width().saturating_sub(1))))
{
for c in left.chain(right) {
grid[c].set_ch(VERT_BOUNDARY);
}
}
set_and_join_box(grid, upper_left, BoxBoundary::Horizontal);
set_and_join_box(
grid,
set_x(upper_left, get_x(bottom_right)),
BoxBoundary::Horizontal,
);
set_and_join_box(
grid,
set_y(upper_left, get_y(bottom_right)),
BoxBoundary::Vertical,
);
set_and_join_box(grid, bottom_right, BoxBoundary::Vertical);
set_and_join_box(grid, area.upper_left(), BoxBoundary::Horizontal);
set_and_join_box(grid, area.upper_right(), BoxBoundary::Horizontal);
set_and_join_box(grid, area.bottom_left(), BoxBoundary::Vertical);
set_and_join_box(grid, area.bottom_right(), BoxBoundary::Vertical);
}
area.skip(1, 1).skip_rows_from_end(1).skip_cols_from_end(1)
@ -1828,7 +1857,7 @@ pub enum WidgetWidth {
mod tests {
use crate::terminal::{Screen, Virtual};
//use melib::text_processing::{Reflow, TextProcessing, _ALICE_CHAPTER_1};
//use melib::text::{Reflow, TextProcessing, _ALICE_CHAPTER_1};
#[test]
fn test_cellbuffer_search() {

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,8 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
#[cfg(target_os = "macos")]
use std::os::fd::OwnedFd;
use std::{
ffi::{CString, OsStr},
os::unix::{
@ -69,16 +71,16 @@ ioctl_write_ptr_bad!(
);
ioctl_none_bad!(
/// Set controling terminal fd for current session.
/// Set controlling terminal fd for current session.
set_controlling_terminal,
TIOCSCTTY
);
/// Create a new pseudoterminal (PTY) with given width, size and execute
/// `command` in it.
pub fn create_pty(width: usize, height: usize, command: String) -> Result<Arc<Mutex<Terminal>>> {
pub fn create_pty(width: usize, height: usize, command: &str) -> Result<Arc<Mutex<Terminal>>> {
#[cfg(not(target_os = "macos"))]
let (frontend_fd, backend_name) = {
let (frontend_fd, backend_name): (nix::pty::PtyMaster, String) = {
// Open a new PTY frontend
let frontend_fd = posix_openpt(OFlag::O_RDWR)?;
@ -103,7 +105,7 @@ pub fn create_pty(width: usize, height: usize, command: String) -> Result<Arc<Mu
(frontend_fd, backend_name)
};
#[cfg(target_os = "macos")]
let (frontend_fd, backend_fd) = {
let (frontend_fd, backend_fd): (OwnedFd, OwnedFd) = {
let winsize = Winsize {
ws_row: <u16>::try_from(height).unwrap(),
ws_col: <u16>::try_from(width).unwrap(),
@ -122,12 +124,12 @@ pub fn create_pty(width: usize, height: usize, command: String) -> Result<Arc<Mu
let backend_fd = open(Path::new(&backend_name), OFlag::O_RDWR, stat::Mode::empty())?;
// assign stdin, stdout, stderr to the pty
dup2(backend_fd, STDIN_FILENO).unwrap();
dup2(backend_fd, STDOUT_FILENO).unwrap();
dup2(backend_fd, STDERR_FILENO).unwrap();
dup2(backend_fd.as_raw_fd(), STDIN_FILENO).unwrap();
dup2(backend_fd.as_raw_fd(), STDOUT_FILENO).unwrap();
dup2(backend_fd.as_raw_fd(), STDERR_FILENO).unwrap();
/* Become session leader */
nix::unistd::setsid().unwrap();
match unsafe { set_controlling_terminal(backend_fd) } {
match unsafe { set_controlling_terminal(backend_fd.as_raw_fd()) } {
Ok(c) if c < 0 => {
log::error!(
"Could not execute `{command}`: ioctl(fd, TIOCSCTTY, NULL) returned {c}",

View File

@ -30,18 +30,17 @@
//! * a struct containing the PID of the child process that talks to this
//! pseudoterminal.
//! * a [`std::fs::File`] handle to the child process's standard input stream.
//! * an [`EmbeddedGrid`] which is a wrapper over
//! [`CellBuffer`](crate::CellBuffer) along with the properties needed to
//! maintain a proper state machine that keeps track of ongoing escape code
//! operations.
//! * an [`EmbeddedGrid`] which is a wrapper over [`CellBuffer`] along with
//! the properties needed to maintain a proper state machine that keeps
//! track of ongoing escape code operations.
//!
//! ## Creation
//!
//! To create a [`Terminal`], see [`create_pty`](super::create_pty).
//! To create a [`Terminal`], see [`create_pty`].
use melib::{
error::{Error, Result},
text_processing::wcwidth,
text::wcwidth,
};
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
@ -257,6 +256,21 @@ impl EmbeddedGrid {
}
}
/*
pub fn to_string_debug(&self) -> String {
let mut out = String::with_capacity(4096);
out.push_str(&format!("screen_buffer: {:?}\n", self.screen_buffer));
let grid = self.buffer();
for y in 0..self.terminal_size().1 {
for x in 0..self.terminal_size().0 {
out.push(grid[(x, y)].ch());
}
out.push('\n');
}
out
}
*/
#[inline]
pub fn set_dirty(&mut self, value: bool) {
self.dirty = value;
@ -314,8 +328,7 @@ impl EmbeddedGrid {
}
pub fn process_byte(&mut self, stdin: &mut std::fs::File, byte: u8) {
let area = self.area();
let EmbeddedGrid {
let Self {
ref mut cursor,
ref mut scroll_region,
ref mut terminal_size,
@ -338,12 +351,12 @@ impl EmbeddedGrid {
initialized: _,
ref mut dirty,
} = self;
let mut grid = normal_screen.grid_mut();
let mut screen = normal_screen;
let is_alternate = match *screen_buffer {
ScreenBuffer::Normal => false,
_ => {
grid = alternate_screen.grid_mut();
screen = alternate_screen;
true
}
};
@ -354,7 +367,10 @@ impl EmbeddedGrid {
if !is_alternate {
cursor.0 = 0;
if cursor.1 >= terminal_size.1 {
if !grid.resize(std::cmp::max(1, grid.cols()), grid.rows() + 2, None) {
if !screen.resize(
std::cmp::max(1, screen.grid().cols()),
screen.grid().rows() + 2,
) {
return;
}
scroll_region.bottom += 1;
@ -402,6 +418,11 @@ impl EmbeddedGrid {
(cursor_x!(), cursor_y!())
};
}
macro_rules! area {
() => {{
screen.area()
}};
}
let mut state = &mut self.state;
match (byte, &mut state) {
@ -422,7 +443,9 @@ impl EmbeddedGrid {
// ESCD Linefeed
//log::trace!("{}", EscCode::from((&(*state), byte)));
if cursor.1 == scroll_region.bottom {
grid.scroll_up(scroll_region, scroll_region.top, 1);
screen
.grid_mut()
.scroll_up(scroll_region, scroll_region.top, 1);
*dirty = true;
} else {
cursor.1 += 1;
@ -436,7 +459,7 @@ impl EmbeddedGrid {
//log::trace!("erasing from {:?} to {:?}", cursor, terminal_size);
for y in cursor.1..terminal_size.1 {
for x in cursor.0..terminal_size.0 {
grid[(x, y)] = Cell::default();
screen.grid_mut()[(x, y)] = Cell::default();
}
}
*dirty = true;
@ -446,7 +469,7 @@ impl EmbeddedGrid {
// ESCK Erase from the cursor to the end of the line
//log::trace!("sending {}", EscCode::from((&(*state), byte)));
for x in cursor.0..terminal_size.0 {
grid[(x, cursor.1)] = Cell::default();
screen.grid_mut()[(x, cursor.1)] = Cell::default();
}
*dirty = true;
*state = State::Normal;
@ -487,7 +510,7 @@ impl EmbeddedGrid {
if cursor.1 + 1 < terminal_size.1 || !is_alternate {
if cursor.1 == scroll_region.bottom && is_alternate {
grid.scroll_up(scroll_region, cursor.1, 1);
screen.grid_mut().scroll_up(scroll_region, cursor.1, 1);
*dirty = true;
} else {
increase_cursor_y!();
@ -569,12 +592,14 @@ impl EmbeddedGrid {
}
}
};
//log::trace!("c = {:?}\tcursor={:?}", c, cursor);
*codepoints = CodepointBuf::None;
if *auto_wrap_mode && *wrap_next {
*wrap_next = false;
if cursor.1 == scroll_region.bottom {
grid.scroll_up(scroll_region, scroll_region.top, 1);
screen
.grid_mut()
.scroll_up(scroll_region, scroll_region.top, 1);
} else {
cursor.1 += 1;
}
@ -584,14 +609,15 @@ impl EmbeddedGrid {
//if c == '↪' {
//log::trace!("↪ cursor is {:?}", cursor_val!());
//}
grid[cursor_val!()].set_ch(c);
grid[cursor_val!()].set_fg(*fg_color);
grid[cursor_val!()].set_bg(*bg_color);
grid[cursor_val!()].set_attrs(*attrs);
screen.grid_mut()[cursor_val!()]
.set_ch(c)
.set_fg(*fg_color)
.set_bg(*bg_color)
.set_attrs(*attrs);
match wcwidth(u32::from(c)) {
Some(0) | None => {
/* Skip drawing zero width characters */
grid[cursor_val!()].set_empty(true);
screen.grid_mut()[cursor_val!()].set_empty(true);
}
Some(1) => {}
Some(n) => {
@ -599,10 +625,11 @@ impl EmbeddedGrid {
* drawn over. Set it as empty to skip drawing it. */
for _ in 1..n {
increase_cursor_x!();
grid[cursor_val!()].set_empty(true);
grid[cursor_val!()].set_fg(*fg_color);
grid[cursor_val!()].set_bg(*bg_color);
grid[cursor_val!()].set_attrs(*attrs);
screen.grid_mut()[cursor_val!()]
.set_empty(true)
.set_fg(*fg_color)
.set_bg(*bg_color)
.set_attrs(*attrs);
}
}
}
@ -621,9 +648,10 @@ impl EmbeddedGrid {
*fg_color = Color::Default;
*bg_color = Color::Default;
*attrs = Attr::DEFAULT;
grid[cursor_val!()].set_fg(Color::Default);
grid[cursor_val!()].set_bg(Color::Default);
grid[cursor_val!()].set_attrs(Attr::DEFAULT);
screen.grid_mut()[cursor_val!()]
.set_fg(Color::Default)
.set_bg(Color::Default)
.set_attrs(Attr::DEFAULT);
*dirty = true;
*state = State::Normal;
}
@ -652,12 +680,13 @@ impl EmbeddedGrid {
}
b"25" => {
*show_cursor = true;
*prev_fg_color = Some(grid[cursor_val!()].fg());
*prev_bg_color = Some(grid[cursor_val!()].bg());
*prev_attrs = Some(grid[cursor_val!()].attrs());
grid[cursor_val!()].set_fg(Color::Black);
grid[cursor_val!()].set_bg(Color::White);
grid[cursor_val!()].set_attrs(Attr::DEFAULT);
*prev_fg_color = Some(screen.grid_mut()[cursor_val!()].fg());
*prev_bg_color = Some(screen.grid_mut()[cursor_val!()].bg());
*prev_attrs = Some(screen.grid_mut()[cursor_val!()].attrs());
screen.grid_mut()[cursor_val!()]
.set_fg(Color::Black)
.set_bg(Color::White)
.set_attrs(Attr::DEFAULT);
*dirty = true;
}
b"1047" | b"1049" => {
@ -685,19 +714,19 @@ impl EmbeddedGrid {
b"25" => {
*show_cursor = false;
if let Some(fg_color) = prev_fg_color.take() {
grid[cursor_val!()].set_fg(fg_color);
screen.grid_mut()[cursor_val!()].set_fg(fg_color);
} else {
grid[cursor_val!()].set_fg(*fg_color);
screen.grid_mut()[cursor_val!()].set_fg(*fg_color);
}
if let Some(bg_color) = prev_bg_color.take() {
grid[cursor_val!()].set_bg(bg_color);
screen.grid_mut()[cursor_val!()].set_bg(bg_color);
} else {
grid[cursor_val!()].set_bg(*bg_color);
screen.grid_mut()[cursor_val!()].set_bg(*bg_color);
}
if let Some(attrs) = prev_attrs.take() {
grid[cursor_val!()].set_attrs(attrs);
screen.grid_mut()[cursor_val!()].set_attrs(attrs);
} else {
grid[cursor_val!()].set_attrs(*attrs);
screen.grid_mut()[cursor_val!()].set_attrs(*attrs);
}
*dirty = true;
}
@ -724,7 +753,8 @@ impl EmbeddedGrid {
/* Erase in Display (ED), VT100. */
/* Erase Below (default). */
grid.clear_area(
let area = area!();
screen.grid_mut().clear_area(
area.skip_rows(std::cmp::min(
cursor.1 + 1 + scroll_region.top,
terminal_size.1.saturating_sub(1),
@ -740,7 +770,7 @@ impl EmbeddedGrid {
/* Erase to right (Default) */
//log::trace!("{}", EscCode::from((&(*state), byte)));
for x in cursor.0..terminal_size.0 {
grid[(x, cursor.1)] = Cell::default();
screen.grid_mut()[(x, cursor.1)] = Cell::default();
}
*dirty = true;
*state = State::Normal;
@ -755,7 +785,7 @@ impl EmbeddedGrid {
1
};
grid.scroll_down(scroll_region, cursor.1, n);
screen.grid_mut().scroll_down(scroll_region, cursor.1, n);
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
@ -771,7 +801,7 @@ impl EmbeddedGrid {
1
};
grid.scroll_up(scroll_region, cursor.1, n);
screen.grid_mut().scroll_up(scroll_region, cursor.1, n);
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
@ -794,7 +824,7 @@ impl EmbeddedGrid {
/* Erase to right (Default) */
//log::trace!("{}", EscCode::from((&(*state), byte)));
for x in cursor.0..terminal_size.0 {
grid[(x, cursor.1)] = Cell::default();
screen.grid_mut()[(x, cursor.1)] = Cell::default();
}
*dirty = true;
*state = State::Normal;
@ -803,7 +833,7 @@ impl EmbeddedGrid {
/* Erase in Line (ED), VT100. */
/* Erase to left (Default) */
for x in 0..=cursor.0 {
grid[(x, cursor.1)] = Cell::default();
screen.grid_mut()[(x, cursor.1)] = Cell::default();
}
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
@ -814,12 +844,13 @@ impl EmbeddedGrid {
/* Erase all */
for y in 0..terminal_size.1 {
for x in 0..terminal_size.0 {
grid[(x, y)] = Cell::default();
screen.grid_mut()[(x, y)] = Cell::default();
}
}
//log::trace!("{}", EscCode::from((&(*state), byte)));
grid.clear_area(
let area = area!();
screen.grid_mut().clear_area(
area.take_cols(terminal_size.0).take_rows(terminal_size.1),
Default::default(),
);
@ -830,7 +861,8 @@ impl EmbeddedGrid {
/* Erase in Display (ED), VT100. */
/* Erase Below (default). */
grid.clear_area(
let area = area!();
screen.grid_mut().clear_area(
area.skip_rows(std::cmp::min(
cursor.1 + 1 + scroll_region.top,
terminal_size.1.saturating_sub(1),
@ -845,7 +877,8 @@ impl EmbeddedGrid {
/* Erase in Display (ED), VT100. */
/* Erase Above */
grid.clear_area(
let area = area!();
screen.grid_mut().clear_area(
area.take_rows(cursor.1.saturating_sub(1) + scroll_region.top),
Default::default(),
);
@ -857,7 +890,8 @@ impl EmbeddedGrid {
/* Erase in Display (ED), VT100. */
/* Erase All */
grid.clear_area(area, Default::default());
let area = area!();
screen.grid_mut().clear_area(area, Default::default());
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
*state = State::Normal;
@ -878,7 +912,7 @@ impl EmbeddedGrid {
break;
}
}
grid[(cur_x, cur_y)] = Cell::default();
screen.grid_mut()[(cur_x, cur_y)] = Cell::default();
cur_x += 1;
ctr += 1;
}
@ -949,11 +983,11 @@ impl EmbeddedGrid {
/* scroll down */
for y in scroll_region.top..scroll_region.bottom {
for x in 0..terminal_size.1 {
grid[(x, y)] = grid[(x, y + 1)];
screen.grid_mut()[(x, y)] = screen.grid()[(x, y + 1)];
}
}
for x in 0..terminal_size.1 {
grid[(x, scroll_region.bottom)] = Cell::default();
screen.grid_mut()[(x, scroll_region.bottom)] = Cell::default();
}
} else if offset + cursor.1 < terminal_size.1 {
cursor.1 += offset;
@ -1060,10 +1094,11 @@ impl EmbeddedGrid {
};
for i in 0..(terminal_size.0 - cursor.0 - offset) {
grid[(cursor.0 + i, cursor.1)] = grid[(cursor.0 + i + offset, cursor.1)];
screen.grid_mut()[(cursor.0 + i, cursor.1)] =
screen.grid()[(cursor.0 + i + offset, cursor.1)];
}
for x in (terminal_size.0 - offset)..terminal_size.0 {
grid[(x, cursor.1)].set_ch(' ');
screen.grid_mut()[(x, cursor.1)].set_ch(' ');
}
//log::trace!(
// "Delete {} Character(s) with cursor at {:?} ",
@ -1203,9 +1238,10 @@ impl EmbeddedGrid {
);
}
}
grid[cursor_val!()].set_fg(*fg_color);
grid[cursor_val!()].set_bg(*bg_color);
grid[cursor_val!()].set_attrs(*attrs);
screen.grid_mut()[cursor_val!()]
.set_fg(*fg_color)
.set_bg(*bg_color)
.set_attrs(*attrs);
*dirty = true;
*state = State::Normal;
}
@ -1315,9 +1351,10 @@ impl EmbeddedGrid {
}
}
}
grid[cursor_val!()].set_fg(*fg_color);
grid[cursor_val!()].set_bg(*bg_color);
grid[cursor_val!()].set_attrs(*attrs);
screen.grid_mut()[cursor_val!()]
.set_fg(*fg_color)
.set_bg(*bg_color)
.set_attrs(*attrs);
*dirty = true;
*state = State::Normal;
}
@ -1420,7 +1457,7 @@ impl EmbeddedGrid {
} else {
Color::Default
};
grid[cursor_val!()].set_fg(*fg_color);
screen.grid_mut()[cursor_val!()].set_fg(*fg_color);
*dirty = true;
*state = State::Normal;
}
@ -1436,7 +1473,7 @@ impl EmbeddedGrid {
} else {
Color::Default
};
grid[cursor_val!()].set_bg(*bg_color);
screen.grid_mut()[cursor_val!()].set_bg(*bg_color);
*dirty = true;
*state = State::Normal;
}
@ -1505,7 +1542,7 @@ impl EmbeddedGrid {
(Ok(r), Ok(g), Ok(b)) => Color::Rgb(r, g, b),
_ => Color::Default,
};
grid[cursor_val!()].set_fg(*fg_color);
screen.grid_mut()[cursor_val!()].set_fg(*fg_color);
*dirty = true;
*state = State::Normal;
}
@ -1529,7 +1566,7 @@ impl EmbeddedGrid {
(Ok(r), Ok(g), Ok(b)) => Color::Rgb(r, g, b),
_ => Color::Default,
};
grid[cursor_val!()].set_bg(*bg_color);
screen.grid_mut()[cursor_val!()].set_bg(*bg_color);
*dirty = true;
*state = State::Normal;
}
@ -1552,7 +1589,7 @@ impl EmbeddedGrid {
(Ok(r), Ok(g), Ok(b)) => Color::Rgb(r, g, b),
_ => Color::Default,
};
grid[cursor_val!()].set_fg(*fg_color);
screen.grid_mut()[cursor_val!()].set_fg(*fg_color);
*dirty = true;
*state = State::Normal;
}
@ -1575,7 +1612,7 @@ impl EmbeddedGrid {
(Ok(r), Ok(g), Ok(b)) => Color::Rgb(r, g, b),
_ => Color::Default,
};
grid[cursor_val!()].set_bg(*bg_color);
screen.grid_mut()[cursor_val!()].set_bg(*bg_color);
*dirty = true;
*state = State::Normal;
}

View File

@ -19,7 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::os::unix::io::{AsRawFd, RawFd};
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
use crossbeam::{channel::Receiver, select};
use nix::poll::{poll, PollFd, PollFlags};
@ -120,11 +120,11 @@ pub enum MouseButton {
Middle,
/// Mouse wheel is going up.
///
/// This event is typically only used with Mouse::Press.
/// This event is typically only used with [`MouseEvent::Press`].
WheelUp,
/// Mouse wheel is going down.
///
/// This event is typically only used with Mouse::Press.
/// This event is typically only used with [`MouseEvent::Press`].
WheelDown,
}
@ -173,31 +173,31 @@ impl std::fmt::Display for Key {
impl<'a> From<&'a String> for Key {
fn from(v: &'a String) -> Self {
Key::Paste(v.to_string())
Self::Paste(v.to_string())
}
}
impl From<TermionKey> for Key {
fn from(k: TermionKey) -> Self {
match k {
TermionKey::Backspace => Key::Backspace,
TermionKey::Left => Key::Left,
TermionKey::Right => Key::Right,
TermionKey::Up => Key::Up,
TermionKey::Down => Key::Down,
TermionKey::Home => Key::Home,
TermionKey::End => Key::End,
TermionKey::PageUp => Key::PageUp,
TermionKey::PageDown => Key::PageDown,
TermionKey::Delete => Key::Delete,
TermionKey::Insert => Key::Insert,
TermionKey::F(u) => Key::F(u),
TermionKey::Char(c) => Key::Char(c),
TermionKey::Alt(c) => Key::Alt(c),
TermionKey::Ctrl(c) => Key::Ctrl(c),
TermionKey::Null => Key::Null,
TermionKey::Esc => Key::Esc,
_ => Key::Char(' '),
TermionKey::Backspace => Self::Backspace,
TermionKey::Left => Self::Left,
TermionKey::Right => Self::Right,
TermionKey::Up => Self::Up,
TermionKey::Down => Self::Down,
TermionKey::Home => Self::Home,
TermionKey::End => Self::End,
TermionKey::PageUp => Self::PageUp,
TermionKey::PageDown => Self::PageDown,
TermionKey::Delete => Self::Delete,
TermionKey::Insert => Self::Insert,
TermionKey::F(u) => Self::F(u),
TermionKey::Char(c) => Self::Char(c),
TermionKey::Alt(c) => Self::Alt(c),
TermionKey::Ctrl(c) => Self::Ctrl(c),
TermionKey::Null => Self::Null,
TermionKey::Esc => Self::Esc,
_ => Self::Char(' '),
}
}
}
@ -215,9 +215,10 @@ enum InputMode {
Paste(Vec<u8>),
}
#[derive(Debug)]
#[derive(Debug, Default)]
/// Main process sends commands to the input thread.
pub enum InputCommand {
#[default]
/// Exit thread
Kill,
}
@ -236,11 +237,13 @@ pub enum InputCommand {
pub fn get_events(
mut closure: impl FnMut((Key, Vec<u8>)),
rx: &Receiver<InputCommand>,
new_command_fd: RawFd,
new_command_fd: &OwnedFd,
working: std::sync::Arc<()>,
) {
let stdin = std::io::stdin();
let stdin_fd = PollFd::new(std::io::stdin().as_raw_fd(), PollFlags::POLLIN);
let stdin2 = std::io::stdin();
let stdin2_fd = stdin2.as_fd();
let stdin_fd = PollFd::new(&stdin2_fd, PollFlags::POLLIN);
let new_command_pollfd = nix::poll::PollFd::new(new_command_fd, nix::poll::PollFlags::POLLIN);
let mut input_mode = InputMode::Normal;
let mut paste_buf = String::with_capacity(256);
@ -296,11 +299,12 @@ pub fn get_events(
let mut error_fd_set = nix::sys::select::FdSet::new();
error_fd_set.insert(new_command_fd);
let timeval: nix::sys::time::TimeSpec = nix::sys::time::TimeSpec::seconds(2);
if nix::sys::select::pselect(None, Some(&mut read_fd_set), None, Some(&mut error_fd_set), Some(&timeval), None).is_err() || error_fd_set.highest() == Some(new_command_fd) || read_fd_set.highest() != Some(new_command_fd) {
let pselect_result = nix::sys::select::pselect(None, Some(&mut read_fd_set), None, Some(&mut error_fd_set), Some(&timeval), None);
if pselect_result.is_err() || error_fd_set.highest().map(|bfd| bfd.as_raw_fd()) == Some(new_command_fd.as_raw_fd()) || read_fd_set.highest().map(|bfd| bfd.as_raw_fd()) != Some(new_command_fd.as_raw_fd()) {
continue 'poll_while;
};
let _ = nix::unistd::read(new_command_fd, buf.as_mut());
match cmd.unwrap() {
let _ = nix::unistd::read(new_command_fd.as_raw_fd(), buf.as_mut());
match cmd.unwrap_or_default() {
InputCommand::Kill => return,
}
}
@ -399,27 +403,27 @@ impl Serialize for Key {
S: Serializer,
{
match self {
Key::Backspace => serializer.serialize_str("Backspace"),
Key::Left => serializer.serialize_str("Left"),
Key::Right => serializer.serialize_str("Right"),
Key::Up => serializer.serialize_str("Up"),
Key::Down => serializer.serialize_str("Down"),
Key::Home => serializer.serialize_str("Home"),
Key::End => serializer.serialize_str("End"),
Key::PageUp => serializer.serialize_str("PageUp"),
Key::PageDown => serializer.serialize_str("PageDown"),
Key::Delete => serializer.serialize_str("Delete"),
Key::Insert => serializer.serialize_str("Insert"),
Key::Esc => serializer.serialize_str("Esc"),
Key::Char('\n') => serializer.serialize_str("Enter"),
Key::Char('\t') => serializer.serialize_str("Tab"),
Key::Char(c) => serializer.serialize_char(*c),
Key::F(n) => serializer.serialize_str(&format!("F{}", n)),
Key::Alt(c) => serializer.serialize_str(&format!("M-{}", c)),
Key::Ctrl(c) => serializer.serialize_str(&format!("C-{}", c)),
Key::Null => serializer.serialize_str("Null"),
Key::Mouse(mev) => mev.serialize(serializer),
Key::Paste(s) => serializer.serialize_str(s),
Self::Backspace => serializer.serialize_str("Backspace"),
Self::Left => serializer.serialize_str("Left"),
Self::Right => serializer.serialize_str("Right"),
Self::Up => serializer.serialize_str("Up"),
Self::Down => serializer.serialize_str("Down"),
Self::Home => serializer.serialize_str("Home"),
Self::End => serializer.serialize_str("End"),
Self::PageUp => serializer.serialize_str("PageUp"),
Self::PageDown => serializer.serialize_str("PageDown"),
Self::Delete => serializer.serialize_str("Delete"),
Self::Insert => serializer.serialize_str("Insert"),
Self::Esc => serializer.serialize_str("Esc"),
Self::Char('\n') => serializer.serialize_str("Enter"),
Self::Char('\t') => serializer.serialize_str("Tab"),
Self::Char(c) => serializer.serialize_char(*c),
Self::F(n) => serializer.serialize_str(&format!("F{}", n)),
Self::Alt(c) => serializer.serialize_str(&format!("M-{}", c)),
Self::Ctrl(c) => serializer.serialize_str(&format!("C-{}", c)),
Self::Null => serializer.serialize_str("Null"),
Self::Mouse(mev) => mev.serialize(serializer),
Self::Paste(s) => serializer.serialize_str(s),
}
}
}
@ -439,11 +443,23 @@ fn test_key_serde() {
);
};
($s:literal, err $v:literal) => {
test_key!($s, err $v, "^")
};
($s:literal, err $v:literal, $extra:literal) => {
assert_eq!(
toml::from_str::<V>(std::concat!("k = \"", $s, "\""))
.unwrap_err()
.to_string(),
$v.to_string()
std::concat!(
"TOML parse error at line 1, column 5\n |\n1 | k = \"",
$s,
"\"\n | ",
$extra,
"^^^^\n",
$v,
'\n',
)
.to_string()
);
};
}
@ -468,9 +484,9 @@ fn test_key_serde() {
test_key!("M-a", ok Key::Alt('a') );
test_key!("F1", ok Key::F(1) );
test_key!("F12", ok Key::F(12) );
test_key!("C-V", err "`V` should be a lowercase and alphanumeric character instead. for key `k` at line 1 column 5");
test_key!("M-V", err "`V` should be a lowercase and alphanumeric character instead. for key `k` at line 1 column 5");
test_key!("F13", err "`13` should be a number 1 <= n <= 12 instead. for key `k` at line 1 column 5");
test_key!("Fc", err "`c` should be a number 1 <= n <= 12 instead. for key `k` at line 1 column 5");
test_key!("adsfsf", err "Cannot derive shortcut from `adsfsf`. Please consult the manual for valid key inputs. for key `k` at line 1 column 5");
test_key!("C-V", err "`V` should be a lowercase and alphanumeric character instead.");
test_key!("M-V", err "`V` should be a lowercase and alphanumeric character instead.");
test_key!("F13", err "`13` should be a number 1 <= n <= 12 instead.");
test_key!("Fc", err "`c` should be a number 1 <= n <= 12 instead.", "");
test_key!("adsfsf", err "Cannot derive shortcut from `adsfsf`. Please consult the manual for valid key inputs.", "^^^^");
}

View File

@ -1,67 +0,0 @@
/*
* meli
*
* Copyright 2017-2018 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! Simple type definitions and macro helper for a `(x, y)` position on the
//! terminal and the areas they define. An [`Area`] consists of two points: the
//! upper left and bottom right corners.
/// A `(x, y)` position on screen.
pub type Pos = (usize, usize);
#[inline(always)]
pub fn get_x(p: Pos) -> usize {
p.0
}
#[inline(always)]
pub fn get_y(p: Pos) -> usize {
p.1
}
#[inline(always)]
pub fn set_x(p: Pos, new_x: usize) -> Pos {
(new_x, p.1)
}
#[inline(always)]
pub fn set_y(p: Pos, new_y: usize) -> Pos {
(p.0, new_y)
}
#[inline(always)]
pub fn pos_inc(p: Pos, inc: (usize, usize)) -> Pos {
(p.0 + inc.0, p.1 + inc.1)
}
#[inline(always)]
pub fn pos_dec(p: Pos, dec: (usize, usize)) -> Pos {
(p.0.saturating_sub(dec.0), p.1.saturating_sub(dec.1))
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Alignment {
/// Stretch to fill all space if possible, center if no meaningful way to
/// stretch.
Fill,
/// Snap to left or top side, leaving space on right or bottom.
Start,
/// Snap to right or bottom side, leaving space on left or top.
End,
/// Center natural width of widget inside the allocation.
#[default]
Center,
}

View File

@ -27,9 +27,9 @@ use termion::{clear, cursor, raw::IntoRawMode, screen::AlternateScreen};
use crate::{
terminal::{
cells::CellBuffer, position::*, BracketModeEnd, BracketModeStart, Cell, Color,
DisableMouse, DisableSGRMouse, EnableMouse, EnableSGRMouse,
RestoreWindowTitleIconFromStack, SaveWindowTitleIconToStack,
cells::CellBuffer, Alignment, BracketModeEnd, BracketModeStart, Cell, Color, DisableMouse,
DisableSGRMouse, EnableMouse, EnableSGRMouse, Pos, RestoreWindowTitleIconFromStack,
SaveWindowTitleIconToStack,
},
Attr, Context,
};
@ -45,9 +45,10 @@ type DrawHorizontalSegmentFn = fn(&mut CellBuffer, &mut StateStdout, usize, usiz
pub struct ScreenGeneration((u64, u64));
impl ScreenGeneration {
pub const NIL: ScreenGeneration = Self((0, 0));
pub const NIL: Self = Self((0, 0));
#[inline]
#[must_use]
pub fn next(self) -> Self {
Self(uuid::Uuid::new_v4().as_u64_pair())
}
@ -234,7 +235,7 @@ impl Screen<Tty> {
Self::init(Tty {
stdout: None,
mouse: false,
draw_horizontal_segment_fn: Screen::draw_horizontal_segment,
draw_horizontal_segment_fn: Self::draw_horizontal_segment,
})
}
@ -328,7 +329,10 @@ impl Screen<Tty> {
}
pub fn switch_to_alternate_screen(&mut self, context: &crate::Context) {
let mut stdout = BufWriter::with_capacity(240 * 80, Box::new(std::io::stdout()) as _);
let mut stdout = BufWriter::with_capacity(
240 * 80,
Box::new(std::io::stdout()) as Box<dyn std::io::Write>,
);
write!(
&mut stdout,
@ -402,6 +406,9 @@ impl Screen<Tty> {
}
if !c.empty() {
write!(stdout, "{}", c.ch()).unwrap();
if c.attrs().intersects(Attr::FORCE_TEXT) {
_ = write!(stdout, "\u{FE0E}");
}
}
}
}
@ -429,6 +436,9 @@ impl Screen<Tty> {
}
if !c.empty() {
write!(stdout, "{}", c.ch()).unwrap();
if c.attrs().intersects(Attr::FORCE_TEXT) {
_ = write!(stdout, "\u{FE0E}");
}
}
}
}
@ -512,6 +522,30 @@ impl<D: private::Sealed> From<&Screen<D>> for Area {
}
}
/// Convenience trait to turn both single `usize` values and `(usize, _)`
/// positions to `x` coordinate.
pub trait IntoColumns: private::Sealed {
#[must_use]
fn into(self) -> usize;
}
impl private::Sealed for usize {}
impl private::Sealed for Pos {}
impl IntoColumns for usize {
#[must_use]
fn into(self) -> usize {
self
}
}
impl IntoColumns for Pos {
#[must_use]
fn into(self) -> usize {
get_x(self)
}
}
impl Area {
#[inline]
pub fn height(&self) -> usize {
@ -536,6 +570,7 @@ impl Area {
/// Get `n`th row of `area` or its last one.
#[inline]
#[must_use]
pub fn nth_row(&self, n: usize) -> Self {
let Self {
offset,
@ -565,6 +600,7 @@ impl Area {
/// Get `n`th col of `area` or its last one.
#[inline]
#[must_use]
pub fn nth_col(&self, n: usize) -> Self {
let Self {
offset,
@ -593,6 +629,7 @@ impl Area {
}
/// Place box given by `(width, height)` in corner of `area`
#[must_use]
pub fn place_inside(&self, (width, height): (usize, usize), upper: bool, left: bool) -> Self {
if self.is_empty() || width < 3 || height < 3 {
return *self;
@ -635,6 +672,7 @@ impl Area {
/// Place given area of dimensions `(width, height)` inside `area` according
/// to given alignment
#[must_use]
pub fn align_inside(
&self,
(width, height): (usize, usize),
@ -668,6 +706,7 @@ impl Area {
/// Place box given by `dimensions` in center of `area`
#[inline]
#[must_use]
pub fn center_inside(&self, dimensions: (usize, usize)) -> Self {
self.align_inside(dimensions, Alignment::Center, Alignment::Center)
}
@ -703,6 +742,7 @@ impl Area {
/// assert_eq!(body.height(), 18);
/// ```
#[inline]
#[must_use]
pub fn skip_rows(&self, n: usize) -> Self {
let n = std::cmp::min(n, self.height());
if self.is_empty() || self.upper_left.1 + n > self.bottom_right.1 {
@ -734,6 +774,7 @@ impl Area {
/// assert_eq!(header, area.take_rows(2));
/// ```
#[inline]
#[must_use]
pub fn skip_rows_from_end(&self, n: usize) -> Self {
let n = std::cmp::min(n, self.height());
if self.is_empty() || self.bottom_right.1 < n {
@ -746,6 +787,21 @@ impl Area {
}
}
#[inline]
#[must_use]
fn _skip_cols_inner(&self, n: usize) -> Self {
let n = std::cmp::min(n, self.width());
if self.is_empty() || self.bottom_right.0 < self.upper_left.0 + n {
return self.into_empty();
}
Self {
offset: pos_inc(self.offset, (n, 0)),
upper_left: pos_inc(self.upper_left, (n, 0)),
..*self
}
}
/// Skip the first `n` rows and return the remaining area.
/// Return value will be an empty area if `n` is more than the width.
///
@ -763,17 +819,10 @@ impl Area {
/// assert_eq!(indent.width(), 118);
/// ```
#[inline]
pub fn skip_cols(&self, n: usize) -> Self {
let n = std::cmp::min(n, self.width());
if self.is_empty() || self.bottom_right.0 < self.upper_left.0 + n {
return self.into_empty();
}
Self {
offset: pos_inc(self.offset, (n, 0)),
upper_left: pos_inc(self.upper_left, (n, 0)),
..*self
}
#[must_use]
pub fn skip_cols(&self, n: impl IntoColumns) -> Self {
let n: usize = n.into();
self._skip_cols_inner(n)
}
/// Skip the last `n` rows and return the remaining area.
@ -794,6 +843,7 @@ impl Area {
/// assert_eq!(indent, area.take_cols(118));
/// ```
#[inline]
#[must_use]
pub fn skip_cols_from_end(&self, n: usize) -> Self {
let n = std::cmp::min(n, self.width());
if self.is_empty() || self.bottom_right.0 < n {
@ -807,6 +857,7 @@ impl Area {
/// Shortcut for using `Area::skip_cols` and `Area::skip_rows` together.
#[inline]
#[must_use]
pub fn skip(&self, n_cols: usize, n_rows: usize) -> Self {
self.skip_cols(n_cols).skip_rows(n_rows)
}
@ -828,6 +879,7 @@ impl Area {
/// assert_eq!(header.height(), 2);
/// ```
#[inline]
#[must_use]
pub fn take_rows(&self, n: usize) -> Self {
let n = std::cmp::min(n, self.height());
if self.is_empty() || self.bottom_right.1 < (self.height() - n) {
@ -860,6 +912,7 @@ impl Area {
/// assert_eq!(header.width(), 2);
/// ```
#[inline]
#[must_use]
pub fn take_cols(&self, n: usize) -> Self {
let n = std::cmp::min(n, self.width());
if self.is_empty() || self.bottom_right.0 < (self.width() - n) {
@ -877,31 +930,49 @@ impl Area {
/// Shortcut for using `Area::take_cols` and `Area::take_rows` together.
#[inline]
#[must_use]
pub fn take(&self, n_cols: usize, n_rows: usize) -> Self {
self.take_cols(n_cols).take_rows(n_rows)
}
#[inline]
#[must_use]
pub const fn upper_left(&self) -> Pos {
self.upper_left
}
#[inline]
#[must_use]
pub const fn bottom_right(&self) -> Pos {
self.bottom_right
}
#[inline]
#[must_use]
pub const fn upper_right(&self) -> Pos {
set_x(self.upper_left, get_x(self.bottom_right))
}
#[inline]
#[must_use]
pub const fn bottom_left(&self) -> Pos {
set_y(self.upper_left, get_y(self.bottom_right))
}
#[inline]
#[must_use]
pub const fn offset(&self) -> Pos {
self.offset
}
#[inline]
#[must_use]
pub const fn generation(&self) -> ScreenGeneration {
self.generation
}
#[inline]
#[must_use]
pub const fn new_empty(generation: ScreenGeneration) -> Self {
Self {
offset: (0, 0),
@ -915,6 +986,7 @@ impl Area {
}
#[inline]
#[must_use]
pub const fn into_empty(self) -> Self {
Self {
offset: (0, 0),
@ -926,12 +998,43 @@ impl Area {
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.empty
|| (self.upper_left.0 > self.bottom_right.0 || self.upper_left.1 > self.bottom_right.1)
}
}
#[inline(always)]
#[must_use]
const fn pos_inc(p: Pos, inc: (usize, usize)) -> Pos {
(p.0 + inc.0, p.1 + inc.1)
}
#[inline(always)]
#[must_use]
const fn get_x(p: Pos) -> usize {
p.0
}
#[inline(always)]
#[must_use]
const fn get_y(p: Pos) -> usize {
p.1
}
#[inline(always)]
#[must_use]
const fn set_x(p: Pos, new_x: usize) -> Pos {
(new_x, p.1)
}
#[inline(always)]
#[must_use]
const fn set_y(p: Pos, new_y: usize) -> Pos {
(p.0, new_y)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -19,7 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use melib::text_processing::TextProcessing;
use melib::text::TextProcessing;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct UText {
@ -30,7 +30,7 @@ pub struct UText {
impl UText {
pub fn new(content: String) -> Self {
UText {
Self {
cursor_pos: content.len(),
grapheme_cursor_pos: content.split_graphemes().len(),
content,

View File

@ -31,7 +31,7 @@
//! `vi`.
//!
//! [`UIEvent`] is the type passed around
//! [`Component`](crate::components::Component)'s when something happens.
//! [`Component`]'s when something happens.
#[macro_use]
mod helpers;
@ -100,9 +100,16 @@ pub enum ForkType {
/// Already finished fork, we only want to restore input/output
Finished,
/// Embedded pty
Embedded(Pid),
Generic(std::process::Child),
NewDraft(File, std::process::Child),
Embedded {
id: Cow<'static, str>,
command: Option<Cow<'static, str>>,
pid: Pid,
},
Generic {
id: Cow<'static, str>,
command: Option<Cow<'static, str>>,
child: std::process::Child,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -117,12 +124,12 @@ pub enum NotificationType {
impl std::fmt::Display for NotificationType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
NotificationType::Info => write!(f, "info"),
NotificationType::Error(melib::error::ErrorKind::None) => write!(f, "error"),
NotificationType::Error(kind) => write!(f, "error: {}", kind),
NotificationType::NewMail => write!(f, "new mail"),
NotificationType::SentMail => write!(f, "sent mail"),
NotificationType::Saved => write!(f, "saved"),
Self::Info => write!(f, "info"),
Self::Error(melib::error::ErrorKind::None) => write!(f, "error"),
Self::Error(kind) => write!(f, "error: {}", kind),
Self::NewMail => write!(f, "new mail"),
Self::SentMail => write!(f, "sent mail"),
Self::Saved => write!(f, "saved"),
}
}
}
@ -140,8 +147,8 @@ pub enum UIEvent {
Command(String),
Notification {
title: Option<Cow<'static, str>>,
source: Option<Error>,
body: Cow<'static, str>,
source: Option<Error>,
kind: Option<NotificationType>,
},
Action(Action),
@ -187,7 +194,7 @@ impl std::fmt::Debug for CallbackFn {
impl From<RefreshEvent> for UIEvent {
fn from(event: RefreshEvent) -> Self {
UIEvent::RefreshEvent(Box::new(event))
Self::RefreshEvent(Box::new(event))
}
}
@ -207,11 +214,11 @@ impl std::fmt::Display for UIMode {
f,
"{}",
match *self {
UIMode::Normal => "NORMAL",
UIMode::Insert => "INSERT",
UIMode::Command => "COMMAND",
UIMode::Fork => "FORK",
UIMode::Embedded => "EMBEDDED",
Self::Normal => "NORMAL",
Self::Insert => "INSERT",
Self::Command => "COMMAND",
Self::Fork => "FORK",
Self::Embedded => "EMBEDDED",
}
)
}
@ -233,15 +240,15 @@ pub mod segment_tree {
}
impl From<SmallVec<[u8; 1024]>> for SegmentTree {
fn from(val: SmallVec<[u8; 1024]>) -> SegmentTree {
SegmentTree::new(val)
fn from(val: SmallVec<[u8; 1024]>) -> Self {
Self::new(val)
}
}
impl SegmentTree {
pub fn new(val: SmallVec<[u8; 1024]>) -> SegmentTree {
pub fn new(val: SmallVec<[u8; 1024]>) -> Self {
if val.is_empty() {
return SegmentTree {
return Self {
array: val.clone(),
tree: val,
};
@ -262,7 +269,7 @@ pub mod segment_tree {
segment_tree[i] = std::cmp::max(segment_tree[2 * i], segment_tree[2 * i + 1]);
}
SegmentTree {
Self {
array: val,
tree: segment_tree,
}
@ -348,7 +355,7 @@ pub struct RateLimit {
impl RateLimit {
pub fn new(reqs: u64, millis: u64, job_executor: Arc<JobExecutor>) -> Self {
RateLimit {
Self {
last_tick: std::time::Instant::now(),
timer: job_executor.create_timer(
std::time::Duration::from_secs(0),
@ -389,7 +396,7 @@ pub enum ContactEvent {
#[derive(Debug)]
pub enum ComposeEvent {
SetReceipients(Vec<melib::Address>),
SetRecipients(Vec<melib::Address>),
}
#[cfg(test)]

View File

@ -23,11 +23,14 @@ use std::{
fs,
fs::OpenOptions,
io::{Read, Write},
os::unix::fs::PermissionsExt,
os::{
fd::{FromRawFd, OwnedFd},
unix::fs::PermissionsExt,
},
path::{Path, PathBuf},
};
use melib::{error::*, uuid::Uuid};
use melib::{error::*, uuid::Uuid, ShellExpandTrait};
/// Temporary file that can optionally cleaned up when it is dropped.
#[derive(Debug)]
@ -53,6 +56,7 @@ impl File {
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&self.path)
.chain_err_summary(|| format!("Could not create/open path {}", self.path.display()))
}
@ -73,7 +77,7 @@ impl File {
inner(&self.path).chain_err_summary(|| format!("Can't read {}", self.path.display()))
}
/// Returned `File` will be deleted when dropped if delete_on_drop is set,
/// Returned `File` will be deleted when dropped if `delete_on_drop` is set,
/// so make sure to add it on `context.temp_files` to reap it later.
pub fn create_temp_file(
bytes: &[u8],
@ -101,7 +105,8 @@ impl File {
path.set_extension(ext);
}
fn inner(path: &Path, bytes: &[u8], delete_on_drop: bool) -> Result<File> {
let mut f = std::fs::File::create(path)?;
let path = path.expand();
let mut f = std::fs::File::create(&path)?;
let metadata = f.metadata()?;
let mut permissions = metadata.permissions();
@ -111,7 +116,7 @@ impl File {
f.write_all(bytes)?;
f.flush()?;
Ok(File {
path: path.to_path_buf(),
path,
delete_on_drop,
})
}
@ -120,6 +125,18 @@ impl File {
}
}
pub fn pipe() -> Result<(OwnedFd, OwnedFd)> {
nix::unistd::pipe()
.map(|(fd1, fd2)| unsafe { (OwnedFd::from_raw_fd(fd1), OwnedFd::from_raw_fd(fd2)) })
.map_err(|err| {
Error::new("Could not create pipe")
.set_source(Some(
(Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>).into(),
))
.set_kind(ErrorKind::OSError)
})
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -21,7 +21,7 @@
//! Various useful utilities.
use melib::{text_processing::Reflow, ShellExpandTrait};
use melib::{text::Reflow, ShellExpandTrait};
pub type AutoCompleteFn = Box<dyn Fn(&Context, &str) -> Vec<AutoCompleteEntry> + Send + Sync>;
@ -49,7 +49,7 @@ use std::collections::HashSet;
use indexmap::IndexMap;
pub use self::tables::*;
use crate::{components::ExtendShortcutsMaps, jobs::JobId, melib::text_processing::TextProcessing};
use crate::{components::ExtendShortcutsMaps, jobs::JobId, melib::text::TextProcessing};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SearchMovement {
@ -114,7 +114,7 @@ impl StatusBar {
None => {}
}
StatusBar {
Self {
container,
status: String::with_capacity(256),
status_message: String::with_capacity(256),
@ -717,7 +717,7 @@ impl Component for StatusBar {
self.dirty = true;
}
UIEvent::StatusEvent(StatusEvent::BufSet(s)) => {
self.display_buffer = s.clone();
self.display_buffer.clone_from(s);
self.dirty = true;
}
UIEvent::StatusEvent(StatusEvent::UpdateStatus(ref mut s)) => {
@ -812,9 +812,12 @@ impl Component for StatusBar {
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
let mut ret = IndexMap::default();
ret.insert(self.container.id(), &self.container as _);
ret.insert(self.ex_buffer.id(), &self.ex_buffer as _);
ret.insert(self.progress_spinner.id(), &self.progress_spinner as _);
ret.insert(self.container.id(), &self.container as &dyn Component);
ret.insert(self.ex_buffer.id(), &self.ex_buffer as &dyn Component);
ret.insert(
self.progress_spinner.id(),
&self.progress_spinner as &dyn Component,
);
ret
}
@ -862,11 +865,11 @@ pub struct Tabbed {
impl Tabbed {
pub fn new(children: Vec<Box<dyn Component>>, context: &Context) -> Self {
let pinned = children.len();
let mut ret = Tabbed {
let mut ret = Self {
help_view: HelpView {
content: Screen::<Virtual>::new(),
curr_views: children
.get(0)
.first()
.map(|c| c.shortcuts(context))
.unwrap_or_default(),
cursor: (0, 0),
@ -979,11 +982,14 @@ impl Component for Tabbed {
}
if (children_maps == self.help_view.curr_views) && must_redraw_shortcuts {
let dialog_area = area.align_inside(
/* add box perimeter padding */
pos_inc(self.help_view.content.area().size(), (1, 1)),
/* horizontal */
// add box perimeter padding
{
let (w, h) = self.help_view.content.area().size();
(w + 1, h + 1)
},
// horizontal
Alignment::Center,
/* vertical */
// vertical
Alignment::Center,
);
context.dirty_areas.push_back(dialog_area);
@ -1154,11 +1160,14 @@ impl Component for Tabbed {
}
self.help_view.curr_views = children_maps;
let dialog_area = area.align_inside(
/* add box perimeter padding */
pos_inc(self.help_view.content.area().size(), (1, 1)),
/* horizontal */
// add box perimeter padding
{
let (w, h) = self.help_view.content.area().size();
(w + 1, h + 1)
},
// horizontal
Alignment::Center,
/* vertical */
// vertical
Alignment::Center,
);
context.dirty_areas.push_back(dialog_area);
@ -1187,7 +1196,7 @@ impl Component for Tabbed {
let (width, height) = self.help_view.content.area().size();
let (cols, rows) = inner_area.size();
if let Some(ref mut search) = self.help_view.search {
use crate::melib::text_processing::search::KMP;
use crate::melib::text::search::KMP;
search.positions = self
.help_view
.content
@ -1377,7 +1386,7 @@ impl Component for Tabbed {
self.dirty = true;
return true;
}
UIEvent::Action(Tab(New(ref mut e))) if e.is_some() => {
UIEvent::Action(Tab(New(ref mut e @ Some(_)))) => {
self.add_component(e.take().unwrap(), context);
self.children[self.cursor_pos]
.process_event(&mut UIEvent::VisibilityChange(false), context);
@ -1559,7 +1568,7 @@ impl Component for Tabbed {
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
let mut ret = IndexMap::default();
for c in &self.children {
ret.insert(c.id(), c as _);
ret.insert(c.id(), c as &dyn Component);
}
ret
}

View File

@ -38,11 +38,11 @@ enum SelectorCursor {
/// Shows a little window with options for user to select.
///
/// Instantiate with Selector::new(). Set single_only to true if user should
/// Instantiate with `Selector::new()`. Set `single_only` to true if user should
/// only choose one of the options. After passing input events to this
/// component, check Selector::is_done to see if the user has finalised their
/// choices. Collect the choices by consuming the Selector with
/// Selector::collect()
/// component, check `Selector::is_done` to see if the user has finalised their
/// choices. Collect the choices by consuming the `Selector` with
/// `Selector::collect()`
pub struct Selector<
T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send,
F: 'static + Sync + Send,
@ -54,11 +54,14 @@ pub struct Selector<
theme_default: ThemeAttribute,
cursor: SelectorCursor,
scroll_x_cursor: usize,
movement: Option<PageMovement>,
vertical_alignment: Alignment,
horizontal_alignment: Alignment,
title: String,
/// If true, user has finished their selection
content: Screen<Virtual>,
initialized: bool,
/// If `true`, user has finished their selection
done: bool,
done_fn: F,
dirty: bool,
@ -94,7 +97,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
PartialEq for Selector<T, F>
{
fn eq(&self, other: &Selector<T, F>) -> bool {
fn eq(&self, other: &Self) -> bool {
self.entries == other.entries
}
}
@ -127,6 +130,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
* cursor */
self.entries[c].1 = !self.entries[c].1;
self.dirty = true;
self.initialized = false;
return true;
}
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => {
@ -169,6 +173,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
}
self.cursor = SelectorCursor::Entry(0);
self.dirty = true;
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
@ -181,6 +186,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
}
self.cursor = SelectorCursor::Entry(c - 1);
self.dirty = true;
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Ok)
@ -190,6 +196,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
let c = self.entries.len().saturating_sub(1);
self.cursor = SelectorCursor::Entry(c);
self.dirty = true;
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
@ -203,6 +210,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
}
self.cursor = SelectorCursor::Entry(c + 1);
self.dirty = true;
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Entry(_))
@ -211,6 +219,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
{
self.cursor = SelectorCursor::Ok;
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Ok)
@ -218,6 +227,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
{
self.cursor = SelectorCursor::Cancel;
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Cancel)
@ -225,15 +235,62 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
{
self.cursor = SelectorCursor::Ok;
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), _)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"])
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"])
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"])
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) =>
{
self.movement = Some(PageMovement::Left(1));
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), _)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) =>
{
self.movement = Some(PageMovement::Right(1));
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), _)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["prev_page"]) =>
{
self.movement = Some(PageMovement::PageUp(1));
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), _)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_page"]) =>
{
self.movement = Some(PageMovement::PageDown(1));
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), _)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
{
self.movement = Some(PageMovement::Home);
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), _)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
{
self.movement = Some(PageMovement::End);
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), _)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"])
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
{
return true
return true;
}
_ => {}
}
@ -256,6 +313,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
self.initialized = false;
}
fn id(&self) -> ComponentId {
@ -285,6 +343,7 @@ impl Component for UIConfirmationDialog {
self.unrealize(context);
}
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Entry(c)) if !self.single_only => {
@ -292,6 +351,7 @@ impl Component for UIConfirmationDialog {
* cursor */
self.entries[c].1 = !self.entries[c].1;
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => {
@ -301,6 +361,7 @@ impl Component for UIConfirmationDialog {
self.unrealize(context);
}
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(Key::Esc), _) => {
@ -314,6 +375,7 @@ impl Component for UIConfirmationDialog {
_ = self.done();
self.cancel(context);
self.set_dirty(true);
self.initialized = false;
return false;
}
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => {
@ -326,6 +388,7 @@ impl Component for UIConfirmationDialog {
self.unrealize(context);
}
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
@ -338,6 +401,7 @@ impl Component for UIConfirmationDialog {
}
self.cursor = SelectorCursor::Entry(c - 1);
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Ok)
@ -347,6 +411,7 @@ impl Component for UIConfirmationDialog {
let c = self.entries.len().saturating_sub(1);
self.cursor = SelectorCursor::Entry(c);
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Unfocused)
@ -357,6 +422,7 @@ impl Component for UIConfirmationDialog {
}
self.cursor = SelectorCursor::Entry(0);
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
@ -370,6 +436,7 @@ impl Component for UIConfirmationDialog {
}
self.cursor = SelectorCursor::Entry(c + 1);
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Entry(_))
@ -378,6 +445,7 @@ impl Component for UIConfirmationDialog {
{
self.cursor = SelectorCursor::Ok;
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Ok)
@ -385,6 +453,7 @@ impl Component for UIConfirmationDialog {
{
self.cursor = SelectorCursor::Cancel;
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), SelectorCursor::Cancel)
@ -392,6 +461,7 @@ impl Component for UIConfirmationDialog {
{
self.cursor = SelectorCursor::Ok;
self.set_dirty(true);
self.initialized = false;
return true;
}
(UIEvent::Input(ref key), _)
@ -456,9 +526,13 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
entries: identifiers,
entry_titles,
cursor: SelectorCursor::Unfocused,
scroll_x_cursor: 0,
movement: None,
vertical_alignment: Alignment::Center,
horizontal_alignment: Alignment::Center,
title: title.to_string(),
content: Screen::<Virtual>::new(),
initialized: false,
done: false,
done_fn,
dirty: true,
@ -485,18 +559,18 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
.collect()
}
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
fn initialize(&mut self, context: &Context) {
let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted");
if !context.settings.terminal.use_color() {
highlighted_attrs.attrs |= Attr::REVERSE;
}
let shortcuts = context.settings.shortcuts.general.key_values();
let navigate_help_string = format!(
"Navigate options with {} to go down, {} to go up, select with {}",
"Navigate options with {} to go down, {} to go up, select with {}, cancel with {}",
shortcuts["scroll_down"],
shortcuts["scroll_up"],
Key::Char('\n')
Key::Char('\n'),
Key::Esc
);
let width = std::cmp::max(
self.entry_titles.iter().map(|e| e.len()).max().unwrap_or(0) + 3,
@ -507,33 +581,38 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
+ 3
// buttons row
+ if self.single_only { 1 } else { 5 };
let dialog_area = area.align_inside(
(width, height),
self.horizontal_alignment,
self.vertical_alignment,
);
let inner_area = create_box(grid, dialog_area);
grid.clear_area(inner_area, self.theme_default);
if !self.content.resize_with_context(width, height, context) {
self.dirty = false;
return;
}
grid.write_string(
let inner_area = self.content.area();
let (_, y) = self.content.grid_mut().write_string(
&self.title,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs | Attr::BOLD,
dialog_area.skip_cols(2),
inner_area.skip_cols(2),
None,
);
grid.write_string(
&navigate_help_string,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs | Attr::ITALICS,
dialog_area.skip_cols(2).skip_rows(height),
None,
);
let y = self
.content
.grid_mut()
.write_string(
&navigate_help_string,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs | Attr::ITALICS,
inner_area.skip_cols(2).skip_rows(y + 2),
None,
)
.1
+ y
+ 2;
let inner_area = inner_area.skip_cols(1).skip_rows(y + 2);
let inner_area = inner_area.skip_cols(1).skip_rows(1);
/* Extra room for buttons Okay/Cancel */
if self.single_only {
for (i, e) in self.entry_titles.iter().enumerate() {
@ -542,7 +621,14 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
} else {
self.theme_default
};
grid.write_string(e, attr.fg, attr.bg, attr.attrs, inner_area.nth_row(i), None);
self.content.grid_mut().write_string(
e,
attr.fg,
attr.bg,
attr.attrs,
inner_area.nth_row(i),
None,
);
}
} else {
for (i, e) in self.entry_titles.iter().enumerate() {
@ -551,7 +637,7 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
} else {
self.theme_default
};
grid.write_string(
self.content.grid_mut().write_string(
&format!("[{}] {}", if self.entries[i].1 { "x" } else { " " }, e),
attr.fg,
attr.bg,
@ -566,7 +652,7 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
} else {
self.theme_default
};
let (x, y) = grid.write_string(
let (x, y) = self.content.grid_mut().write_string(
OK,
attr.fg,
attr.bg,
@ -579,7 +665,7 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
} else {
self.theme_default
};
grid.write_string(
self.content.grid_mut().write_string(
CANCEL,
attr.fg,
attr.bg,
@ -588,6 +674,145 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
None,
);
}
self.initialized = true;
}
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted");
if !context.settings.terminal.use_color() {
highlighted_attrs.attrs |= Attr::REVERSE;
}
if !self.initialized {
// [ref:FIXME]: don't re-initialize when the only change is highlight index.
self.initialize(context);
}
let (width, height) = self.content.area().size();
let dialog_area = area.align_inside(
(width + 2, height + 2),
self.horizontal_alignment,
self.vertical_alignment,
);
let inner_area = create_box(grid, dialog_area);
let rows = inner_area.height();
if let Some(mvm) = self.movement.take() {
match mvm {
PageMovement::Up(_) | PageMovement::Down(_) => {}
PageMovement::Right(amount) => {
self.scroll_x_cursor = self.scroll_x_cursor.saturating_add(amount);
}
PageMovement::Left(amount) => {
self.scroll_x_cursor = self.scroll_x_cursor.saturating_sub(amount);
}
PageMovement::PageUp(multiplier) => match self.cursor {
SelectorCursor::Unfocused => {
self.cursor = SelectorCursor::Entry(0);
self.initialize(context);
}
SelectorCursor::Entry(c) => {
self.cursor = SelectorCursor::Entry(c.saturating_sub(multiplier * rows));
self.initialize(context);
}
SelectorCursor::Ok | SelectorCursor::Cancel
if !self.entry_titles.is_empty() =>
{
self.cursor = SelectorCursor::Entry(
self.entry_titles.len().saturating_sub(multiplier * rows),
);
self.initialize(context);
}
SelectorCursor::Ok | SelectorCursor::Cancel => {}
},
PageMovement::PageDown(multiplier) => match self.cursor {
SelectorCursor::Unfocused => {
self.cursor = SelectorCursor::Entry(
self.entry_titles
.len()
.saturating_sub(1)
.min(multiplier * rows),
);
self.initialize(context);
}
SelectorCursor::Entry(c)
if c.saturating_add(multiplier * rows) < self.entry_titles.len()
&& !self.entry_titles.is_empty() =>
{
self.cursor = SelectorCursor::Entry(
self.entry_titles
.len()
.saturating_sub(1)
.min(c.saturating_add(multiplier * rows)),
);
self.initialize(context);
}
SelectorCursor::Entry(_) => {
self.cursor = SelectorCursor::Ok;
self.initialize(context);
}
SelectorCursor::Ok | SelectorCursor::Cancel => {}
},
PageMovement::Home if !self.entry_titles.is_empty() => {
self.cursor = SelectorCursor::Entry(0);
self.initialize(context);
}
PageMovement::End
if matches!(self.cursor, SelectorCursor::Ok | SelectorCursor::Cancel) => {}
PageMovement::End
if !matches!(self.cursor, SelectorCursor::Entry(c) if c +1 == self.entry_titles.len())
&& !self.entry_titles.is_empty() =>
{
self.cursor = SelectorCursor::Entry(self.entry_titles.len().saturating_sub(1));
self.initialize(context);
}
PageMovement::Home | PageMovement::End => {}
}
}
let skip_rows = match self.cursor {
SelectorCursor::Unfocused => 0,
SelectorCursor::Entry(e) if e >= rows => e.min(height.saturating_sub(rows)),
SelectorCursor::Entry(_) => 0,
SelectorCursor::Ok | SelectorCursor::Cancel => height.saturating_sub(rows),
};
self.scroll_x_cursor = self
.scroll_x_cursor
.min(width.saturating_sub(inner_area.width()));
grid.copy_area(
self.content.grid(),
inner_area,
self.content
.area()
.skip_cols(self.scroll_x_cursor)
.skip_rows(skip_rows),
);
if height > dialog_area.height() {
let inner_area = inner_area.skip_rows(1);
ScrollBar::default().set_show_arrows(true).draw(
grid,
inner_area.nth_col(inner_area.width().saturating_sub(1)),
context,
// position
skip_rows,
// visible_rows
inner_area.height(),
// length
height,
);
}
if width > dialog_area.width() {
let inner_area = inner_area.skip_cols(1);
ScrollBar::default().set_show_arrows(true).draw_horizontal(
grid,
inner_area.nth_row(inner_area.height().saturating_sub(1)),
context,
// position
self.scroll_x_cursor,
// visible_cols
inner_area.width(),
// length
width,
);
}
context.dirty_areas.push_back(dialog_area);
self.dirty = false;
}

View File

@ -19,16 +19,16 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use melib::text_processing::{LineBreakText, Truncate};
use melib::text::{LineBreakText, Truncate};
use super::*;
use crate::terminal::embedded::EmbeddedGrid;
use crate::{jobs::JoinHandle, terminal::embedded::EmbeddedGrid};
/// A pager for text.
/// `Pager` holds its own content in its own `CellBuffer` and when `draw` is
/// called, it draws the current view of the text. It is responsible for
/// scrolling etc.
#[derive(Clone, Debug, Default)]
#[derive(Debug, Default)]
pub struct Pager {
text: String,
cursor: (usize, usize),
@ -51,12 +51,39 @@ pub struct Pager {
/// events.
rows_lt_height: bool,
filtered_content: Option<(String, Result<EmbeddedGrid>)>,
filter_job: Option<(String, JoinHandle<Result<EmbeddedGrid>>)>,
text_lines: Vec<String>,
line_breaker: LineBreakText,
movement: Option<PageMovement>,
id: ComponentId,
}
impl Clone for Pager {
fn clone(&self) -> Self {
Self {
filter_job: None,
text: self.text.clone(),
cursor: self.cursor,
reflow: self.reflow,
height: self.height,
width: self.width,
minimum_width: self.minimum_width,
search: self.search.clone(),
dirty: true,
colors: self.colors,
initialised: false,
show_scrollbar: self.show_scrollbar,
cols_lt_width: self.cols_lt_width,
rows_lt_height: self.rows_lt_height,
filtered_content: self.filtered_content.clone(),
text_lines: self.text_lines.clone(),
line_breaker: self.line_breaker.clone(),
movement: self.movement,
id: ComponentId::default(),
}
}
}
impl std::fmt::Display for Pager {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "pager")
@ -124,31 +151,19 @@ impl Pager {
pub fn from_string(
text: String,
context: Option<&Context>,
context: &Context,
cursor_pos: Option<usize>,
mut width: Option<usize>,
colors: ThemeAttribute,
) -> Self {
let pager_filter: Option<&String> = if let Some(context) = context {
context.settings.pager.filter.as_ref()
} else {
None
};
let pager_filter: Option<&String> = context.settings.pager.filter.as_ref();
let pager_minimum_width: usize = if let Some(context) = context {
context.settings.pager.minimum_width
} else {
0
};
let pager_minimum_width: usize = context.settings.pager.minimum_width;
let reflow: Reflow = if let Some(context) = context {
if context.settings.pager.split_long_lines {
Reflow::All
} else {
Reflow::No
}
} else {
let reflow: Reflow = if context.settings.pager.split_long_lines {
Reflow::All
} else {
Reflow::No
};
if let Some(ref mut width) = width.as_mut() {
@ -174,20 +189,20 @@ impl Pager {
};
if let Some(bin) = pager_filter {
ret.filter(bin);
ret.filter(bin, context);
}
ret
}
pub fn filter(&mut self, cmd: &str) {
let _f = |bin: &str, text: &str| -> Result<EmbeddedGrid> {
pub fn filter(&mut self, cmd: &str, context: &Context) {
async fn filter_fut(bin: String, text: String) -> Result<EmbeddedGrid> {
use std::{
io::Write,
process::{Command, Stdio},
};
let mut filter_child = Command::new("sh")
.args(["-c", bin])
.args(["-c", &bin])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -208,13 +223,13 @@ impl Pager {
embedded.process_byte(&mut dev_null, b);
}
Ok(embedded)
};
let buf = _f(cmd, &self.text);
if let Some((width, height)) = buf.as_ref().ok().map(EmbeddedGrid::terminal_size) {
self.width = width;
self.height = height;
}
self.filtered_content = Some((cmd.to_string(), buf));
let fut = Box::pin(filter_fut(cmd.to_string(), self.text.clone()));
let handle = context
.main_loop_handler
.job_executor
.spawn_blocking(format!("Running pager filter {cmd}").into(), fut);
self.filter_job = Some((cmd.to_string(), handle));
}
pub fn cursor_pos(&self) -> usize {
@ -244,7 +259,7 @@ impl Pager {
self.height = self.text_lines.len();
self.width = width;
if let Some(ref mut search) = self.search {
use melib::text_processing::search::KMP;
use melib::text::search::KMP;
search.positions.clear();
for (y, l) in self.text_lines.iter().enumerate() {
search.positions.extend(
@ -278,7 +293,7 @@ impl Pager {
_context: &mut Context,
up_to: usize,
) {
if self.line_breaker.is_finished() {
if self.line_breaker.is_finished() || self.filtered_content.is_some() {
return;
}
let old_lines_no = self.text_lines.len();
@ -294,7 +309,7 @@ impl Pager {
};
let new_lines_no = self.text_lines.len() - old_lines_no;
if let Some(ref mut search) = self.search {
use melib::text_processing::search::KMP;
use melib::text::search::KMP;
for (y, l) in self.text_lines.iter().enumerate().skip(old_lines_no) {
search.positions.extend(
l.kmp_search(&search.pattern)
@ -317,15 +332,14 @@ impl Pager {
.area()
.skip_cols(self.cursor.0)
.skip_rows(self.cursor.1)
.take_cols(content.terminal_size().0.saturating_sub(area.width()))
.take_rows(content.terminal_size().1.saturating_sub(area.height())),
.take_cols(content.terminal_size().0.min(area.width()))
.take_rows(content.terminal_size().1.min(area.height())),
);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(
cmd.to_string(),
)));
return;
}
Err(ref err) => {
let mut cmd = cmd.as_str();
@ -338,6 +352,7 @@ impl Pager {
))));
}
}
return;
}
{
@ -759,11 +774,35 @@ impl Component for Pager {
return true;
}
UIEvent::Action(View(Filter(ref cmd))) => {
self.filter(cmd);
self.filter(cmd, context);
self.initialised = false;
self.dirty = true;
return true;
}
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
if self
.filter_job
.as_ref()
.map(|(_, h)| h == job_id)
.unwrap_or(false) =>
{
let (cmd, mut handle) = self.filter_job.take().unwrap();
match handle.chan.try_recv() {
Err(_) => { /* search was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
Ok(Some(buf)) => {
if let Some((width, height)) =
buf.as_ref().ok().map(EmbeddedGrid::terminal_size)
{
self.width = width;
self.height = height;
}
self.filtered_content = Some((cmd, buf));
}
}
self.initialised = false;
self.set_dirty(true);
}
UIEvent::Action(Action::Listing(ListingAction::Search(pattern))) => {
self.search = Some(SearchPattern {
pattern: pattern.to_string(),
@ -835,13 +874,26 @@ impl Component for Pager {
String::new(),
)));
}
UIEvent::Input(ref key)
if context.settings.shortcuts.pager.commands.iter().any(|cmd| {
if cmd.shortcut == *key {
for cmd in &cmd.command {
context.replies.push_back(UIEvent::Command(cmd.to_string()));
}
return true;
}
false
}) =>
{
return true;
}
_ => {}
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
self.dirty || !self.initialised
}
fn set_dirty(&mut self, value: bool) {
@ -864,4 +916,12 @@ impl Component for Pager {
fn id(&self) -> ComponentId {
self.id
}
fn kill(&mut self, uuid: ComponentId, context: &mut Context) {
if self.id != uuid {
return;
}
context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
}
}

View File

@ -144,7 +144,7 @@ impl<const N: usize> Default for DataColumns<N> {
for elem in &mut data[..] {
elem.write(cl());
}
let ptr = &data as *const [MaybeUninit<T>; N];
let ptr = std::ptr::addr_of!(data);
unsafe { (ptr as *const [T; N]).read() }
}
@ -214,7 +214,7 @@ impl<const N: usize> DataColumns<N> {
width_accum += self.widths[i];
}
// add column gaps
width_accum += 2 * N.saturating_sub(1);
width_accum += N.saturating_sub(1);
debug_assert!(growees >= growees_max);
if width_accum >= screen_width || screen_height == 0 || screen_width == 0 || growees == 0 {
self.width_accum = width_accum;
@ -260,7 +260,7 @@ impl<const N: usize> DataColumns<N> {
break;
}
x_offset -= self.widths[col];
x_offset = x_offset.saturating_sub(2);
x_offset = x_offset.saturating_sub(1);
}
for col in start_col..N {

View File

@ -20,7 +20,7 @@
*/
use super::*;
use crate::melib::text_processing::Truncate;
use crate::melib::text::Truncate;
pub struct TextField {
inner: UText,
@ -39,7 +39,7 @@ impl std::fmt::Debug for TextField {
}
impl Default for TextField {
fn default() -> TextField {
fn default() -> Self {
Self {
inner: UText::new(String::with_capacity(256)),
autocomplete: None,

View File

@ -22,7 +22,7 @@
use std::{borrow::Cow, collections::HashMap, time::Duration};
use super::*;
use crate::melib::text_processing::TextProcessing;
use crate::melib::text::TextProcessing;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum FormFocus {
@ -49,8 +49,8 @@ impl std::fmt::Debug for Field {
}
impl Default for Field {
fn default() -> Field {
Field::Text(TextField::default())
fn default() -> Self {
Self::Text(TextField::default())
}
}
@ -253,8 +253,8 @@ impl<T: 'static + std::fmt::Debug + Copy + Default + Send + Sync> FormWidget<T>
action: (Cow<'static, str>, T),
cursor_up_shortcut: Key,
cursor_down_shortcut: Key,
) -> FormWidget<T> {
FormWidget {
) -> Self {
Self {
buttons: ButtonWidget::new(action),
focus: FormFocus::Fields,
hide_buttons: false,
@ -556,8 +556,8 @@ impl<T> ButtonWidget<T>
where
T: 'static + std::fmt::Debug + Copy + Default + Send + Sync,
{
pub fn new(init_val: (Cow<'static, str>, T)) -> ButtonWidget<T> {
ButtonWidget {
pub fn new(init_val: (Cow<'static, str>, T)) -> Self {
Self {
layout: vec![init_val.0.clone()],
buttons: vec![init_val].into_iter().collect(),
result: None,
@ -676,7 +676,7 @@ impl AutoCompleteEntry {
impl From<String> for AutoCompleteEntry {
fn from(val: String) -> Self {
AutoCompleteEntry {
Self {
entry: val,
description: String::new(),
}
@ -686,7 +686,7 @@ impl From<String> for AutoCompleteEntry {
impl From<&(&str, &str, TokenStream)> for AutoCompleteEntry {
fn from(val: &(&str, &str, TokenStream)) -> Self {
let (a, b, _) = val;
AutoCompleteEntry {
Self {
entry: a.to_string(),
description: b.to_string(),
}
@ -696,7 +696,7 @@ impl From<&(&str, &str, TokenStream)> for AutoCompleteEntry {
impl From<(String, String)> for AutoCompleteEntry {
fn from(val: (String, String)) -> Self {
let (a, b) = val;
AutoCompleteEntry {
Self {
entry: a,
description: b,
}
@ -731,7 +731,7 @@ impl Component for AutoComplete {
}
let page_no = (self.cursor.saturating_sub(1)).wrapping_div(rows);
let top_idx = page_no * rows;
let x_offset = if rows < self.entries.len() { 1 } else { 0 };
let x_offset = usize::from(rows < self.entries.len());
grid.clear_area(area, crate::conf::value(context, "theme_default"));
let width = self
@ -817,7 +817,7 @@ impl Component for AutoComplete {
impl AutoComplete {
pub fn new(entries: Vec<AutoCompleteEntry>) -> Box<Self> {
let mut ret = AutoComplete {
let mut ret = Self {
entries: Vec::new(),
cursor: 0,
dirty: true,
@ -1102,7 +1102,7 @@ impl ProgressSpinner {
theme_attr.attrs |= Attr::REVERSE;
}
theme_attr.attrs |= Attr::BOLD;
ProgressSpinner {
Self {
timer,
stage: 0,
kind: Ok(kind),

View File

@ -1,13 +1,13 @@
[package]
name = "melib"
version = "0.8.5-rc.3"
version = "0.8.5"
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
edition = "2021"
build = "build.rs"
rust-version = "1.68.2"
rust-version = "1.70.0"
homepage = "https://meli.delivery"
repository = "https://git.meli.delivery/meli/meli.git"
homepage = "https://meli-email.org"
repository = "https://git.meli-email.org/meli/meli.git"
description = "library for e-mail clients and other e-mail applications"
keywords = ["mail", "mua", "maildir", "imap", "jmap"]
categories = ["email", "parser-implementations"]
@ -37,9 +37,9 @@ libc = { version = "0.2.125", features = ["extra_traits"] }
libloading = "^0.7"
log = { version = "0.4", features = ["std"] }
native-tls = { version = "0.2.3", default-features = false, optional = true }
nix = "^0.24"
nix = { version = "0.27", default-features = false, features = ["fs", "socket", "dir", "hostname"] }
nom = { version = "7" }
notify = { version = "4.0.15", optional = true }
notify = { version = "6.1.1", optional = true }
polling = "2.8"
regex = { version = "1" }
rusqlite = { version = "^0.29", default-features = false, features = ["array"], optional = true }
@ -49,9 +49,9 @@ serde_json = { version = "1.0", features = ["raw_value"] }
serde_path_to_error = { version = "0.1" }
smallvec = { version = "^1.5.0", features = ["serde"] }
smol = "1.0.0"
socket2 = { version = "0.4", features = [] }
unicode-segmentation = { version = "1.2.1", default-features = false, optional = true }
socket2 = { version = "0.5", features = [] }
unicode-segmentation = { version = "1.2.1", default-features = false }
url = { version = "2.4", optional = true }
uuid = { version = "^1", features = ["serde", "v4", "v5"] }
xdg = "2.1.0"
@ -64,27 +64,24 @@ http = ["isahc"]
http-static = ["isahc", "isahc/static-curl"]
imap = ["imap-codec", "tls"]
imap-trace = ["imap"]
jmap = ["http"]
jmap = ["http", "url/serde"]
jmap-trace = ["jmap"]
nntp = ["tls"]
nntp-trace = ["nntp"]
maildir = ["notify"]
mbox = ["notify"]
notmuch = []
notmuch = ["notify"]
smtp = ["tls", "base64"]
smtp-trace = ["smtp"]
sqlite3 = ["rusqlite"]
sqlite3-static = ["sqlite3", "rusqlite/bundled-full"]
tls = ["native-tls"]
tls-static = ["tls", "native-tls/vendored"]
text-processing = []
unicode-algorithms = ["text-processing", "unicode-segmentation"]
unicode-algorithms-cached = ["text-processing", "unicode-segmentation"]
vcard = []
[build-dependencies]
flate2 = { version = "1.0.16" }
[dev-dependencies]
mailin-embedded = { version = "0.7", features = ["rtls"] }
mailin-embedded = { version = "0.8", features = ["rtls"] }
stderrlog = "^0.5"

View File

@ -22,24 +22,6 @@ Library for handling mail.
|------------------------------|-------------------------------------|--------------------------|
| `sqlite` | `rusqlite` | Used in IMAP cache. |
|------------------------------|-------------------------------------|--------------------------|
| `unicode-algorithms` | `unicode-segmentation` | Linebreaking algo etc |
| | | For a fresh clean build, |
| | | Network access is |
| | | required to fetch data |
| | | from Unicode's website. |
|------------------------------|-------------------------------------|--------------------------|
| `unicode-algorithms-cached` | `unicode-segmentation` | Linebreaking algo etc |
| | | but it uses a cached |
| | | version of Unicode data |
| | | which might be stale. |
| | | |
| | | Use this feature instead |
| | | of the previous one for |
| | | building without network |
| | | access. |
|------------------------------|-------------------------------------|--------------------------|
| `unicode-algorithms` | `unicode-segmentation` | |
|------------------------------|-------------------------------------|--------------------------|
| `vcard` | | vcard parsing |
|------------------------------|-------------------------------------|--------------------------|
| `gpgme` | | GPG use with libgpgme |

View File

@ -21,15 +21,14 @@
#![allow(clippy::needless_range_loop)]
#[cfg(any(feature = "unicode-algorithms", feature = "unicode-algorithms-cached"))]
include!("src/text_processing/types.rs");
include!("src/text/types.rs");
fn main() -> Result<(), std::io::Error> {
#[cfg(any(feature = "unicode-algorithms", feature = "unicode-algorithms-cached"))]
{
const MOD_PATH: &str = "src/text_processing/tables.rs";
const MOD_PATH: &str = "src/text/tables.rs";
println!("cargo:rerun-if-env-changed=UNICODE_REGENERATE_TABLES");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", MOD_PATH);
println!("cargo:rerun-if-changed={MOD_PATH}");
/* Line break tables */
use std::{
fs::File,
@ -54,24 +53,6 @@ fn main() -> Result<(), std::io::Error> {
);
return Ok(());
}
if cfg!(feature = "unicode-algorithms-cached") {
const CACHED_MODULE: &[u8] =
include_bytes!(concat!("./src/text_processing/tables.rs.gz"));
let mut gz = GzDecoder::new(CACHED_MODULE);
use flate2::bufread::GzDecoder;
let mut v = String::with_capacity(
8, /*
str::parse::<usize>(unsafe {
std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap())
})
.unwrap_or_else(|_| panic!("was not compressed with size comment header",)),*/
);
gz.read_to_string(&mut v)?;
let mut file = File::create(mod_path)?;
file.write_all(v.as_bytes())?;
return Ok(());
}
let mut child = Command::new("curl")
.args(["-o", "-", LINE_BREAK_TABLE_URL])
.stdout(Stdio::piped())
@ -350,7 +331,7 @@ fn main() -> Result<(), std::io::Error> {
let mut file = File::create(mod_path)?;
file.write_all(
br#"/*
* meli - text_processing crate.
* meli - text crate.
*
* Copyright 2017-2020 Manos Pitsidianakis
*

Some files were not shown because too many files have changed in this diff Show More