Create initial prototype of `fluent_embed_interaction`
Dependencies
- [2]
LYZBTYIWReplace `proc-macro-error` with `proc-macro-error2` - [3]
TIPBMFLWMigrate to edition 2024 - [4]
VZYZRAO4Move `output-macros` crate into workspace - [5]
KZLFC7OWRename `fluent_embed_runtime` to `fluent_embed` - [6]
HHJDRLLNCreate `fluent_embed_runtime` crate - [7]
SHNZZSZGCreate `cli_macros` shim crate - [8]
7M4UI3TWUpdate dependencies to latest versions - [9]
O77KA6C4Create `fluent_embed` crate - [10]
VQBJBFEXImprove error handling for missing Fluent messages - [11]
UN2XEIEUMigrate from `locale_select` to `env_preferences` - [12]
OWXLFLRMMerge `cli_macros` shim into `fluent_embed` - [13]
AL3CCMWZRemove deprecated `output-macros` crate - [14]
YNEOCYMGCreate `locale-select` crate - [15]
XGRU7WZEAdd `expand` feature for proc-macro debugging - [*]
KDUI7LHJ - [*]
UKFEFT6LCreate basic `Output` proc-macro
Change contents
- file addition: fluent_embed_interaction[17.1]
- file addition: src[0.36]
- file addition: prompt[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 messagelet 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 usepub(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 = truefluent_embed = { path = "../fluent_embed" }icu_locale.workspace = truethiserror.workspace = true - replacement in Cargo.toml at line 2
members = ["fluent_embed", "fluent_embed_derive"]members = ["fluent_embed", "fluent_embed_derive", "fluent_embed_interaction"] - edit in Cargo.toml at line 6
dialoguer = { version = "0.11.0", default-features = false, features = ["editor","fuzzy-select","password",] } - edit in Cargo.lock at line 104
[[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
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
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
[[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
][[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
][[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
"wasi","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
[[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
[[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
"getrandom","getrandom 0.2.15", - replacement in Cargo.lock at line 1317
"linux-raw-sys","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
[[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
][[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
"rustix","rustix 0.38.44", - edit in Cargo.lock at line 1532
][[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
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
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
name = "zeroize"version = "1.8.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"[[package]]