Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd461fa820 | ||
![]() |
2870624d44 | ||
![]() |
30c599ab92 | ||
![]() |
c34465bab5 | ||
![]() |
1f6471556e | ||
![]() |
0cd0864fa1 |
11 changed files with 381 additions and 15 deletions
34
Makefile
34
Makefile
|
@ -25,7 +25,7 @@ CARGO_ARGS ?=
|
|||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility
|
||||
CARGO_SORT_BIN = cargo-sort
|
||||
CARGO_HACK_BIN = cargo-hack
|
||||
PRINTF = /usr/bin/printf
|
||||
PRINTF := `command -v printf`
|
||||
|
||||
# Options
|
||||
PREFIX ?= /usr/local
|
||||
|
@ -55,7 +55,8 @@ YELLOW ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 3)
|
|||
|
||||
.PHONY: meli
|
||||
meli: check-deps
|
||||
${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release --bin meli
|
||||
@echo ${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --release --bin meli
|
||||
@${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release --bin meli
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
|
@ -75,7 +76,8 @@ help:
|
|||
@echo " - ${BOLD}deb-dist${ANSI_RESET} (builds debian package in the parent directory)"
|
||||
@echo " - ${BOLD}distclean${ANSI_RESET} (cleans distribution build artifacts)"
|
||||
@echo " - ${BOLD}build-rustdoc${ANSI_RESET} (builds rustdoc documentation for all packages in \$$CARGO_TARGET_DIR)"
|
||||
@echo "\nENVIRONMENT variables of interest:"
|
||||
@echo ""
|
||||
@echo "ENVIRONMENT variables of interest:"
|
||||
@$(PRINTF) "* MELI_FEATURES "
|
||||
@[ -z $${MELI_FEATURES+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${MELI_FEATURES}${ANSI_RESET}
|
||||
@$(PRINTF) "* PREFIX "
|
||||
|
@ -89,20 +91,26 @@ help:
|
|||
@$(PRINTF) "* NO_MAN "
|
||||
@[ $${NO_MAN+x} ] && echo "set" || echo "unset"
|
||||
@$(PRINTF) "* NO_COLOR "
|
||||
@[ $${NO_COLOR+x} ] && echo "set" || echo "unset"
|
||||
@([ $${NO_COLOR+x} ] && [ "$${NO_COLOR}" != "" ] && echo "set") || echo "unset"
|
||||
@echo "* CARGO_BIN = ${UNDERLINE}${CARGO_BIN}${ANSI_RESET}"
|
||||
@$(PRINTF) "* CARGO_ARGS "
|
||||
@[ -z $${CARGO_ARGS+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${CARGO_ARGS}${ANSI_RESET}
|
||||
@([ -z "${CARGO_ARGS}" ] && echo "unset") || echo = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}
|
||||
@$(PRINTF) "* RUSTFLAGS = "
|
||||
@([ -z "${RUSTFLAGS}" ] && echo "unset") || echo = ${UNDERLINE}${RUSTFLAGS}${ANSI_RESET}
|
||||
@$(PRINTF) "* AUTHOR (for deb-dist) "
|
||||
@[ -z $${AUTHOR+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${AUTHOR}${ANSI_RESET}
|
||||
@echo "* MIN_RUSTC = ${UNDERLINE}${MIN_RUSTC}${ANSI_RESET}"
|
||||
@echo "* VERSION = ${UNDERLINE}${VERSION}${ANSI_RESET}"
|
||||
@echo "* GIT_COMMIT = ${UNDERLINE}${GIT_COMMIT}${ANSI_RESET}"
|
||||
@#@echo "* CARGO_COLOR = ${CARGO_COLOR}"
|
||||
@echo "* CARGO_TARGET_DIR = ${CARGO_TARGET_DIR}"
|
||||
@echo ""
|
||||
@echo "Built-in/binary utilities"
|
||||
@echo "* PRINTF = ${UNDERLINE}${PRINTF}${ANSI_RESET}"
|
||||
|
||||
.PHONY: check
|
||||
check: check-tagrefs
|
||||
RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
|
@ -111,15 +119,18 @@ fmt:
|
|||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test
|
||||
test: test-docs
|
||||
RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test-docs
|
||||
test-docs:
|
||||
RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --doc
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --doc
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --doc
|
||||
|
||||
.PHONY: test-feature-permutations
|
||||
test-feature-permutations:
|
||||
|
@ -204,7 +215,8 @@ deb-dist:
|
|||
|
||||
.PHONY: build-rustdoc
|
||||
build-rustdoc:
|
||||
RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open
|
||||
@echo RUSTDOCFLAGS=\""--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}"\" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" --all-features --no-deps --workspace --document-private-items --open
|
||||
@RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open
|
||||
|
||||
.PHONY: check-tagrefs
|
||||
check-tagrefs:
|
||||
|
|
|
@ -293,6 +293,11 @@ Path of
|
|||
.Xr mutt 1
|
||||
compatible alias file in the option
|
||||
They are parsed and imported read-only.
|
||||
.It Ic notmuch_address_book_query Ar String
|
||||
.Pq Em optional
|
||||
Query passed to
|
||||
.Qq Li notmuch address
|
||||
to import contacts into meli. Contacts are parsed and imported read-only.
|
||||
.It Ic mailboxes Ar mailbox
|
||||
.Pq Em optional
|
||||
Configuration for each mailbox.
|
||||
|
@ -987,7 +992,7 @@ Example:
|
|||
.Bd -literal
|
||||
[composing]
|
||||
editor_cmd = '~/.local/bin/vim +/^$'
|
||||
embed = true
|
||||
embedded_pty = true
|
||||
custom_compose_hooks = [ { name ="spellcheck", command="aspell --mode email --dont-suggest --ignore-case list" }]
|
||||
.Ed
|
||||
.\"
|
||||
|
|
|
@ -71,6 +71,13 @@ pub use themes::*;
|
|||
|
||||
pub use self::{composing::*, pgp::*, shortcuts::*, tags::*};
|
||||
|
||||
/// Utility macro to access an [`AccountConf`] setting field from
|
||||
/// [`Context`](crate::Context) indexed by `$account_hash`
|
||||
///
|
||||
/// The value returned is the optionally overriden one in the
|
||||
/// [`AccountConf::conf_override`] field, otherwise the global one.
|
||||
///
|
||||
/// See also the [`mailbox_settings`](crate::mailbox_settings) macro.
|
||||
#[macro_export]
|
||||
macro_rules! account_settings {
|
||||
($context:ident[$account_hash:expr].$setting:ident.$field:ident) => {{
|
||||
|
@ -87,6 +94,14 @@ macro_rules! account_settings {
|
|||
}};
|
||||
}
|
||||
|
||||
/// Utility macro to access an [`AccountConf`] setting field from
|
||||
/// [`Context`](crate::Context) indexed by `$account_hash` and a mailbox.
|
||||
///
|
||||
/// The value returned is the optionally overriden one in the
|
||||
/// [`FileMailboxConf::conf_override`] field, otherwise the
|
||||
/// [`AccountConf::conf_override`] field, otherwise the global one.
|
||||
///
|
||||
/// See also the [`account_settings`] macro.
|
||||
#[macro_export]
|
||||
macro_rules! mailbox_settings {
|
||||
($context:ident[$account_hash:expr][$mailbox_path:expr].$setting:ident.$field:ident) => {{
|
||||
|
|
|
@ -909,17 +909,41 @@ impl AutoComplete {
|
|||
}
|
||||
}
|
||||
|
||||
/// A widget that draws a scrollbar.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use meli::{Area, Component, CellBuffer, Context, utilities::ScrollBar};
|
||||
/// // Mock `Component::draw` impl
|
||||
/// fn draw(grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
/// let position = 0;
|
||||
/// let visible_rows = area.height();
|
||||
/// let scrollbar_area = area.nth_col(area.width());
|
||||
/// let total_rows = 100;
|
||||
/// ScrollBar::default().set_show_arrows(true).draw(
|
||||
/// grid,
|
||||
/// scrollbar_area,
|
||||
/// context,
|
||||
/// position,
|
||||
/// visible_rows,
|
||||
/// total_rows,
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct ScrollBar {
|
||||
pub show_arrows: bool,
|
||||
}
|
||||
|
||||
impl ScrollBar {
|
||||
/// Update `self.show_arrows` field.
|
||||
pub fn set_show_arrows(&mut self, new_val: bool) -> &mut Self {
|
||||
self.show_arrows = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Draw `self` vertically.
|
||||
pub fn draw(
|
||||
&self,
|
||||
grid: &mut CellBuffer,
|
||||
|
@ -976,6 +1000,7 @@ impl ScrollBar {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw `self` horizontally.
|
||||
pub fn draw_horizontal(
|
||||
&self,
|
||||
grid: &mut CellBuffer,
|
||||
|
@ -1033,6 +1058,82 @@ impl ScrollBar {
|
|||
}
|
||||
}
|
||||
|
||||
/// A widget that displays a customizable progress spinner.
|
||||
///
|
||||
/// It uses a [`Timer`](crate::jobs::Timer) and each time its timer fires, it
|
||||
/// cycles to the next stage of its `kind` sequence.
|
||||
///
|
||||
/// `kind` is an array of strings/string slices and an
|
||||
/// [`Duration` interval](std::time::Duration), and each item represents a stage
|
||||
/// or frame of the progress spinner. For example, a
|
||||
/// `(Duration::from_millis(130), &["-", "\\", "|", "/"])` value would cycle
|
||||
/// through the sequence `-`, `\`, `|`, `/`, `-`, `\`, `|` and so on roughly
|
||||
/// every 130 milliseconds.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::collections::HashSet;
|
||||
///
|
||||
/// use meli::{jobs::JobId, utilities::ProgressSpinner, Component, Context, StatusEvent, UIEvent};
|
||||
///
|
||||
/// struct JobMonitoringWidget {
|
||||
/// progress_spinner: ProgressSpinner,
|
||||
/// in_progress_jobs: HashSet<JobId>,
|
||||
/// }
|
||||
///
|
||||
/// impl JobMonitoringWidget {
|
||||
/// fn new(context: &Context, container: Box<dyn Component>) -> Self {
|
||||
/// let mut progress_spinner = ProgressSpinner::new(20, context);
|
||||
/// match context.settings.terminal.progress_spinner_sequence.as_ref() {
|
||||
/// Some(meli::conf::terminal::ProgressSpinnerSequence::Integer(k)) => {
|
||||
/// progress_spinner.set_kind(*k);
|
||||
/// }
|
||||
/// Some(meli::conf::terminal::ProgressSpinnerSequence::Custom {
|
||||
/// ref frames,
|
||||
/// ref interval_ms,
|
||||
/// }) => {
|
||||
/// progress_spinner.set_custom_kind(frames.clone(), *interval_ms);
|
||||
/// }
|
||||
/// None => {}
|
||||
/// }
|
||||
/// Self {
|
||||
/// progress_spinner,
|
||||
/// in_progress_jobs: Default::default(),
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Mock `Component::process_event` impl.
|
||||
/// fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
/// match event {
|
||||
/// UIEvent::StatusEvent(StatusEvent::JobCanceled(ref job_id))
|
||||
/// | UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) => {
|
||||
/// self.in_progress_jobs.remove(job_id);
|
||||
/// if self.in_progress_jobs.is_empty() {
|
||||
/// self.progress_spinner.stop();
|
||||
/// }
|
||||
/// self.progress_spinner.set_dirty(true);
|
||||
/// false
|
||||
/// }
|
||||
/// UIEvent::StatusEvent(StatusEvent::NewJob(ref job_id)) => {
|
||||
/// if self.in_progress_jobs.is_empty() {
|
||||
/// self.progress_spinner.start();
|
||||
/// }
|
||||
/// self.progress_spinner.set_dirty(true);
|
||||
/// self.in_progress_jobs.insert(*job_id);
|
||||
/// false
|
||||
/// }
|
||||
/// UIEvent::Timer(_) => {
|
||||
/// if self.progress_spinner.process_event(event, context) {
|
||||
/// return true;
|
||||
/// }
|
||||
/// false
|
||||
/// }
|
||||
/// _ => false,
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct ProgressSpinner {
|
||||
timer: crate::jobs::Timer,
|
||||
|
@ -1120,6 +1221,7 @@ impl ProgressSpinner {
|
|||
pub const INTERVAL_MS: u64 = 50;
|
||||
const INTERVAL: std::time::Duration = std::time::Duration::from_millis(Self::INTERVAL_MS);
|
||||
|
||||
/// See source code of [`Self::KINDS`].
|
||||
pub fn new(kind: usize, context: &Context) -> Self {
|
||||
let kind = kind % Self::KINDS.len();
|
||||
let width = Self::KINDS[kind]
|
||||
|
@ -1151,10 +1253,12 @@ impl ProgressSpinner {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
/// See source code of [`Self::KINDS`].
|
||||
pub fn set_kind(&mut self, kind: usize) {
|
||||
self.stage = 0;
|
||||
self.width = Self::KINDS[kind % Self::KINDS.len()]
|
||||
|
@ -1197,11 +1301,12 @@ impl ProgressSpinner {
|
|||
|
||||
impl std::fmt::Display for ProgressSpinner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "progress bar")
|
||||
write!(f, "progress spinner")
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ProgressSpinner {
|
||||
/// Draw current stage, if `self` is dirty.
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
if self.dirty {
|
||||
grid.clear_area(area, self.theme_attr);
|
||||
|
@ -1224,6 +1329,7 @@ impl Component for ProgressSpinner {
|
|||
}
|
||||
}
|
||||
|
||||
/// If the `event` is our timer firing, proceed to next stage.
|
||||
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
|
||||
match event {
|
||||
UIEvent::Timer(id) if *id == self.timer.id() => {
|
||||
|
|
|
@ -21,10 +21,70 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
//! Helping users move to newer `meli` versions.
|
||||
//!
|
||||
//! # How version information is stored in the filesystem and examined
|
||||
//!
|
||||
//! On start-up, `meli` checks the contents of `${XDG_DATA_HOME}/meli/.version`
|
||||
//! (the "version file") to determine if there has been a version upgrade since
|
||||
//! the last time it was launched, if any. Regardless of the version file
|
||||
//! existence or its contents, it will write the latest version string as
|
||||
//! recorded in the module `const` global [`LATEST`] **unless** the version
|
||||
//! file's content match `LATEST`.
|
||||
//!
|
||||
//! [`LATEST`] is verified to contain the actual version at compile-time using
|
||||
//! Cargo's environment variable `CARGO_PKG_VERSION`.
|
||||
//!
|
||||
//! If the version file does not exist, no migrations need to be performed.
|
||||
//!
|
||||
//! If the version file is determined to be a previous version,
|
||||
//! [`calculate_migrations`] is called which examines every migration in the
|
||||
//! version range starting from the previous version up to the latest. If any
|
||||
//! migration is applicable, it asks the user interactively whether to perform
|
||||
//! them. This happens in [`version_setup`].
|
||||
//!
|
||||
//! # How `meli` encodes version information statically with types and modules
|
||||
//!
|
||||
//! Every release **MUST** have a module associated with it. The module
|
||||
//! contains:
|
||||
//!
|
||||
//! - a public [`VersionIdentifier`] `const` item
|
||||
//! - a `struct` that represents the version, named by convention `VX_Y_Z[..]`.
|
||||
//! The `struct` definition can be empty (e.g. `pub struct V0_0_1;`). The
|
||||
//! `struct` **MUST** implement the [`Version`] trait, which can be used to
|
||||
//! retrieve the version identifier and the migrations.
|
||||
//! - Any number of structs representing migrations, which implement the
|
||||
//! [`Migration`] trait, and which are returned by the [`Version::migrations`]
|
||||
//! method.
|
||||
//!
|
||||
//! All versions must be stored in an `IndexMap` (type alias [`VersionMap`])
|
||||
//! which is retrieved by the function [`versions`].
|
||||
//!
|
||||
//! # How migrations work
|
||||
//!
|
||||
//! Migrations are **not** guaranteed to be lossless; stored metadata
|
||||
//! information in the filesystem may be lost.
|
||||
//!
|
||||
//! Migrations can optionally claim they are not applicable, which means they
|
||||
//! will be skipped entirely if migrations are to be applied. The check is done
|
||||
//! in the [`Migration::is_applicable`] trait method which can opt-in to make
|
||||
//! checks in the configuration file.
|
||||
//!
|
||||
//! Migrations can be performed using the [`Migration::perform`] method, which
|
||||
//! can optionally attempt to make a "dry run" application, which can check for
|
||||
//! errors but not make any actual changes in the filesystem.
|
||||
//!
|
||||
//! If a migration can be reverted, it can implement the revert logic in
|
||||
//! [`Migration::revert`] which follows the same logic as [`Migration::perform`]
|
||||
//! but in reverse. It is not always possible a migration can be reverted, since
|
||||
//! migrations are not necessarily lossless.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// A container for [`Version`]s indexed by their [`VersionIdentifier`].
|
||||
///
|
||||
/// Internally it is returned by the [`versions`] function in this
|
||||
/// module.
|
||||
pub type VersionMap = IndexMap<VersionIdentifier, Box<dyn Version + Send + Sync + 'static>>;
|
||||
|
||||
/// Utility macro to define version module imports and a function `versions() ->
|
||||
|
@ -80,7 +140,10 @@ pub type VersionMap = IndexMap<VersionIdentifier, Box<dyn Version + Send + Sync
|
|||
#[macro_export]
|
||||
macro_rules! decl_version_map {
|
||||
($($version_id:path => $m:ident::$v:ident),*$(,)?) => {
|
||||
fn versions() -> &'static VersionMap {
|
||||
/// Return all versions in a [`VersionMap`] container.
|
||||
///
|
||||
/// The value is lazily initialized on first access.
|
||||
pub fn versions() -> &'static VersionMap {
|
||||
use std::sync::OnceLock;
|
||||
#[allow(dead_code)]
|
||||
const fn const_bytes_cmp(lhs: &[u8], rhs: &[u8]) -> std::cmp::Ordering {
|
||||
|
@ -180,6 +243,8 @@ macro_rules! decl_version_map {
|
|||
};
|
||||
}
|
||||
|
||||
/// Wrapper macro over [`decl_version_map`] that also defines the arguments as
|
||||
/// modules.
|
||||
macro_rules! decl_version_mods {
|
||||
($($version_id:path => $m:ident::$v:ident),*$(,)?) => {
|
||||
$(
|
||||
|
@ -209,9 +274,20 @@ use melib::{error::*, log};
|
|||
|
||||
use crate::{conf::FileSettings, terminal::Ask};
|
||||
|
||||
/// The latest version as defined in the Cargo manifest file of `meli`.
|
||||
///
|
||||
/// On compile-time if the `CARGO_PKG_VERSION` environment variable is
|
||||
/// available, the macro [`decl_version_map`] asserts that it matches the actual
|
||||
/// latest version string.
|
||||
pub const LATEST: VersionIdentifier = v0_8_10::V0_8_10_ID;
|
||||
|
||||
/// An application version identifier.
|
||||
/// An application version identifier with [Semantic Versioning v2.0.0]
|
||||
/// semantics.
|
||||
///
|
||||
/// There's no support for "Build metadata" of the specification since we're not
|
||||
/// using those.
|
||||
///
|
||||
/// [Semantic Versioning v2.0.0]: https://semver.org/spec/v2.0.0.html
|
||||
#[derive(Clone, Copy, Debug, Eq)]
|
||||
pub struct VersionIdentifier {
|
||||
string: &'static str,
|
||||
|
@ -222,6 +298,8 @@ pub struct VersionIdentifier {
|
|||
}
|
||||
|
||||
impl VersionIdentifier {
|
||||
/// An invalid non-existent release, `v0.0.0`, used for comparison with
|
||||
/// other identifiers.
|
||||
pub const NULL: Self = Self {
|
||||
string: "0.0.0",
|
||||
major: 0,
|
||||
|
@ -262,6 +340,8 @@ impl std::fmt::Display for VersionIdentifier {
|
|||
}
|
||||
}
|
||||
|
||||
/// Compare `(self.major, self.minor, self.patch, self.pre)` fields with another
|
||||
/// [`VersionIdentifier`].
|
||||
impl Ord for VersionIdentifier {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
(self.major, self.minor, self.patch, self.pre).cmp(&(
|
||||
|
@ -273,6 +353,8 @@ impl Ord for VersionIdentifier {
|
|||
}
|
||||
}
|
||||
|
||||
/// Compare `(self.major, self.minor, self.patch, self.pre)` fields with another
|
||||
/// [`VersionIdentifier`].
|
||||
impl PartialOrd for VersionIdentifier {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
|
@ -351,6 +433,8 @@ impl std::fmt::Debug for dyn Migration + Send + Sync {
|
|||
|
||||
/// Return the path to the `.version` file, a plain text file that contains the
|
||||
/// version of meli that "owns" the configuration and data files.
|
||||
///
|
||||
/// The actual path examined is `${XDG_DATA_HOME}/meli/.version`.
|
||||
pub fn version_file() -> Result<PathBuf> {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli")?;
|
||||
Ok(xdg_dirs.place_data_file(".version")?)
|
||||
|
@ -358,6 +442,10 @@ pub fn version_file() -> Result<PathBuf> {
|
|||
|
||||
/// Inspect current/previous version setup, perform migrations if necessary,
|
||||
/// etc.
|
||||
///
|
||||
/// This function requires an interactive user session, if stdout is not an
|
||||
/// interactive TTY, the process caller must ensure `stdin` contains the
|
||||
/// necessary input (`y`, `n`, newline) otherwise this function _blocks_.
|
||||
pub fn version_setup(
|
||||
config: &Path,
|
||||
writer: &mut impl std::io::Write,
|
||||
|
|
|
@ -20,8 +20,11 @@
|
|||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
//! <https://release.meli-email.org/v0.8.10>
|
||||
|
||||
use crate::version_migrations::*;
|
||||
|
||||
/// <https://release.meli-email.org/v0.8.10>
|
||||
pub const V0_8_10_ID: VersionIdentifier = VersionIdentifier {
|
||||
string: "0.8.10",
|
||||
major: 0,
|
||||
|
@ -30,6 +33,7 @@ pub const V0_8_10_ID: VersionIdentifier = VersionIdentifier {
|
|||
pre: "",
|
||||
};
|
||||
|
||||
/// <https://release.meli-email.org/v0.8.10>
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct V0_8_10;
|
||||
|
||||
|
|
|
@ -20,8 +20,11 @@
|
|||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
//! <https://release.meli-email.org/v0.8.8>
|
||||
|
||||
use crate::version_migrations::*;
|
||||
|
||||
/// <https://release.meli-email.org/v0.8.8>
|
||||
pub const V0_8_8_ID: VersionIdentifier = VersionIdentifier {
|
||||
string: "0.8.8",
|
||||
major: 0,
|
||||
|
@ -30,6 +33,7 @@ pub const V0_8_8_ID: VersionIdentifier = VersionIdentifier {
|
|||
pre: "",
|
||||
};
|
||||
|
||||
/// <https://release.meli-email.org/v0.8.8>
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct V0_8_8;
|
||||
|
||||
|
@ -43,6 +47,9 @@ impl Version for V0_8_8 {
|
|||
}
|
||||
}
|
||||
|
||||
/// Rename `addressbook` to `contacts`.
|
||||
///
|
||||
/// "The storage file for contacts, stored in the application's data folder, was renamed from `addressbook` to `contacts` to better reflect its purpose."
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct AddressbookRename;
|
||||
|
||||
|
|
|
@ -20,8 +20,11 @@
|
|||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
//! <https://release.meli-email.org/v0.8.9>
|
||||
|
||||
use crate::version_migrations::*;
|
||||
|
||||
/// <https://release.meli-email.org/v0.8.9>
|
||||
pub const V0_8_9_ID: VersionIdentifier = VersionIdentifier {
|
||||
string: "0.8.9",
|
||||
major: 0,
|
||||
|
@ -30,6 +33,7 @@ pub const V0_8_9_ID: VersionIdentifier = VersionIdentifier {
|
|||
pre: "",
|
||||
};
|
||||
|
||||
/// <https://release.meli-email.org/v0.8.9>
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct V0_8_9;
|
||||
|
||||
|
|
|
@ -97,6 +97,12 @@ impl AccountSettings {
|
|||
self.extra.get("vcard_folder").map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn notmuch_address_book_query(&self) -> Option<&str> {
|
||||
self.extra
|
||||
.get("notmuch_address_book_query")
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
/// Get the server password, either directly from the `server_password`
|
||||
/// settings value, or by running the `server_password_command` and reading
|
||||
/// the output.
|
||||
|
@ -152,6 +158,7 @@ impl AccountSettings {
|
|||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
}
|
||||
_ = self.extra.swap_remove("notmuch_address_book_query");
|
||||
}
|
||||
{
|
||||
if let Some(mutt_alias_file) = self.extra.swap_remove("mutt_alias_file") {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
pub mod jscontact;
|
||||
pub mod mutt;
|
||||
pub mod notmuchcontact;
|
||||
pub mod vcard;
|
||||
|
||||
mod card;
|
||||
|
@ -143,6 +144,60 @@ impl Contacts {
|
|||
}
|
||||
}
|
||||
}
|
||||
use std::process::Command;
|
||||
if let Some(notmuch_addressbook_query) = s.notmuch_address_book_query() {
|
||||
match Command::new("sh")
|
||||
.args([
|
||||
"-c",
|
||||
&format!(
|
||||
"notmuch address --format=json {}",
|
||||
notmuch_addressbook_query
|
||||
),
|
||||
])
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.output()
|
||||
{
|
||||
Ok(notmuch_addresses) => {
|
||||
if notmuch_addresses.status.success() {
|
||||
match std::str::from_utf8(¬much_addresses.stdout) {
|
||||
Ok(notmuch_address_out) => {
|
||||
match notmuchcontact::parse_notmuch_contacts(notmuch_address_out) {
|
||||
Ok(contacts) => {
|
||||
for c in contacts {
|
||||
ret.add_card(c.clone());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"Unable to parse notmuch contact result into cards: {} {}",
|
||||
notmuch_address_out,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"Unable to read from notmuch address query: {} {}",
|
||||
notmuch_addressbook_query,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Error ({}) running notmuch address: {} {}",
|
||||
notmuch_addresses.status,
|
||||
String::from_utf8_lossy(¬much_addresses.stdout),
|
||||
String::from_utf8_lossy(¬much_addresses.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => log::warn!("Unable to run notmuch address command: {}", e),
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
|
|
63
melib/src/contacts/notmuchcontact.rs
Normal file
63
melib/src/contacts/notmuchcontact.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// 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 crate::{contacts::Card, error::Result};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct NotmuchContact {
|
||||
pub name: String,
|
||||
pub address: String,
|
||||
#[serde(rename = "name-addr")]
|
||||
pub name_addr: String,
|
||||
}
|
||||
|
||||
pub fn parse_notmuch_contacts(input: &str) -> Result<Vec<Card>> {
|
||||
let mut cards = Vec::new();
|
||||
let abook = serde_json::from_str::<Vec<NotmuchContact>>(input)?;
|
||||
|
||||
for c in abook.iter() {
|
||||
cards.push(
|
||||
Card::new()
|
||||
.set_title(c.name_addr.clone())
|
||||
.set_email(c.address.clone())
|
||||
.set_name(c.name.clone())
|
||||
.set_external_resource(true)
|
||||
.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(cards)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_addressbook_notmuchcontact() {
|
||||
let cards = parse_notmuch_contacts(
|
||||
r#"[{"name": "Full Name", "address": "user@example.com", "name-addr": "Full Name <user@example.com>"},
|
||||
{"name": "Full2 Name", "address": "user2@example.com", "name-addr": "Full2 Name <user2@example.com>"}]"#
|
||||
).unwrap();
|
||||
assert_eq!(cards[0].name(), "Full Name");
|
||||
assert_eq!(cards[0].title(), "Full Name <user@example.com>");
|
||||
assert_eq!(cards[0].email(), "user@example.com");
|
||||
assert_eq!(cards[1].name(), "Full2 Name");
|
||||
assert_eq!(cards[1].title(), "Full2 Name <user2@example.com>");
|
||||
assert_eq!(cards[1].email(), "user2@example.com");
|
||||
}
|
Loading…
Add table
Reference in a new issue