Create initial prototype of `fluent_embed_interaction`

finchie
May 29, 2025, 7:30 AM
JUV7C6ET4ZQJNLO7B4JB7XMLV2YUBZB4ARJHN4JEPHDGGBWGLEVAC

Dependencies

  • [2] LYZBTYIW Replace `proc-macro-error` with `proc-macro-error2`
  • [3] TIPBMFLW Migrate to edition 2024
  • [4] VZYZRAO4 Move `output-macros` crate into workspace
  • [5] KZLFC7OW Rename `fluent_embed_runtime` to `fluent_embed`
  • [6] HHJDRLLN Create `fluent_embed_runtime` crate
  • [7] SHNZZSZG Create `cli_macros` shim crate
  • [8] 7M4UI3TW Update dependencies to latest versions
  • [9] O77KA6C4 Create `fluent_embed` crate
  • [10] VQBJBFEX Improve error handling for missing Fluent messages
  • [11] UN2XEIEU Migrate from `locale_select` to `env_preferences`
  • [12] OWXLFLRM Merge `cli_macros` shim into `fluent_embed`
  • [13] AL3CCMWZ Remove deprecated `output-macros` crate
  • [14] YNEOCYMG Create `locale-select` crate
  • [15] XGRU7WZE Add `expand` feature for proc-macro debugging
  • [*] KDUI7LHJ
  • [*] UKFEFT6L Create basic `Output` proc-macro

