use crate::cli::WipAction;
use crate::config::Config;
use crate::error::{Result, TixError};
use crate::storage::Store;
use crate::wip::{ColumnLimit, LimitPolicy, WipSnapshot};
pub fn run(config: &mut Config, store: &Store, action: &WipAction) -> Result<()> {
match action {
WipAction::Show { board } => show(config, store, board.as_deref()),
WipAction::Set {
column,
max_items,
max_estimate,
unit,
policy,
disabled,
board,
} => set(
config,
store,
board.as_deref(),
column,
*max_items,
*max_estimate,
unit.as_deref(),
policy.as_deref(),
*disabled,
),
WipAction::Unset { column, board } => unset(config, store, board.as_deref(), column),
WipAction::Unit {
name,
factor,
remove,
board,
} => unit(config, store, board.as_deref(), name, *factor, *remove),
}
}
fn board_name(config: &Config, requested: Option<&str>) -> String {
match requested {
Some(name) => name.to_string(),
None => crate::workflow::active_board_name(config).to_string(),
}
}
fn save(config: &mut Config, store: &Store) -> Result<()> {
config.validate()?;
let path = crate::commands::views::config_path(store);
config
.save(&path)
.map_err(|error| TixError::Config(format!("could not save `{}`: {error}", path.display())))
}
fn show(config: &Config, store: &Store, requested: Option<&str>) -> Result<()> {
let name = board_name(config, requested);
let board = config
.kanban
.configurations
.get(&name)
.ok_or_else(|| TixError::Config(format!("no board `{name}` in tix.toml")))?;
let issues = store.list()?;
let snapshot = WipSnapshot::build(board, &issues);
println!("board: {name}");
if board.wip.units.is_empty() {
println!("units: -");
} else {
let units: Vec<String> = board
.wip
.units
.iter()
.map(|(unit, factor)| format!("{unit}={factor}"))
.collect();
println!("units: {}", units.join(", "));
}
println!("COLUMN LIMIT USAGE");
for usage in snapshot.columns() {
let limit = match &usage.limit {
Some(limit) => describe_limit(limit),
None => "-".to_string(),
};
let used = match &usage.limit {
Some(_) => usage.cell(),
None => usage.items.to_string(),
};
println!("{} {limit} {used}", usage.column);
}
for diagnostic in snapshot.diagnostics() {
eprintln!("{}", diagnostic.render());
}
Ok(())
}
fn describe_limit(limit: &ColumnLimit) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(max) = limit.max_items {
parts.push(format!("max_items={max}"));
}
if let Some(max) = limit.max_estimate {
match limit.unit.as_deref() {
Some(unit) => parts.push(format!("max_estimate={max}{unit}")),
None => parts.push(format!("max_estimate={max}")),
}
}
match limit.policy {
Some(policy) => parts.push(format!("policy={}", policy.as_str())),
None => parts.push("policy=-".to_string()),
}
if !limit.enabled {
parts.push("disabled".to_string());
}
parts.join(",")
}
#[allow(clippy::too_many_arguments)]
fn set(
config: &mut Config,
store: &Store,
requested: Option<&str>,
column: &str,
max_items: Option<u32>,
max_estimate: Option<u32>,
unit: Option<&str>,
policy: Option<&str>,
disabled: bool,
) -> Result<()> {
let name = board_name(config, requested);
let policy = match policy {
Some(raw) => Some(parse_policy(raw)?),
None => None,
};
let board = config
.kanban
.configurations
.get_mut(&name)
.ok_or_else(|| TixError::Config(format!("no board `{name}` in tix.toml")))?;
if !board.columns.iter().any(|configured| configured == column) {
return Err(TixError::Config(format!(
"`{column}` is not a column on board `{name}`; valid columns: {}",
board.columns.join(", ")
)));
}
let limit = board
.wip
.limits
.entry(column.to_string())
.or_insert_with(ColumnLimit::default);
if let Some(max) = max_items {
limit.max_items = Some(max);
}
if let Some(max) = max_estimate {
limit.max_estimate = Some(max);
}
if let Some(unit) = unit {
limit.unit = Some(unit.to_string());
}
if let Some(policy) = policy {
limit.policy = Some(policy);
}
limit.enabled = !disabled;
save(config, store)?;
println!("Set the limit for `{column}` on board {name}");
Ok(())
}
fn unset(config: &mut Config, store: &Store, requested: Option<&str>, column: &str) -> Result<()> {
let name = board_name(config, requested);
let board = config
.kanban
.configurations
.get_mut(&name)
.ok_or_else(|| TixError::Config(format!("no board `{name}` in tix.toml")))?;
if board.wip.limits.shift_remove(column).is_none() {
return Err(TixError::Config(format!(
"board `{name}` has no limit on `{column}`"
)));
}
save(config, store)?;
println!("Removed the limit for `{column}` on board {name}");
Ok(())
}
fn unit(
config: &mut Config,
store: &Store,
requested: Option<&str>,
name: &str,
factor: Option<u32>,
remove: bool,
) -> Result<()> {
let board_name = board_name(config, requested);
let board = config
.kanban
.configurations
.get_mut(&board_name)
.ok_or_else(|| TixError::Config(format!("no board `{board_name}` in tix.toml")))?;
if remove {
if board.wip.units.shift_remove(name).is_none() {
return Err(TixError::Config(format!(
"board `{board_name}` does not declare the unit `{name}`"
)));
}
save(config, store)?;
println!("Removed estimate unit `{name}` on board {board_name}");
return Ok(());
}
let factor = factor.ok_or_else(|| {
TixError::InvalidInput(format!(
"`tix wip unit {name} <FACTOR>` needs the factor to the board's base unit \
(or --remove)"
))
})?;
if factor == 0 {
return Err(TixError::Config(format!(
"the factor for `{name}` must be at least 1"
)));
}
board.wip.units.insert(name.to_string(), factor);
save(config, store)?;
println!("Estimate unit `{name}` = {factor} on board {board_name}");
Ok(())
}
fn parse_policy(raw: &str) -> Result<LimitPolicy> {
match raw.trim().to_ascii_lowercase().as_str() {
"warn" => Ok(LimitPolicy::Warn),
"block" => Ok(LimitPolicy::Block),
other => Err(TixError::InvalidInput(format!(
"unknown policy `{other}`; `warn` reports the violation and proceeds, \
`block` refuses the move unless it is overridden"
))),
}
}