#[allow(clippy::wildcard_imports)]
use super::*;
impl App {
pub(in crate::app) fn open_settings(&mut self, prompt: Option<SettingsPrompt>) {
self.settings = Some(SettingsModal {
selected: 0,
prompt,
section: ConfigSection::Dashboards,
..SettingsModal::default()
});
self.mode = Mode::Settings;
}
pub(in crate::app) fn handle_settings_key(&mut self, key: KeyEvent) -> Result<Action> {
let Some(section) = self.settings.as_ref().map(|m| m.section) else {
self.mode = Mode::Board;
return Ok(Action::None);
};
match section {
ConfigSection::Dashboards => self.handle_settings_dashboards_key(key),
ConfigSection::Cycles | ConfigSection::Milestones => {
self.handle_settings_planning_key(key)
}
ConfigSection::Types | ConfigSection::CustomAttributes => {
self.handle_settings_list_key(key)
}
}
}
pub(in crate::app) fn handle_settings_dashboards_key(
&mut self,
key: KeyEvent,
) -> Result<Action> {
let prompt_active = self.settings.as_ref().and_then(|m| m.prompt.clone());
let Some(p) = prompt_active else {
return self.handle_settings_browse(key);
};
match key.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
}
Ok(Action::None)
}
KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => {
match p {
SettingsPrompt::AddColumn(name) => self.settings_commit_add_column(name),
SettingsPrompt::NewBoard(name) => self.settings_commit_new_board(name),
SettingsPrompt::RenameBoard { old, buffer } => {
self.settings_commit_rename_board(old, buffer)
}
SettingsPrompt::ConfirmDeleteBoard { name, .. } => {
self.settings_commit_delete_board(name)
}
SettingsPrompt::SetEditor(value) => self.settings_commit_editor(value),
SettingsPrompt::SetViewer(value) => self.settings_commit_viewer(value),
SettingsPrompt::AddType(_) | SettingsPrompt::AddAttribute(_) => {}
}
Ok(Action::None)
}
KeyCode::Char(c) => {
self.settings_prompt_push(c);
Ok(Action::None)
}
KeyCode::Backspace => {
self.settings_prompt_pop();
Ok(Action::None)
}
_ => Ok(Action::None),
}
}
pub(in crate::app) fn settings_prompt_push(&mut self, c: char) {
if let Some(m) = self.settings.as_mut()
&& let Some(prompt) = m.prompt.as_mut()
{
match prompt {
SettingsPrompt::AddColumn(b)
| SettingsPrompt::NewBoard(b)
| SettingsPrompt::RenameBoard { buffer: b, .. }
| SettingsPrompt::SetEditor(b)
| SettingsPrompt::SetViewer(b)
| SettingsPrompt::AddType(b)
| SettingsPrompt::AddAttribute(b) => b.push(c),
SettingsPrompt::ConfirmDeleteBoard { .. } => {}
}
}
}
pub(in crate::app) fn settings_prompt_pop(&mut self) {
if let Some(m) = self.settings.as_mut()
&& let Some(prompt) = m.prompt.as_mut()
{
match prompt {
SettingsPrompt::AddColumn(b)
| SettingsPrompt::NewBoard(b)
| SettingsPrompt::RenameBoard { buffer: b, .. }
| SettingsPrompt::SetEditor(b)
| SettingsPrompt::SetViewer(b)
| SettingsPrompt::AddType(b)
| SettingsPrompt::AddAttribute(b) => {
b.pop();
}
SettingsPrompt::ConfirmDeleteBoard { .. } => {}
}
}
}
pub(in crate::app) fn handle_settings_browse(&mut self, key: KeyEvent) -> Result<Action> {
let level = self.settings.as_ref().map(|m| m.level).unwrap_or_default();
match key.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
self.close_config_mode();
Ok(Action::None)
}
KeyCode::Left | KeyCode::Char('h') => {
if level == ConfigLevel::Sections {
self.close_config_mode();
} else {
self.config_move_level(-1);
}
Ok(Action::None)
}
KeyCode::Right | KeyCode::Char('l') => {
match level {
ConfigLevel::Sections => self.config_move_level(1),
ConfigLevel::Index => self.settings_drill_board(),
ConfigLevel::Detail => {}
}
Ok(Action::None)
}
KeyCode::Up | KeyCode::Char('k') => {
match level {
ConfigLevel::Sections => self.config_prev_section(),
ConfigLevel::Index => {
let count = self.store.config.configs().count();
if let Some(m) = self.settings.as_mut() {
m.move_selected(-1, count);
}
}
ConfigLevel::Detail => {
let count = board_rows_len(self.active_board_config());
if let Some(m) = self.settings.as_mut() {
m.move_detail(-1, count);
}
}
}
Ok(Action::None)
}
KeyCode::Down | KeyCode::Char('j') => {
match level {
ConfigLevel::Sections => self.config_next_section(),
ConfigLevel::Index => {
let count = self.store.config.configs().count();
if let Some(m) = self.settings.as_mut() {
m.move_selected(1, count);
}
}
ConfigLevel::Detail => {
let count = board_rows_len(self.active_board_config());
if let Some(m) = self.settings.as_mut() {
m.move_detail(1, count);
}
}
}
Ok(Action::None)
}
KeyCode::Char('K') => self.settings_move_column(-1),
KeyCode::Char('J') => self.settings_move_column(1),
KeyCode::Tab => {
self.settings_next_board();
Ok(Action::None)
}
KeyCode::BackTab => {
self.settings_prev_board();
Ok(Action::None)
}
KeyCode::Enter => match level {
ConfigLevel::Sections => {
self.config_move_level(1);
Ok(Action::None)
}
ConfigLevel::Index => {
self.settings_drill_board();
Ok(Action::None)
}
ConfigLevel::Detail => self.settings_toggle_selected(),
},
KeyCode::Char(' ') => self.settings_toggle_selected(),
KeyCode::Char('a') => {
if let Some(m) = self.settings.as_mut() {
m.prompt = Some(SettingsPrompt::AddColumn(String::new()));
}
Ok(Action::None)
}
KeyCode::Char('n') => {
if let Some(m) = self.settings.as_mut() {
m.prompt = Some(SettingsPrompt::NewBoard(String::new()));
}
Ok(Action::None)
}
KeyCode::Char('e') => {
let old = self.active_board_name().to_string();
if let Some(m) = self.settings.as_mut() {
m.prompt = Some(SettingsPrompt::RenameBoard {
old: old.clone(),
buffer: old,
});
}
Ok(Action::None)
}
KeyCode::Char('x') | KeyCode::Char('X') => {
if self.store.config.configs().count() <= 1 {
self.status_msg = Some("Cannot delete the last board".to_string());
return Ok(Action::None);
}
let name = self.active_board_name().to_string();
let referencing = Self::board_reference_count(&name);
if let Some(m) = self.settings.as_mut() {
m.prompt = Some(SettingsPrompt::ConfirmDeleteBoard { name, referencing });
}
Ok(Action::None)
}
_ => Ok(Action::None),
}
}
pub(in crate::app) fn handle_settings_planning_key(&mut self, key: KeyEvent) -> Result<Action> {
if self.planning.is_none() {
let mut modal = PlanningModal::new();
modal.segment = match self.settings.as_ref().map(|m| m.section) {
Some(ConfigSection::Milestones) => PlanningSegment::Milestones,
_ => PlanningSegment::Cycles,
};
self.planning = Some(modal);
}
if let Some(prompt) = self.planning.as_ref().and_then(|m| m.prompt.clone()) {
return self.handle_planning_confirm_key(key, prompt);
}
let level = if self.milestone_mode {
ConfigLevel::Index
} else {
self.settings.as_ref().map(|m| m.level).unwrap_or_default()
};
match key.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
self.close_config_mode();
Ok(Action::None)
}
KeyCode::Left | KeyCode::Char('h') => {
if !self.milestone_mode {
if level == ConfigLevel::Sections {
self.close_config_mode();
} else {
self.config_move_level(-1);
}
}
Ok(Action::None)
}
KeyCode::Right | KeyCode::Char('l') => {
match level {
ConfigLevel::Sections => self.config_move_level(1),
ConfigLevel::Index => return self.planning_open_edit(),
ConfigLevel::Detail => {}
}
Ok(Action::None)
}
KeyCode::Up | KeyCode::Char('k') => {
match level {
ConfigLevel::Sections => self.config_prev_section(),
_ => {
let len = self.planning_rows_len();
if let Some(m) = self.planning.as_mut() {
m.move_selected(-1, len);
}
}
}
Ok(Action::None)
}
KeyCode::Down | KeyCode::Char('j') => {
match level {
ConfigLevel::Sections => self.config_next_section(),
_ => {
let len = self.planning_rows_len();
if let Some(m) = self.planning.as_mut() {
m.move_selected(1, len);
}
}
}
Ok(Action::None)
}
KeyCode::Enter => match level {
ConfigLevel::Sections => {
self.config_move_level(1);
Ok(Action::None)
}
_ => self.planning_open_edit(),
},
KeyCode::Char('n') => {
self.planning_open_create();
Ok(Action::None)
}
KeyCode::Char('e') => self.planning_open_edit(),
KeyCode::Char('x') | KeyCode::Char('X') => self.planning_confirm_delete(),
KeyCode::Char('m') | KeyCode::Char('M') => {
let segment = if self.milestone_mode
|| self.settings.as_ref().map(|m| m.section) == Some(ConfigSection::Milestones)
{
PlanningSegment::Milestones
} else {
PlanningSegment::Cycles
};
self.open_planning_modal();
if let Some(modal) = self.planning.as_mut() {
modal.segment = segment;
modal.selected = 0;
modal.prompt = None;
}
Ok(Action::None)
}
KeyCode::Char('?') => {
self.open_help(Mode::Settings);
Ok(Action::None)
}
_ => Ok(Action::None),
}
}
pub(in crate::app) fn handle_settings_list_key(&mut self, key: KeyEvent) -> Result<Action> {
if let Some(prompt) = self.settings.as_ref().and_then(|m| m.prompt.clone()) {
match key.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
}
}
KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => match prompt {
SettingsPrompt::AddType(value) => self.settings_commit_add_type(value),
SettingsPrompt::AddAttribute(value) => {
self.settings_commit_add_attribute(value)
}
_ => {}
},
KeyCode::Char(c) => self.settings_prompt_push(c),
KeyCode::Backspace => self.settings_prompt_pop(),
_ => {}
}
return Ok(Action::None);
}
let section = self
.settings
.as_ref()
.map(|m| m.section)
.unwrap_or_default();
let level = self.settings.as_ref().map(|m| m.level).unwrap_or_default();
match key.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
self.close_config_mode();
Ok(Action::None)
}
KeyCode::Left | KeyCode::Char('h') => {
if level == ConfigLevel::Sections {
self.close_config_mode();
} else {
self.config_move_level(-1);
}
Ok(Action::None)
}
KeyCode::Right | KeyCode::Char('l') => {
if level == ConfigLevel::Sections {
self.config_move_level(1);
}
Ok(Action::None)
}
KeyCode::Up | KeyCode::Char('k') => {
match level {
ConfigLevel::Sections => self.config_prev_section(),
_ => {
let len = self.settings_list_len(section);
if let Some(m) = self.settings.as_mut() {
m.move_selected(-1, len);
}
}
}
Ok(Action::None)
}
KeyCode::Down | KeyCode::Char('j') => {
match level {
ConfigLevel::Sections => self.config_next_section(),
_ => {
let len = self.settings_list_len(section);
if let Some(m) = self.settings.as_mut() {
m.move_selected(1, len);
}
}
}
Ok(Action::None)
}
KeyCode::Enter => {
if level == ConfigLevel::Sections {
self.config_move_level(1);
}
Ok(Action::None)
}
KeyCode::Char('n') => {
let prompt = match section {
ConfigSection::CustomAttributes => SettingsPrompt::AddAttribute(String::new()),
_ => SettingsPrompt::AddType(String::new()),
};
if let Some(m) = self.settings.as_mut() {
m.prompt = Some(prompt);
}
Ok(Action::None)
}
KeyCode::Char('x') | KeyCode::Char('X') => {
self.settings_remove_selected(section);
Ok(Action::None)
}
_ => Ok(Action::None),
}
}
pub(in crate::app) fn settings_list_len(&self, section: ConfigSection) -> usize {
match section {
ConfigSection::Types => self.store.config.custom_types().len(),
ConfigSection::CustomAttributes => self.store.config.attributes().len(),
_ => 0,
}
}
pub(in crate::app) fn settings_remove_selected(&mut self, section: ConfigSection) {
let selected = self.settings.as_ref().map(|m| m.selected).unwrap_or(0);
let removed = match section {
ConfigSection::Types => {
let mut types = self.store.config.custom_types();
if selected >= types.len() {
return;
}
let removed = types.remove(selected);
self.store.config.types = Some(types);
removed
}
ConfigSection::CustomAttributes => {
let mut attrs = self.store.config.attributes();
if selected >= attrs.len() {
return;
}
let removed = attrs.remove(selected);
self.store.config.custom_attributes.keys = attrs;
removed
}
_ => return,
};
if !self.persist_config() {
return;
}
let len = self.settings_list_len(section);
if let Some(m) = self.settings.as_mut() {
let target = selected.saturating_sub(1).min(len.saturating_sub(1));
m.move_selected_to(target, len);
}
self.status_msg = Some(match section {
ConfigSection::Types => format!("Removed type '{removed}'"),
_ => format!("Removed attribute '{removed}'"),
});
}
pub(in crate::app) fn settings_commit_add_type(&mut self, value: String) {
let value = value.trim().to_string();
if value.is_empty() {
self.status_msg = Some("Type name is required".to_string());
return;
}
let mut types = self.store.config.custom_types();
if types.iter().any(|t| t.eq_ignore_ascii_case(&value)) {
self.status_msg = Some(format!("Type '{value}' already exists"));
return;
}
types.push(value.clone());
self.store.config.types = Some(types);
if !self.persist_config() {
return;
}
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
}
self.status_msg = Some(format!("Added type '{value}'"));
}
pub(in crate::app) fn settings_commit_add_attribute(&mut self, value: String) {
let value = value.trim().to_string();
if value.is_empty() {
self.status_msg = Some("Attribute name is required".to_string());
return;
}
let mut attrs = self.store.config.attributes();
if attrs.iter().any(|key| key.eq_ignore_ascii_case(&value)) {
self.status_msg = Some(format!("Attribute '{value}' already exists"));
return;
}
attrs.push(value.clone());
self.store.config.custom_attributes.keys = attrs;
if !self.persist_config() {
return;
}
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
}
self.status_msg = Some(format!("Added attribute '{value}'"));
}
pub(in crate::app) fn close_config_mode(&mut self) {
self.settings = None;
self.planning = None;
self.planning_form = None;
self.milestone_mode = false;
self.mode = Mode::Board;
self.rebuild_board();
}
pub(in crate::app) fn config_set_section(&mut self, section: ConfigSection) {
if self.milestone_mode {
return;
}
if let Some(m) = self.settings.as_mut() {
m.set_section(section);
m.focus(ConfigLevel::Sections);
}
let segment = match section {
ConfigSection::Dashboards | ConfigSection::Types | ConfigSection::CustomAttributes => {
return;
}
ConfigSection::Cycles => PlanningSegment::Cycles,
ConfigSection::Milestones => PlanningSegment::Milestones,
};
let modal = self.planning.get_or_insert_with(PlanningModal::new);
if modal.segment != segment {
modal.segment = segment;
modal.selected = 0;
modal.prompt = None;
}
}
pub(in crate::app) fn config_next_section(&mut self) {
if self.milestone_mode {
return;
}
if let Some(next) = self.settings.as_ref().map(|m| m.section.next()) {
self.config_set_section(next);
}
}
pub(in crate::app) fn config_prev_section(&mut self) {
if self.milestone_mode {
return;
}
if let Some(prev) = self.settings.as_ref().map(|m| m.section.prev()) {
self.config_set_section(prev);
}
}
pub(in crate::app) fn config_move_level(&mut self, delta: isize) {
if let Some(m) = self.settings.as_mut() {
m.move_level(delta);
}
}
pub(in crate::app) fn settings_drill_board(&mut self) {
let Some(index) = self.settings.as_ref().map(|m| m.selected) else {
return;
};
if index >= self.store.config.configs().count() {
return;
}
self.switch_board(index);
if let Some(m) = self.settings.as_mut() {
m.focus(ConfigLevel::Detail);
m.detail_selected = 0;
}
}
pub(in crate::app) fn settings_move_column(&mut self, delta: isize) -> Result<Action> {
let Some(modal) = self.settings.as_ref() else {
return Ok(Action::None);
};
if modal.level != ConfigLevel::Detail {
return Ok(Action::None);
}
let from = modal.detail_selected;
let board_name = self.active_board_name().to_string();
let Some(board_config) = self.store.config.kanban.configurations.get_mut(&board_name)
else {
return Ok(Action::None);
};
if from >= board_config.columns.len() {
return Ok(Action::None);
}
let to = from as isize + delta;
if to < 0 || to as usize >= board_config.columns.len() {
return Ok(Action::None);
}
board_config.columns.swap(from, to as usize);
if !self.persist_config() {
return Ok(Action::None);
}
self.rebuild_board();
if let Some(m) = self.settings.as_mut() {
m.detail_selected = to as usize;
}
self.status_msg = Some(format!("Moved column to position {}", to + 1));
Ok(Action::None)
}
pub(in crate::app) fn apply_config_click(&mut self, action: Action) {
match action {
Action::ConfigSection(section) => self.config_set_section(section),
Action::SettingsRow(row) => {
let count = self.store.config.configs().count();
if let Some(modal) = self.settings.as_mut() {
modal.focus(ConfigLevel::Index);
modal.move_selected_to(row, count);
}
}
Action::ConfigDetailRow(row) => {
let count = board_rows_len(self.active_board_config());
if let Some(modal) = self.settings.as_mut() {
modal.focus(ConfigLevel::Detail);
modal.move_detail_to(row, count);
}
}
Action::ConfigPlanningRow(row) => {
let len = self.planning_rows_len();
if let Some(modal) = self.settings.as_mut() {
modal.focus(ConfigLevel::Index);
}
if let Some(modal) = self.planning.as_mut() {
modal.move_selected_to(row, len);
}
}
Action::ConfigListRow(row) => {
let section = self
.settings
.as_ref()
.map(|modal| modal.section)
.unwrap_or_default();
let len = self.settings_list_len(section);
if let Some(modal) = self.settings.as_mut() {
modal.focus(ConfigLevel::Index);
modal.move_selected_to(row, len);
}
}
_ => {}
}
}
pub(in crate::app) fn board_reference_count(_name: &str) -> usize {
0
}
pub(in crate::app) fn settings_next_board(&mut self) {
self.tabs.next();
self.active_config = self.tabs.selected;
self.switch_config();
if let Some(m) = self.settings.as_mut() {
m.selected = 0;
m.detail_selected = 0;
}
}
pub(in crate::app) fn settings_prev_board(&mut self) {
self.tabs.previous();
self.active_config = self.tabs.selected;
self.switch_config();
if let Some(m) = self.settings.as_mut() {
m.selected = 0;
m.detail_selected = 0;
}
}
pub(in crate::app) fn settings_toggle_selected(&mut self) -> Result<Action> {
let Some(modal) = self.settings.as_ref() else {
return Ok(Action::None);
};
if modal.level != ConfigLevel::Detail {
return Ok(Action::None);
}
let col_count = self.active_board_config().columns.len();
let selected = modal.detail_selected;
if selected >= col_count {
return self.settings_toggle_display_row(selected - col_count);
}
let col = self
.settings
.as_ref()
.and_then(|m| self.active_board_config().columns.get(m.detail_selected))
.cloned();
let Some(col) = col else {
return Ok(Action::None);
};
let board_name = self.active_board_name().to_string();
let now_hidden = {
let board_config = self.store.config.kanban.configurations.get_mut(&board_name);
match board_config {
Some(bc) => {
if let Some(pos) = bc.hidden_columns.iter().position(|h| h == &col) {
bc.hidden_columns.remove(pos);
false
} else {
bc.hidden_columns.push(col.clone());
true
}
}
None => return Ok(Action::None),
}
};
if !self.persist_config() {
return Ok(Action::None);
}
if let Some(session) = self.view_session.as_mut() {
session.visibility = false;
}
self.rebuild_board();
let last = self.active_board_config().columns.len().saturating_sub(1);
if let Some(m) = self.settings.as_mut()
&& m.detail_selected > last
{
m.detail_selected = last;
}
self.status_msg = Some(match now_hidden {
true => format!("'{col}' hidden"),
false => format!("'{col}' shown"),
});
Ok(Action::None)
}
pub(in crate::app) fn settings_toggle_display_row(&mut self, row: usize) -> Result<Action> {
if row >= crate::ui::settings::DISPLAY_ROWS {
let general = row - crate::ui::settings::DISPLAY_ROWS;
let prompt = match general {
0 => {
SettingsPrompt::SetEditor(self.store.config.editor.clone().unwrap_or_default())
}
_ => {
SettingsPrompt::SetViewer(self.store.config.viewer.clone().unwrap_or_default())
}
};
if let Some(m) = self.settings.as_mut() {
m.prompt = Some(prompt);
}
return Ok(Action::None);
}
let board_name = self.active_board_name().to_string();
let action_msg = {
let board_config = self.store.config.kanban.configurations.get_mut(&board_name);
let Some(board_config) = board_config else {
return Ok(Action::None);
};
match row {
0 => {
board_config.wrap_titles = !board_config.wrap_titles;
if board_config.wrap_titles {
"Wrap titles: on".to_string()
} else {
"Wrap titles: off".to_string()
}
}
1 => {
board_config.title_lines = board_config.title_lines % 5 + 1;
format!("Title lines: {}", board_config.title_lines)
}
2 => {
board_config.order_by = board_config.order_by.next();
format!("Order by: {}", board_config.order_by.as_str())
}
_ => return Ok(Action::None),
}
};
if !self.persist_config() {
return Ok(Action::None);
}
self.rebuild_board();
self.status_msg = Some(action_msg);
Ok(Action::None)
}
pub(in crate::app) fn settings_commit_add_column(&mut self, name: String) {
let name = name.trim().to_string();
if name.is_empty() {
self.status_msg = Some("Column name is required".to_string());
return;
}
let board_name = self.active_board_name().to_string();
let board_config = self.store.config.kanban.configurations.get_mut(&board_name);
let Some(board_config) = board_config else {
self.status_msg = Some("board not found".to_string());
return;
};
if board_config.columns.iter().any(|c| c == &name) {
self.status_msg = Some(format!("Column '{name}' already exists"));
return;
}
board_config.columns.push(name.clone());
if !self.persist_config() {
return;
}
self.rebuild_board();
let last = self.active_board_config().columns.len().saturating_sub(1);
if let Some(ref mut m) = self.settings {
m.prompt = None;
m.selected = last;
}
self.status_msg = Some(format!("Added column '{name}'"));
}
pub(in crate::app) fn settings_commit_new_board(&mut self, name: String) {
let name = name.trim().to_string();
if name.is_empty() {
self.status_msg = Some("Board name is required".to_string());
return;
}
if self.store.config.kanban.configurations.contains_key(&name) {
self.status_msg = Some(format!("Board '{name}' already exists"));
return;
}
if let Err(e) = self.ensure_board_store() {
self.status_msg = Some(format!("Cannot create docs/boards/: {e}"));
return;
}
self.store
.config
.kanban
.configurations
.insert(name.clone(), crate::config::BoardConfiguration::default());
if !self.persist_config() {
return;
}
self.reload_tabs();
let idx = self
.store
.config
.configs()
.position(|(n, _)| n == name.as_str())
.unwrap_or(0);
self.active_config = idx;
self.tabs.selected = idx;
self.switch_config();
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
m.selected = 0;
}
self.status_msg = Some(format!("Added board '{name}'"));
}
pub(in crate::app) fn ensure_board_store(&self) -> Result<()> {
let store = crate::board_store::BoardStore::new(&self.project_root());
if store.exists() {
return Ok(());
}
std::fs::create_dir_all(store.dir())?;
Ok(())
}
pub(in crate::app) fn settings_commit_rename_board(&mut self, old: String, new: String) {
let new = new.trim().to_string();
if new.is_empty() {
self.status_msg = Some("Board name is required".to_string());
return;
}
if new == old {
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
}
return;
}
if self.store.config.kanban.configurations.contains_key(&new) {
self.status_msg = Some(format!("Board '{new}' already exists"));
return;
}
let board_store = crate::board_store::BoardStore::new(&self.project_root());
if board_store.exists()
&& board_store.get(&old).is_ok()
&& let Err(e) = board_store.rename(&old, &new)
{
self.status_msg = Some(format!("Cannot rename board '{old}': {e}"));
return;
}
let Some(idx) = self.store.config.kanban.configurations.get_index_of(&old) else {
self.status_msg = Some(format!("Board '{old}' not found"));
return;
};
if let Some(board) = self.store.config.kanban.configurations.shift_remove(&old) {
self.store
.config
.kanban
.configurations
.shift_insert(idx, new.clone(), board);
}
if !self.persist_config() {
return;
}
self.reload_tabs();
self.active_config = idx.min(self.tabs.names.len().saturating_sub(1));
self.tabs.selected = self.active_config;
self.switch_config();
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
m.selected = 0;
}
self.status_msg = Some(format!("Renamed board '{old}' to '{new}'"));
}
pub(in crate::app) fn settings_commit_editor(&mut self, value: String) {
let value = value.trim().to_string();
self.store.config.editor = if value.is_empty() {
None
} else {
Some(value.clone())
};
if !self.persist_config() {
return;
}
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
}
self.status_msg = Some(match value.is_empty() {
true => "Editor: (default)".to_string(),
false => format!("Editor: {value}"),
});
}
pub(in crate::app) fn viewer_label(&self) -> String {
match self.store.config.viewer.as_deref().map(str::trim) {
Some(name) if !name.is_empty() && name != "auto" => name.to_string(),
_ => match editor::resolve_viewer(None) {
Ok(name) => format!("{name} (auto)"),
Err(_) => format!(
"(none — install one of {})",
editor::VIEWER_CANDIDATES.join(", ")
),
},
}
}
pub(in crate::app) fn settings_commit_viewer(&mut self, value: String) {
let value = value.trim().to_string();
self.store.config.viewer = if value.is_empty() {
None
} else {
Some(value.clone())
};
if !self.persist_config() {
return;
}
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
}
self.status_msg = Some(match value.is_empty() {
true => "Viewer: (auto)".to_string(),
false => format!("Viewer: {value}"),
});
}
pub(in crate::app) fn settings_commit_delete_board(&mut self, name: String) {
if self.store.config.kanban.configurations.len() <= 1 {
self.status_msg = Some("Cannot delete the last board".to_string());
return;
}
if !self.store.config.kanban.configurations.contains_key(&name) {
self.status_msg = Some(format!("Board '{name}' not found"));
return;
}
let idx = self.active_config;
let board_store = crate::board_store::BoardStore::new(&self.project_root());
if board_store.exists() {
match board_store.delete(&name) {
Ok(()) | Err(TixError::NotFound(_)) => {}
Err(e) => {
self.status_msg = Some(format!("Cannot delete board '{name}': {e}"));
return;
}
}
}
self.store.config.kanban.configurations.shift_remove(&name);
if !self.persist_config() {
return;
}
self.reload_tabs();
let count = self.store.config.kanban.configurations.len();
let target = idx.min(count.saturating_sub(1));
self.active_config = target;
self.tabs.selected = target;
self.switch_config();
if let Some(m) = self.settings.as_mut() {
m.prompt = None;
m.selected = 0;
}
self.status_msg = Some(format!("Deleted board '{name}'"));
}
pub(in crate::app) fn persist_config(&mut self) -> bool {
match self.store.config_path() {
Ok(path) => match self.store.config.save(&path) {
Ok(()) => true,
Err(e) => {
self.status_msg = Some(format!("settings save: {e}"));
false
}
},
Err(e) => {
self.status_msg = Some(format!("settings path: {e}"));
false
}
}
}
pub(in crate::app) fn reload_tabs(&mut self) {
let names: Vec<String> = self
.store
.config
.configs()
.map(|(name, _)| name.to_string())
.collect();
self.tabs = BoardTabs::new(names);
}
pub(in crate::app) fn active_board_name(&self) -> &str {
self.store
.config
.configs()
.nth(self.active_config)
.map(|(name, _)| name)
.unwrap_or("default")
}
}