use crate::{
reporter::{MaxProgressRunning, ShowProgress},
user_config::helpers::resolve_ui_setting,
};
use serde::{
Deserialize, Deserializer,
de::{self, Unexpected},
};
use std::{collections::BTreeMap, fmt, process::Command};
use target_spec::{Platform, TargetSpec};
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(in crate::user_config) struct DeserializedUiConfig {
pub(in crate::user_config) show_progress: Option<UiShowProgress>,
#[serde(default, deserialize_with = "deserialize_max_progress_running")]
max_progress_running: Option<MaxProgressRunning>,
input_handler: Option<bool>,
output_indent: Option<bool>,
#[serde(default)]
pager: Option<PagerSetting>,
#[serde(default)]
paginate: Option<PaginateSetting>,
#[serde(default)]
streampager: DeserializedStreampagerConfig,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct DefaultUiConfig {
show_progress: UiShowProgress,
#[serde(deserialize_with = "deserialize_max_progress_running_required")]
max_progress_running: MaxProgressRunning,
input_handler: bool,
output_indent: bool,
pub(in crate::user_config) pager: PagerSetting,
pub(in crate::user_config) paginate: PaginateSetting,
pub(in crate::user_config) streampager: DefaultStreampagerConfig,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(in crate::user_config) struct DeserializedUiOverrideData {
pub(in crate::user_config) show_progress: Option<UiShowProgress>,
#[serde(default, deserialize_with = "deserialize_max_progress_running")]
pub(in crate::user_config) max_progress_running: Option<MaxProgressRunning>,
pub(in crate::user_config) input_handler: Option<bool>,
pub(in crate::user_config) output_indent: Option<bool>,
#[serde(default)]
pub(in crate::user_config) pager: Option<PagerSetting>,
#[serde(default)]
pub(in crate::user_config) paginate: Option<PaginateSetting>,
#[serde(default)]
pub(in crate::user_config) streampager: DeserializedStreampagerConfig,
}
#[derive(Clone, Debug)]
pub(in crate::user_config) struct CompiledUiOverride {
platform_spec: TargetSpec,
data: UiOverrideData,
}
impl CompiledUiOverride {
pub(in crate::user_config) fn new(
platform_spec: TargetSpec,
data: DeserializedUiOverrideData,
) -> Self {
Self {
platform_spec,
data: UiOverrideData {
show_progress: data.show_progress,
max_progress_running: data.max_progress_running,
input_handler: data.input_handler,
output_indent: data.output_indent,
pager: data.pager,
paginate: data.paginate,
streampager_interface: data.streampager.interface,
streampager_wrapping: data.streampager.wrapping,
streampager_show_ruler: data.streampager.show_ruler,
},
}
}
pub(in crate::user_config) fn matches(&self, host_platform: &Platform) -> bool {
self.platform_spec
.eval(host_platform)
.unwrap_or( false)
}
pub(in crate::user_config) fn data(&self) -> &UiOverrideData {
&self.data
}
}
#[derive(Clone, Debug, Default)]
pub(in crate::user_config) struct UiOverrideData {
show_progress: Option<UiShowProgress>,
max_progress_running: Option<MaxProgressRunning>,
input_handler: Option<bool>,
output_indent: Option<bool>,
pager: Option<PagerSetting>,
paginate: Option<PaginateSetting>,
streampager_interface: Option<StreampagerInterface>,
streampager_wrapping: Option<StreampagerWrapping>,
streampager_show_ruler: Option<bool>,
}
impl UiOverrideData {
pub(in crate::user_config) fn pager(&self) -> Option<&PagerSetting> {
self.pager.as_ref()
}
pub(in crate::user_config) fn paginate(&self) -> Option<&PaginateSetting> {
self.paginate.as_ref()
}
pub(in crate::user_config) fn streampager_interface(&self) -> Option<&StreampagerInterface> {
self.streampager_interface.as_ref()
}
pub(in crate::user_config) fn streampager_wrapping(&self) -> Option<&StreampagerWrapping> {
self.streampager_wrapping.as_ref()
}
pub(in crate::user_config) fn streampager_show_ruler(&self) -> Option<&bool> {
self.streampager_show_ruler.as_ref()
}
}
#[derive(Clone, Debug)]
pub struct UiConfig {
pub show_progress: UiShowProgress,
pub max_progress_running: MaxProgressRunning,
pub input_handler: bool,
pub output_indent: bool,
pub pager: PagerSetting,
pub paginate: PaginateSetting,
pub streampager: StreampagerConfig,
}
impl UiConfig {
pub(in crate::user_config) fn resolve(
default_config: &DefaultUiConfig,
default_overrides: &[CompiledUiOverride],
user_config: Option<&DeserializedUiConfig>,
user_overrides: &[CompiledUiOverride],
host_platform: &Platform,
) -> Self {
Self {
show_progress: resolve_ui_setting(
&default_config.show_progress,
default_overrides,
user_config.and_then(|c| c.show_progress.as_ref()),
user_overrides,
host_platform,
|data| data.show_progress.as_ref(),
),
max_progress_running: resolve_ui_setting(
&default_config.max_progress_running,
default_overrides,
user_config.and_then(|c| c.max_progress_running.as_ref()),
user_overrides,
host_platform,
|data| data.max_progress_running.as_ref(),
),
input_handler: resolve_ui_setting(
&default_config.input_handler,
default_overrides,
user_config.and_then(|c| c.input_handler.as_ref()),
user_overrides,
host_platform,
|data| data.input_handler.as_ref(),
),
output_indent: resolve_ui_setting(
&default_config.output_indent,
default_overrides,
user_config.and_then(|c| c.output_indent.as_ref()),
user_overrides,
host_platform,
|data| data.output_indent.as_ref(),
),
pager: resolve_ui_setting(
&default_config.pager,
default_overrides,
user_config.and_then(|c| c.pager.as_ref()),
user_overrides,
host_platform,
|data| data.pager.as_ref(),
),
paginate: resolve_ui_setting(
&default_config.paginate,
default_overrides,
user_config.and_then(|c| c.paginate.as_ref()),
user_overrides,
host_platform,
|data| data.paginate.as_ref(),
),
streampager: StreampagerConfig {
interface: resolve_ui_setting(
&default_config.streampager.interface,
default_overrides,
user_config.and_then(|c| c.streampager.interface.as_ref()),
user_overrides,
host_platform,
|data| data.streampager_interface.as_ref(),
),
wrapping: resolve_ui_setting(
&default_config.streampager.wrapping,
default_overrides,
user_config.and_then(|c| c.streampager.wrapping.as_ref()),
user_overrides,
host_platform,
|data| data.streampager_wrapping.as_ref(),
),
show_ruler: resolve_ui_setting(
&default_config.streampager.show_ruler,
default_overrides,
user_config.and_then(|c| c.streampager.show_ruler.as_ref()),
user_overrides,
host_platform,
|data| data.streampager_show_ruler.as_ref(),
),
},
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum UiShowProgress {
#[default]
Auto,
None,
Bar,
Counter,
Only,
}
impl From<UiShowProgress> for ShowProgress {
fn from(ui: UiShowProgress) -> Self {
match ui {
UiShowProgress::Auto => ShowProgress::Auto {
suppress_success: false,
},
UiShowProgress::None => ShowProgress::None,
UiShowProgress::Bar => ShowProgress::Running,
UiShowProgress::Counter => ShowProgress::Counter,
UiShowProgress::Only => ShowProgress::Auto {
suppress_success: true,
},
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum PaginateSetting {
#[default]
Auto,
Never,
}
pub const BUILTIN_PAGER_NAME: &str = ":builtin";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(in crate::user_config) struct DeserializedStreampagerConfig {
pub(in crate::user_config) interface: Option<StreampagerInterface>,
pub(in crate::user_config) wrapping: Option<StreampagerWrapping>,
pub(in crate::user_config) show_ruler: Option<bool>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(in crate::user_config) struct DefaultStreampagerConfig {
pub(in crate::user_config) interface: StreampagerInterface,
pub(in crate::user_config) wrapping: StreampagerWrapping,
pub(in crate::user_config) show_ruler: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StreampagerConfig {
pub interface: StreampagerInterface,
pub wrapping: StreampagerWrapping,
pub show_ruler: bool,
}
impl StreampagerConfig {
pub fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode {
use streampager::config::InterfaceMode;
match self.interface {
StreampagerInterface::FullScreenClearOutput => InterfaceMode::FullScreen,
StreampagerInterface::QuitIfOnePage => InterfaceMode::Hybrid,
StreampagerInterface::QuitQuicklyOrClearOutput => {
InterfaceMode::Delayed(std::time::Duration::from_secs(2))
}
}
}
pub fn streampager_wrapping_mode(&self) -> streampager::config::WrappingMode {
use streampager::config::WrappingMode;
match self.wrapping {
StreampagerWrapping::None => WrappingMode::Unwrapped,
StreampagerWrapping::Word => WrappingMode::WordBoundary,
StreampagerWrapping::Anywhere => WrappingMode::GraphemeBoundary,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StreampagerInterface {
#[default]
QuitIfOnePage,
FullScreenClearOutput,
QuitQuicklyOrClearOutput,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StreampagerWrapping {
None,
#[default]
Word,
Anywhere,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommandNameAndArgs {
command: Vec<String>,
env: BTreeMap<String, String>,
}
impl CommandNameAndArgs {
pub fn command_name(&self) -> &str {
&self.command[0]
}
pub fn args(&self) -> &[String] {
&self.command[1..]
}
pub fn to_command(&self) -> Command {
let mut cmd = Command::new(self.command_name());
cmd.args(self.args());
cmd.envs(&self.env);
cmd
}
}
impl<'de> Deserialize<'de> for CommandNameAndArgs {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(CommandNameAndArgsVisitor)
}
}
struct CommandNameAndArgsVisitor;
impl<'de> de::Visitor<'de> for CommandNameAndArgsVisitor {
type Value = CommandNameAndArgs;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
"a command string (\"less -FRX\"), \
an array ([\"less\", \"-FRX\"]), \
or a table ({ command = [\"less\", \"-FRX\"], env = { ... } })",
)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
let command: Vec<String> = shell_words::split(v).map_err(de::Error::custom)?;
if command.is_empty() {
return Err(de::Error::custom("command string must not be empty"));
}
Ok(CommandNameAndArgs {
command,
env: BTreeMap::new(),
})
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut command = Vec::new();
while let Some(arg) = seq.next_element::<String>()? {
command.push(arg);
}
if command.is_empty() {
return Err(de::Error::custom("command array must not be empty"));
}
Ok(CommandNameAndArgs {
command,
env: BTreeMap::new(),
})
}
fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
#[derive(Deserialize)]
struct StructuredInner {
command: Vec<String>,
#[serde(default)]
env: BTreeMap<String, String>,
}
let inner = StructuredInner::deserialize(de::value::MapAccessDeserializer::new(map))?;
if inner.command.is_empty() {
return Err(de::Error::custom("command array must not be empty"));
}
Ok(CommandNameAndArgs {
command: inner.command,
env: inner.env,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PagerSetting {
Builtin,
External(CommandNameAndArgs),
}
#[cfg(test)]
impl Default for PagerSetting {
fn default() -> Self {
Self::External(CommandNameAndArgs {
command: vec!["less".to_owned(), "-FRX".to_owned()],
env: [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
.into_iter()
.collect(),
})
}
}
impl<'de> Deserialize<'de> for PagerSetting {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(PagerSettingVisitor)
}
}
struct PagerSettingVisitor;
impl<'de> de::Visitor<'de> for PagerSettingVisitor {
type Value = PagerSetting;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter
.write_str("\":builtin\", a command string, an array, or a table with command and env")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
if v == BUILTIN_PAGER_NAME {
return Ok(PagerSetting::Builtin);
}
let cmd = CommandNameAndArgsVisitor.visit_str(v)?;
Ok(PagerSetting::External(cmd))
}
fn visit_seq<A: de::SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
let args = CommandNameAndArgsVisitor.visit_seq(seq)?;
Ok(PagerSetting::External(args))
}
fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
let args = CommandNameAndArgsVisitor.visit_map(map)?;
Ok(PagerSetting::External(args))
}
}
struct MaxProgressRunningVisitor;
impl<'de> de::Visitor<'de> for MaxProgressRunningVisitor {
type Value = MaxProgressRunning;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a non-negative integer or \"infinite\"")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(MaxProgressRunning::Count(v as usize))
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
if v < 0 {
Err(E::invalid_value(Unexpected::Signed(v), &self))
} else {
Ok(MaxProgressRunning::Count(v as usize))
}
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
if v == "infinite" {
Ok(MaxProgressRunning::Infinite)
} else {
v.parse::<usize>()
.map(MaxProgressRunning::Count)
.map_err(|_| E::invalid_value(Unexpected::Str(v), &self))
}
}
}
fn deserialize_max_progress_running<'de, D>(
deserializer: D,
) -> Result<Option<MaxProgressRunning>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_option(OptionMaxProgressRunningVisitor)
}
struct OptionMaxProgressRunningVisitor;
impl<'de> de::Visitor<'de> for OptionMaxProgressRunningVisitor {
type Value = Option<MaxProgressRunning>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a non-negative integer, \"infinite\", or null")
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
deserializer
.deserialize_any(MaxProgressRunningVisitor)
.map(Some)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
fn deserialize_max_progress_running_required<'de, D>(
deserializer: D,
) -> Result<MaxProgressRunning, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(MaxProgressRunningVisitor)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{platform::detect_host_platform_for_tests, user_config::DefaultUserConfig};
fn make_override(platform: &str, data: DeserializedUiOverrideData) -> CompiledUiOverride {
let platform_spec =
TargetSpec::new(platform.to_string()).expect("valid platform spec in test");
CompiledUiOverride::new(platform_spec, data)
}
#[test]
fn test_ui_config_show_progress() {
let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "auto""#).unwrap();
assert!(matches!(config.show_progress, Some(UiShowProgress::Auto)));
let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "none""#).unwrap();
assert!(matches!(config.show_progress, Some(UiShowProgress::None)));
let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "bar""#).unwrap();
assert!(matches!(config.show_progress, Some(UiShowProgress::Bar)));
let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "counter""#).unwrap();
assert!(matches!(
config.show_progress,
Some(UiShowProgress::Counter)
));
let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "only""#).unwrap();
assert!(matches!(config.show_progress, Some(UiShowProgress::Only)));
let config: DeserializedUiConfig = toml::from_str("").unwrap();
assert!(config.show_progress.is_none());
toml::from_str::<DeserializedUiConfig>(r#"show-progress = "invalid""#).unwrap_err();
}
#[test]
fn test_ui_show_progress_to_show_progress() {
assert_eq!(
ShowProgress::from(UiShowProgress::Auto),
ShowProgress::Auto {
suppress_success: false
}
);
assert_eq!(ShowProgress::from(UiShowProgress::None), ShowProgress::None);
assert_eq!(
ShowProgress::from(UiShowProgress::Bar),
ShowProgress::Running
);
assert_eq!(
ShowProgress::from(UiShowProgress::Counter),
ShowProgress::Counter
);
assert_eq!(
ShowProgress::from(UiShowProgress::Only),
ShowProgress::Auto {
suppress_success: true
}
);
}
#[test]
fn test_ui_config_max_progress_running() {
let config: DeserializedUiConfig = toml::from_str("max-progress-running = 10").unwrap();
assert!(matches!(
config.max_progress_running,
Some(MaxProgressRunning::Count(10))
));
let config: DeserializedUiConfig = toml::from_str("max-progress-running = 0").unwrap();
assert!(matches!(
config.max_progress_running,
Some(MaxProgressRunning::Count(0))
));
let config: DeserializedUiConfig =
toml::from_str(r#"max-progress-running = "infinite""#).unwrap();
assert!(matches!(
config.max_progress_running,
Some(MaxProgressRunning::Infinite)
));
toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "INFINITE""#).unwrap_err();
let config: DeserializedUiConfig = toml::from_str("").unwrap();
assert!(config.max_progress_running.is_none());
toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "invalid""#).unwrap_err();
}
#[test]
fn test_ui_config_input_handler() {
let config: DeserializedUiConfig = toml::from_str("input-handler = true").unwrap();
assert_eq!(config.input_handler, Some(true));
let config: DeserializedUiConfig = toml::from_str("input-handler = false").unwrap();
assert_eq!(config.input_handler, Some(false));
let config: DeserializedUiConfig = toml::from_str("").unwrap();
assert!(config.input_handler.is_none());
}
#[test]
fn test_ui_config_output_indent() {
let config: DeserializedUiConfig = toml::from_str("output-indent = true").unwrap();
assert_eq!(config.output_indent, Some(true));
let config: DeserializedUiConfig = toml::from_str("output-indent = false").unwrap();
assert_eq!(config.output_indent, Some(false));
let config: DeserializedUiConfig = toml::from_str("").unwrap();
assert!(config.output_indent.is_none());
}
#[test]
fn test_resolved_ui_config_defaults_only() {
let defaults = DefaultUserConfig::from_embedded().ui;
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[], None, &[], &host);
assert_eq!(resolved.show_progress, defaults.show_progress);
assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
assert_eq!(resolved.input_handler, defaults.input_handler);
assert_eq!(resolved.output_indent, defaults.output_indent);
}
#[test]
fn test_resolved_ui_config_user_config_overrides_defaults() {
let defaults = DefaultUserConfig::from_embedded().ui;
let user_config = DeserializedUiConfig {
show_progress: Some(UiShowProgress::Bar),
max_progress_running: Some(MaxProgressRunning::Count(4)),
output_indent: Some(false),
..Default::default()
};
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
assert_eq!(resolved.show_progress, UiShowProgress::Bar);
assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
assert_eq!(resolved.input_handler, defaults.input_handler); assert!(!resolved.output_indent);
}
#[test]
fn test_resolved_ui_config_user_override_applies() {
let defaults = DefaultUserConfig::from_embedded().ui;
let override_ = make_override(
"cfg(all())",
DeserializedUiOverrideData {
show_progress: Some(UiShowProgress::Counter),
input_handler: Some(false),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
assert_eq!(resolved.show_progress, UiShowProgress::Counter);
assert_eq!(resolved.max_progress_running, defaults.max_progress_running); assert!(!resolved.input_handler);
assert_eq!(resolved.output_indent, defaults.output_indent); }
#[test]
fn test_resolved_ui_config_default_override_applies() {
let defaults = DefaultUserConfig::from_embedded().ui;
let override_ = make_override(
"cfg(all())",
DeserializedUiOverrideData {
show_progress: Some(UiShowProgress::Counter),
input_handler: Some(false),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[override_], None, &[], &host);
assert_eq!(resolved.show_progress, UiShowProgress::Counter);
assert_eq!(resolved.max_progress_running, defaults.max_progress_running); assert!(!resolved.input_handler);
assert_eq!(resolved.output_indent, defaults.output_indent); }
#[test]
fn test_resolved_ui_config_platform_override_no_match() {
let defaults = DefaultUserConfig::from_embedded().ui;
let override_ = make_override(
"cfg(any())",
DeserializedUiOverrideData {
show_progress: Some(UiShowProgress::Counter),
max_progress_running: Some(MaxProgressRunning::Count(2)),
input_handler: Some(false),
output_indent: Some(false),
pager: Some(PagerSetting::default()),
paginate: Some(PaginateSetting::Never),
streampager: Default::default(),
},
);
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
assert_eq!(resolved.show_progress, defaults.show_progress);
assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
assert_eq!(resolved.input_handler, defaults.input_handler);
assert_eq!(resolved.output_indent, defaults.output_indent);
}
#[test]
fn test_resolved_ui_config_first_matching_user_override_wins() {
let defaults = DefaultUserConfig::from_embedded().ui;
let override1 = make_override(
"cfg(all())",
DeserializedUiOverrideData {
show_progress: Some(UiShowProgress::Bar),
..Default::default()
},
);
let override2 = make_override(
"cfg(all())",
DeserializedUiOverrideData {
show_progress: Some(UiShowProgress::Counter), max_progress_running: Some(MaxProgressRunning::Count(4)),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[], None, &[override1, override2], &host);
assert_eq!(resolved.show_progress, UiShowProgress::Bar);
assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
}
#[test]
fn test_resolved_ui_config_user_override_beats_default_override() {
let defaults = DefaultUserConfig::from_embedded().ui;
let user_override = make_override(
"cfg(all())",
DeserializedUiOverrideData {
show_progress: Some(UiShowProgress::Bar),
..Default::default()
},
);
let default_override = make_override(
"cfg(all())",
DeserializedUiOverrideData {
show_progress: Some(UiShowProgress::Counter), max_progress_running: Some(MaxProgressRunning::Count(4)),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(
&defaults,
&[default_override],
None,
&[user_override],
&host,
);
assert_eq!(resolved.show_progress, UiShowProgress::Bar);
assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
}
#[test]
fn test_resolved_ui_config_override_beats_user_base() {
let defaults = DefaultUserConfig::from_embedded().ui;
let user_config = DeserializedUiConfig {
show_progress: Some(UiShowProgress::None),
max_progress_running: Some(MaxProgressRunning::Count(2)),
..Default::default()
};
let default_override = make_override(
"cfg(all())",
DeserializedUiOverrideData {
show_progress: Some(UiShowProgress::Counter),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(
&defaults,
&[default_override],
Some(&user_config),
&[],
&host,
);
assert_eq!(resolved.show_progress, UiShowProgress::Counter);
assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(2));
}
#[test]
fn test_paginate_setting_parsing() {
let config: DeserializedUiConfig = toml::from_str(r#"paginate = "auto""#).unwrap();
assert_eq!(config.paginate, Some(PaginateSetting::Auto));
let config: DeserializedUiConfig = toml::from_str(r#"paginate = "never""#).unwrap();
assert_eq!(config.paginate, Some(PaginateSetting::Never));
let config: DeserializedUiConfig = toml::from_str("").unwrap();
assert!(config.paginate.is_none());
let err = toml::from_str::<DeserializedUiConfig>(r#"paginate = "invalid""#).unwrap_err();
assert!(
err.to_string().contains("unknown variant"),
"error should mention 'unknown variant': {err}"
);
}
#[test]
fn test_command_name_and_args_parsing() {
#[derive(Debug, Deserialize)]
struct Wrapper {
cmd: CommandNameAndArgs,
}
let wrapper: Wrapper = toml::from_str(r#"cmd = "less -FRX""#).unwrap();
assert_eq!(
wrapper.cmd,
CommandNameAndArgs {
command: vec!["less".to_owned(), "-FRX".to_owned()],
env: BTreeMap::new(),
}
);
assert_eq!(wrapper.cmd.command_name(), "less");
assert_eq!(wrapper.cmd.args(), &["-FRX".to_owned()]);
let wrapper: Wrapper = toml::from_str(r#"cmd = ["less", "-F", "-R", "-X"]"#).unwrap();
assert_eq!(
wrapper.cmd,
CommandNameAndArgs {
command: vec![
"less".to_owned(),
"-F".to_owned(),
"-R".to_owned(),
"-X".to_owned()
],
env: BTreeMap::new(),
}
);
assert_eq!(wrapper.cmd.command_name(), "less");
assert_eq!(
wrapper.cmd.args(),
&["-F".to_owned(), "-R".to_owned(), "-X".to_owned()]
);
let cmd: CommandNameAndArgs = toml::from_str(
r#"
command = ["less", "-FRX"]
env = { LESSCHARSET = "utf-8" }
"#,
)
.unwrap();
let expected_env: BTreeMap<String, String> =
[("LESSCHARSET".to_owned(), "utf-8".to_owned())]
.into_iter()
.collect();
assert_eq!(
cmd,
CommandNameAndArgs {
command: vec!["less".to_owned(), "-FRX".to_owned()],
env: expected_env,
}
);
assert_eq!(cmd.command_name(), "less");
assert_eq!(cmd.args(), &["-FRX".to_owned()]);
let wrapper: Wrapper = toml::from_str(r#"cmd = 'my-pager "arg with spaces"'"#).unwrap();
assert_eq!(
wrapper.cmd,
CommandNameAndArgs {
command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
env: BTreeMap::new(),
}
);
let wrapper: Wrapper = toml::from_str(r#"cmd = "my-pager 'arg with spaces'""#).unwrap();
assert_eq!(
wrapper.cmd,
CommandNameAndArgs {
command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
env: BTreeMap::new(),
}
);
let wrapper: Wrapper =
toml::from_str(r#"cmd = 'my-pager "quoted \"nested\" arg"'"#).unwrap();
assert_eq!(
wrapper.cmd,
CommandNameAndArgs {
command: vec!["my-pager".to_owned(), "quoted \"nested\" arg".to_owned()],
env: BTreeMap::new(),
}
);
let wrapper: Wrapper = toml::from_str(r#"cmd = '"/path/to/my pager" --flag'"#).unwrap();
assert_eq!(
wrapper.cmd,
CommandNameAndArgs {
command: vec!["/path/to/my pager".to_owned(), "--flag".to_owned()],
env: BTreeMap::new(),
}
);
let wrapper: Wrapper =
toml::from_str(r#"cmd = 'cmd "first arg" "second arg" third'"#).unwrap();
assert_eq!(
wrapper.cmd,
CommandNameAndArgs {
command: vec![
"cmd".to_owned(),
"first arg".to_owned(),
"second arg".to_owned(),
"third".to_owned(),
],
env: BTreeMap::new(),
}
);
}
#[test]
fn test_command_and_pager_empty_errors() {
#[derive(Debug, Deserialize)]
struct Wrapper {
#[expect(dead_code)]
cmd: CommandNameAndArgs,
}
let cmd_cases = [
("empty array", "cmd = []"),
("empty string", r#"cmd = """#),
("whitespace-only string", r#"cmd = " ""#),
(
"structured with empty command",
r#"cmd = { command = [], env = { LESSCHARSET = "utf-8" } }"#,
),
];
for (name, input) in cmd_cases {
let err = toml::from_str::<Wrapper>(input).unwrap_err();
assert!(
err.to_string().contains("must not be empty"),
"CommandNameAndArgs {name}: error should mention 'must not be empty': {err}"
);
}
let pager_cases = [
("empty array", "pager = []"),
("empty string", r#"pager = """#),
];
for (name, input) in pager_cases {
let err = toml::from_str::<DeserializedUiConfig>(input).unwrap_err();
assert!(
err.to_string().contains("must not be empty"),
"PagerSetting {name}: error should mention 'must not be empty': {err}"
);
}
let unclosed_quote_cases = [
("unclosed double quote", r#"cmd = 'pager "unclosed'"#),
("unclosed single quote", r#"cmd = "pager 'unclosed""#),
];
for (name, input) in unclosed_quote_cases {
let err = toml::from_str::<Wrapper>(input).unwrap_err();
assert!(
err.to_string().contains("missing closing quote"),
"CommandNameAndArgs {name}: error should mention 'missing closing quote': {err}"
);
}
}
#[test]
fn test_command_name_and_args_to_command() {
let cmd = CommandNameAndArgs {
command: vec!["echo".to_owned(), "hello".to_owned()],
env: BTreeMap::new(),
};
let std_cmd = cmd.to_command();
assert_eq!(cmd.command_name(), "echo");
drop(std_cmd);
}
#[test]
fn test_pager_setting_parsing() {
let config: DeserializedUiConfig = toml::from_str(r#"pager = "less -FRX""#).unwrap();
assert_eq!(
config.pager,
Some(PagerSetting::External(CommandNameAndArgs {
command: vec!["less".to_owned(), "-FRX".to_owned()],
env: BTreeMap::new(),
}))
);
let config: DeserializedUiConfig = toml::from_str(r#"pager = ["less", "-FRX"]"#).unwrap();
assert_eq!(
config.pager,
Some(PagerSetting::External(CommandNameAndArgs {
command: vec!["less".to_owned(), "-FRX".to_owned()],
env: BTreeMap::new(),
}))
);
let config: DeserializedUiConfig = toml::from_str(
r#"
[pager]
command = ["less", "-FRX"]
env = { LESSCHARSET = "utf-8" }
"#,
)
.unwrap();
let expected_env: BTreeMap<String, String> =
[("LESSCHARSET".to_owned(), "utf-8".to_owned())]
.into_iter()
.collect();
assert_eq!(
config.pager,
Some(PagerSetting::External(CommandNameAndArgs {
command: vec!["less".to_owned(), "-FRX".to_owned()],
env: expected_env,
}))
);
let config: DeserializedUiConfig = toml::from_str("").unwrap();
assert!(config.pager.is_none());
}
#[test]
fn test_resolved_ui_config_pager_defaults() {
let defaults = DefaultUserConfig::from_embedded().ui;
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[], None, &[], &host);
assert_eq!(resolved.pager, defaults.pager);
assert_eq!(resolved.paginate, defaults.paginate);
}
#[test]
fn test_resolved_ui_config_pager_override() {
let defaults = DefaultUserConfig::from_embedded().ui;
let custom_pager = PagerSetting::External(CommandNameAndArgs {
command: vec!["more".to_owned()],
env: BTreeMap::new(),
});
let override_ = make_override(
"cfg(all())",
DeserializedUiOverrideData {
pager: Some(custom_pager.clone()),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
assert_eq!(resolved.pager, custom_pager);
assert_eq!(resolved.paginate, defaults.paginate);
}
#[test]
fn test_resolved_ui_config_paginate_override() {
let defaults = DefaultUserConfig::from_embedded().ui;
let override_ = make_override(
"cfg(all())",
DeserializedUiOverrideData {
paginate: Some(PaginateSetting::Never),
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
assert_eq!(resolved.paginate, PaginateSetting::Never);
assert_eq!(resolved.pager, defaults.pager);
}
#[test]
fn test_pager_setting_builtin() {
let config: DeserializedUiConfig = toml::from_str(r#"pager = ":builtin""#).unwrap();
assert_eq!(config.pager, Some(PagerSetting::Builtin));
}
#[test]
fn test_streampager_config_parsing() {
let config: DeserializedUiConfig = toml::from_str(
r#"
[streampager]
interface = "full-screen-clear-output"
wrapping = "anywhere"
show-ruler = false
"#,
)
.unwrap();
assert_eq!(
config.streampager.interface,
Some(StreampagerInterface::FullScreenClearOutput)
);
assert_eq!(
config.streampager.wrapping,
Some(StreampagerWrapping::Anywhere)
);
assert_eq!(config.streampager.show_ruler, Some(false));
let config: DeserializedUiConfig = toml::from_str(
r#"
[streampager]
interface = "quit-quickly-or-clear-output"
"#,
)
.unwrap();
assert_eq!(
config.streampager.interface,
Some(StreampagerInterface::QuitQuicklyOrClearOutput)
);
assert_eq!(config.streampager.wrapping, None);
assert_eq!(config.streampager.show_ruler, None);
let config: DeserializedUiConfig = toml::from_str("").unwrap();
assert_eq!(config.streampager.interface, None);
assert_eq!(config.streampager.wrapping, None);
assert_eq!(config.streampager.show_ruler, None);
}
#[test]
fn test_streampager_config_resolution() {
let defaults = DefaultUserConfig::from_embedded().ui;
let override_ = make_override(
"cfg(all())",
DeserializedUiOverrideData {
streampager: DeserializedStreampagerConfig {
interface: Some(StreampagerInterface::FullScreenClearOutput),
wrapping: None,
show_ruler: None,
},
..Default::default()
},
);
let host = detect_host_platform_for_tests();
let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
assert_eq!(
resolved.streampager.interface,
StreampagerInterface::FullScreenClearOutput
);
assert_eq!(resolved.streampager.wrapping, defaults.streampager.wrapping);
assert_eq!(
resolved.streampager.show_ruler,
defaults.streampager.show_ruler
);
}
}