Change contents

  • file addition: fluent_embed_interaction (d--r------)
    [17.1]
  • file addition: src (d--r------)
    [0.36]
  • file addition: prompt (d--r------)
    [0.53]
  • file addition: select.rs (----------)
    [0.73]
    use super::macros::{impl_new, impl_with_default, impl_with_prompt};
    use crate::{InteractionEnvironment, InteractionError, NON_INTERACTIVE_MESSAGE};
    use fluent_embed::{LocalizationError, Localize};
    #[derive(Default)]
    pub struct Select {
    default: Option<usize>,
    items: Option<Vec<String>>,
    prompt: Option<String>,
    }
    impl_new!(Select);
    impl_with_default!(Select, usize);
    impl_with_prompt!(Select);
    impl Select {
    pub fn with_items<L: Localize>(mut self, items: &[L]) -> Result<Self, LocalizationError> {
    let localized_items = items
    .iter()
    .map(|item| {
    let mut buffer = Vec::new();
    item.localize(&mut buffer).map(|_| buffer)
    })
    .collect::<Result<Vec<Vec<u8>>, LocalizationError>>()?
    .into_iter()
    .map(|item| String::from_utf8(item).map_err(LocalizationError::InvalidOutput))
    .collect::<Result<Vec<String>, LocalizationError>>()?;
    self.items = Some(localized_items);
    Ok(self)
    }
    pub fn interact(self, environment: &InteractionEnvironment) -> Result<usize, InteractionError> {
    match environment.context {
    crate::InteractionContext::Terminal => {
    let mut prompt = dialoguer::FuzzySelect::with_theme(&*crate::THEME);
    if let Some(default) = self.default {
    prompt = prompt.default(default);
    }
    if let Some(items) = self.items {
    prompt = prompt.items(&items);
    }
    if let Some(prompt_text) = self.prompt {
    prompt = prompt.with_prompt(prompt_text);
    }
    Ok(prompt.interact()?)
    }
    crate::InteractionContext::NonInteractive => panic!("{NON_INTERACTIVE_MESSAGE}"),
    }
    }
    }
  • file addition: password.rs (----------)
    [0.73]
    use super::macros::{impl_new, impl_with_prompt, impl_with_validator};
    use crate::{InteractionEnvironment, InteractionError, NON_INTERACTIVE_MESSAGE};
    use fluent_embed::{LocalizationError, Localize};
    struct PasswordConfirmation {
    prompt: String,
    mismatch_error: String,
    }
    #[derive(Default)]
    pub struct Password {
    confirmation: Option<PasswordConfirmation>,
    prompt: Option<String>,
    validator: Option<Box<dyn Fn(&String) -> Result<(), String>>>,
    }
    impl_new!(Password);
    impl_with_prompt!(Password);
    impl_with_validator!(Password);
    impl Password {
    pub fn with_confirmation<L1: Localize, L2: Localize>(
    mut self,
    confirmation: L1,
    mismatch_error: L2,
    ) -> Result<Self, LocalizationError> {
    let mut buffer = Vec::new();
    confirmation.localize(&mut buffer)?;
    let confirmation_text = String::from_utf8(buffer)?;
    let mut buffer = Vec::new();
    mismatch_error.localize(&mut buffer)?;
    let mismatch_error_text = String::from_utf8(buffer)?;
    self.confirmation = Some(PasswordConfirmation {
    prompt: confirmation_text,
    mismatch_error: mismatch_error_text,
    });
    Ok(self)
    }
    pub fn interact(
    self,
    environment: &InteractionEnvironment,
    ) -> Result<String, InteractionError> {
    match environment.context {
    crate::InteractionContext::Terminal => {
    let mut prompt = dialoguer::Password::with_theme(&*crate::THEME);
    if let Some(confirmation) = self.confirmation {
    prompt =
    prompt.with_confirmation(confirmation.prompt, confirmation.mismatch_error);
    }
    if let Some(prompt_text) = self.prompt {
    prompt = prompt.with_prompt(prompt_text);
    }
    if let Some(validator) = self.validator {
    prompt = prompt.validate_with(validator);
    }
    Ok(prompt.interact()?)
    }
    crate::InteractionContext::NonInteractive => panic!("{NON_INTERACTIVE_MESSAGE}"),
    }
    }
    }
  • file addition: mod.rs (----------)
    [0.73]
    mod confirm;
    mod input;
    mod macros;
    mod password;
    mod select;
    pub use confirm::Confirm;
    pub use input::Input;
    pub use password::Password;
    pub use select::Select;
  • file addition: macros.rs (----------)
    [0.73]
    macro_rules! impl_new {
    ($newtype:ident) => {
    impl $newtype {
    pub fn new() -> Self {
    Self::default()
    }
    }
    };
    }
    macro_rules! impl_with_default {
    ($newtype:ident, $field_type:ty) => {
    impl $newtype {
    pub fn with_default(mut self, default: $field_type) -> Self {
    self.default = Some(default);
    self
    }
    }
    };
    }
    macro_rules! impl_with_prompt {
    ($newtype:ident) => {
    impl $newtype {
    pub fn with_prompt<L: Localize>(
    mut self,
    prompt: L,
    ) -> Result<Self, LocalizationError> {
    let mut buffer = Vec::new();
    prompt.localize(&mut buffer)?;
    let localized_text = String::from_utf8(buffer)?;
    self.prompt = Some(localized_text);
    Ok(self)
    }
    }
    };
    }
    macro_rules! impl_with_validator {
    ($newtype:ident) => {
    impl $newtype {
    pub fn with_validator<L: Localize, V: Fn(&String) -> Result<(), L> + 'static>(
    mut self,
    validator: V,
    ) -> Self {
    self.validator = Some(Box::new(move |input: &String| -> Result<(), String> {
    match validator(input) {
    Ok(()) => Ok(()),
    Err(message) => {
    // Localize the error message
    let mut buffer = Vec::new();
    message
    .localize(&mut buffer)
    .map_err(|error| error.to_string())?;
    Err(String::from_utf8_lossy(&buffer).to_string())
    }
    }
    }));
    self
    }
    }
    };
    }
    // Re-export the macro helpers for other modules to use
    pub(crate) use {impl_new, impl_with_default, impl_with_prompt, impl_with_validator};
  • file addition: input.rs (----------)
    [0.73]
    use super::macros::{impl_new, impl_with_prompt, impl_with_validator};
    use crate::{InteractionEnvironment, InteractionError, NON_INTERACTIVE_MESSAGE};
    use fluent_embed::{LocalizationError, Localize};
    #[derive(Default)]
    pub struct Input {
    default: Option<String>,
    prompt: Option<String>,
    validator: Option<Box<dyn Fn(&String) -> Result<(), String>>>,
    }
    impl_new!(Input);
    impl_with_prompt!(Input);
    impl_with_validator!(Input);
    impl Input {
    pub fn with_default<L: Localize>(mut self, default: L) -> Result<Self, LocalizationError> {
    let mut buffer = Vec::new();
    default.localize(&mut buffer)?;
    let localized_text = String::from_utf8(buffer)?;
    self.default = Some(localized_text);
    Ok(self)
    }
    pub fn interact(
    self,
    environment: &InteractionEnvironment,
    ) -> Result<String, InteractionError> {
    match environment.context {
    crate::InteractionContext::Terminal => {
    let mut prompt = dialoguer::Input::with_theme(&*crate::THEME);
    if let Some(default) = self.default {
    prompt = prompt.default(default);
    }
    if let Some(prompt_text) = self.prompt {
    prompt = prompt.with_prompt(prompt_text);
    }
    if let Some(validator) = self.validator {
    prompt = prompt.validate_with(validator);
    }
    Ok(prompt.interact()?)
    }
    crate::InteractionContext::NonInteractive => panic!("{NON_INTERACTIVE_MESSAGE}"),
    }
    }
    }
  • file addition: confirm.rs (----------)
    [0.73]
    use super::macros::{impl_new, impl_with_default, impl_with_prompt};
    use crate::{InteractionEnvironment, InteractionError, NON_INTERACTIVE_MESSAGE};
    use fluent_embed::{LocalizationError, Localize};
    #[derive(Default)]
    pub struct Confirm {
    default: Option<bool>,
    prompt: Option<String>,
    }
    impl_new!(Confirm);
    impl_with_default!(Confirm, bool);
    impl_with_prompt!(Confirm);
    impl Confirm {
    pub fn interact(self, environment: &InteractionEnvironment) -> Result<bool, InteractionError> {
    match environment.context {
    crate::InteractionContext::Terminal => {
    let mut prompt = dialoguer::Confirm::with_theme(&*crate::THEME);
    if let Some(default) = self.default {
    prompt = prompt.default(default);
    }
    if let Some(prompt_text) = self.prompt {
    prompt = prompt.with_prompt(prompt_text);
    }
    Ok(prompt.interact()?)
    }
    crate::InteractionContext::NonInteractive => panic!("{NON_INTERACTIVE_MESSAGE}"),
    }
    }
    }
  • file addition: lib.rs (----------)
    [0.53]
    mod editor;
    mod prompt;
    use std::sync::LazyLock;
    use thiserror::Error;
    pub use editor::Editor;
    pub use prompt::*;
    static THEME: LazyLock<dialoguer::theme::ColorfulTheme> =
    LazyLock::new(dialoguer::theme::ColorfulTheme::default);
    const NON_INTERACTIVE_MESSAGE: &str = "Attempted to prompt the user in a non-interactive context";
    enum InteractionContext {
    Terminal,
    NonInteractive,
    }
    #[derive(Debug, Error)]
    pub enum InteractionError {
    #[error(transparent)]
    IO(std::io::Error),
    }
    impl From<dialoguer::Error> for InteractionError {
    fn from(value: dialoguer::Error) -> Self {
    match value {
    dialoguer::Error::IO(error) => InteractionError::IO(error),
    }
    }
    }
    pub struct InteractionEnvironment {
    context: InteractionContext,
    }
    impl InteractionEnvironment {
    pub fn new(interactive: bool) -> Self {
    Self {
    context: match interactive {
    true => InteractionContext::Terminal,
    false => InteractionContext::NonInteractive,
    },
    }
    }
    }
  • file addition: editor.rs (----------)
    [0.53]
    use crate::{
    InteractionContext, InteractionEnvironment, InteractionError, NON_INTERACTIVE_MESSAGE,
    };
    pub struct Editor {
    extension: Option<String>,
    }
    impl Editor {
    pub fn new() -> Self {
    Self { extension: None }
    }
    pub fn with_extension(mut self, extension: &str) -> Self {
    self.extension = Some(extension.to_string());
    self
    }
    pub fn edit(
    &self,
    environment: &InteractionEnvironment,
    text: &str,
    ) -> Result<Option<String>, InteractionError> {
    match environment.context {
    InteractionContext::Terminal => {
    let mut editor = dialoguer::Editor::new();
    if let Some(extension) = &self.extension {
    editor.extension(extension);
    }
    Ok(editor.edit(text)?)
    }
    InteractionContext::NonInteractive => panic!("{NON_INTERACTIVE_MESSAGE}"),
    }
    }
    }
  • file addition: Cargo.toml (----------)
    [0.36]
    [package]
    name = "fluent_embed_interaction"
    version = "0.1.0"
    edition = "2024"
    [lints]
    workspace = true
    [dependencies]
    dialoguer.workspace = true
    fluent_embed = { path = "../fluent_embed" }
    icu_locale.workspace = true
    thiserror.workspace = true
  • replacement in Cargo.toml at line 2
    [4.210][4.1581:1631]()
    members = ["fluent_embed", "fluent_embed_derive"]
    [4.210]
    [3.36]
    members = ["fluent_embed", "fluent_embed_derive", "fluent_embed_interaction"]
  • edit in Cargo.toml at line 6
    [4.5579]
    [4.5579]
    dialoguer = { version = "0.11.0", default-features = false, features = [
    "editor",
    "fuzzy-select",
    "password",
    ] }
  • edit in Cargo.lock at line 104
    [4.5759]
    [4.5759]
    [[package]]
    name = "console"
    version = "0.15.11"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
    dependencies = [
    "encode_unicode",
    "libc",
    "once_cell",
    "unicode-width 0.2.0",
    "windows-sys",
    ]
  • edit in Cargo.lock at line 164
    [4.6712]
    [4.6712]
    name = "dialoguer"
    version = "0.11.0"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
    dependencies = [
    "console",
    "fuzzy-matcher",
    "shell-words",
    "tempfile",
    "thiserror 1.0.69",
    "zeroize",
    ]
    [[package]]
  • edit in Cargo.lock at line 223
    [4.7611]
    [4.7611]
    name = "encode_unicode"
    version = "1.0.0"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
    [[package]]
  • edit in Cargo.lock at line 270
    [4.1268]
    [4.1268]
    [[package]]
    name = "fastrand"
    version = "2.3.0"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
  • edit in Cargo.lock at line 361
    [4.9796]
    [4.1514]
    ]
    [[package]]
    name = "fluent_embed_interaction"
    version = "0.1.0"
    dependencies = [
    "dialoguer",
    "fluent_embed",
    "icu_locale",
    "thiserror 2.0.12",
  • edit in Cargo.lock at line 423
    [4.10957]
    [4.1737]
    ]
    [[package]]
    name = "fuzzy-matcher"
    version = "0.3.7"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
    dependencies = [
    "thread_local",
  • replacement in Cargo.lock at line 452
    [4.11190][4.11190:11199]()
    "wasi",
    [4.11190]
    [4.11199]
    "wasi 0.11.0+wasi-snapshot-preview1",
    ]
    [[package]]
    name = "getrandom"
    version = "0.3.3"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
    dependencies = [
    "cfg-if",
    "libc",
    "r-efi",
    "wasi 0.14.2+wasi-0.2.4",
  • edit in Cargo.lock at line 876
    [4.22595]
    [4.22595]
    [[package]]
    name = "linux-raw-sys"
    version = "0.9.4"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
  • edit in Cargo.lock at line 1180
    [4.30095]
    [4.30095]
    [[package]]
    name = "r-efi"
    version = "5.2.0"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
  • replacement in Cargo.lock at line 1214
    [4.30794][4.30794:30808]()
    "getrandom",
    [4.30794]
    [4.30808]
    "getrandom 0.2.15",
  • replacement in Cargo.lock at line 1317
    [4.33298][4.33298:33316]()
    "linux-raw-sys",
    [4.33298]
    [4.33316]
    "linux-raw-sys 0.4.15",
    "windows-sys",
    ]
    [[package]]
    name = "rustix"
    version = "1.0.7"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
    dependencies = [
    "bitflags",
    "errno",
    "libc",
    "linux-raw-sys 0.9.4",
  • edit in Cargo.lock at line 1374
    [4.34426]
    [4.34426]
    [[package]]
    name = "shell-words"
    version = "1.1.0"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
  • edit in Cargo.lock at line 1459
    [4.36654]
    [4.36654]
    ]
    [[package]]
    name = "tempfile"
    version = "3.20.0"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
    dependencies = [
    "fastrand",
    "getrandom 0.3.3",
    "once_cell",
    "rustix 1.0.7",
    "windows-sys",
  • replacement in Cargo.lock at line 1480
    [4.36870][4.36870:36881]()
    "rustix",
    [4.36870]
    [4.36881]
    "rustix 0.38.44",
  • edit in Cargo.lock at line 1532
    [2.618]
    [4.38151]
    ]
    [[package]]
    name = "thread_local"
    version = "1.1.8"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
    dependencies = [
    "cfg-if",
    "once_cell",
  • edit in Cargo.lock at line 1645
    [4.40906]
    [4.40906]
    name = "wasi"
    version = "0.14.2+wasi-0.2.4"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
    dependencies = [
    "wit-bindgen-rt",
    ]
    [[package]]
  • edit in Cargo.lock at line 1885
    [4.45274]
    [4.45274]
    name = "wit-bindgen-rt"
    version = "0.39.0"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
    dependencies = [
    "bitflags",
    ]
    [[package]]
  • edit in Cargo.lock at line 2027
    [4.48812]
    [4.48812]
    name = "zeroize"
    version = "1.8.1"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
    [[package]]