use super::{
AnimationState, DialogInputState, FilterState, FocusState, MultiSelectState, PersistenceState,
RelationshipState, SelectionHub, SprintViewState, UiState, ViewState,
};
use crate::app::AppMode;
use crate::tui_context::TuiContext;
use kanban_core::{AppConfig, InputState};
use kanban_service::StoreManager;
use kanban_view::board_list::BoardList;
use kanban_view::model::Model;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
pub(in crate::app) fn default_store_manager() -> StoreManager {
let mut registry = kanban_persistence::StoreRegistry::new();
let mut backends = kanban_backend::KanbanBackendRegistry::new();
registry.register(Box::new(kanban_persistence_sqlite::SqliteStoreFactory));
backends.register(Box::new(kanban_persistence_sqlite::SqliteBackendFactory));
registry.register(Box::new(kanban_persistence_json::JsonStoreFactory));
backends.register(Box::new(kanban_persistence_json::JsonBackendFactory));
StoreManager::new(registry, backends)
}
pub struct App {
pub store_manager: Arc<StoreManager>,
pub should_quit: bool,
pub quit_with_pending: bool, pub quit_with_migration: bool, pub mode: AppMode,
pub mode_stack: Vec<AppMode>,
pub input: InputState,
pub ctx: TuiContext,
pub app_config: AppConfig,
pub selection: SelectionHub,
pub board_list: BoardList,
pub animation: AnimationState,
pub filter: FilterState,
pub dialog_input: DialogInputState,
pub focus: FocusState,
pub persistence: PersistenceState,
pub multi_select: MultiSelectState,
pub ui_state: UiState,
pub sprint_view: SprintViewState,
pub view: ViewState,
pub model: Model,
pub relationship: RelationshipState,
pub save_error: Option<String>,
pub pending_key: Option<char>,
pub has_data_file: bool,
pub cli_file_provided: bool,
pub cli_file_override: bool,
pub config_storage_backend: String,
pub config_storage_location: String,
pub original_storage_backend: Option<String>,
pub original_storage_location: Option<String>,
pub export_dialog: Option<ExportDialogState>,
pub migration_state: MigrationState,
pub export_result_rx: Option<tokio::sync::oneshot::Receiver<Result<String, String>>>,
pub needs_redraw: bool,
pub error_log: Arc<Mutex<crate::error_log::ErrorLogState>>,
pub auto_open_seen_count: usize,
pub(crate) choose_storage_backend: StorageBackendChoice,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum StorageBackendChoice {
#[default]
Json,
Sqlite,
}
impl StorageBackendChoice {
pub(crate) fn extension(self) -> &'static str {
match self {
Self::Json => ".json",
Self::Sqlite => ".sqlite",
}
}
pub(crate) fn toggle(self) -> Self {
match self {
Self::Json => Self::Sqlite,
Self::Sqlite => Self::Json,
}
}
}
pub(crate) fn swap_known_extension(filename: &str, new_ext: &str) -> String {
const KNOWN: &[&str] = &[".json", ".sqlite", ".sqlite3", ".db"];
for ext in KNOWN {
if let Some(stem) = filename.strip_suffix(ext) {
return format!("{}{}", stem, new_ext);
}
}
format!("{}{}", filename, new_ext)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExportStep {
SelectBoards,
ExportOptions,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ExportFormat {
#[default]
Json,
Sqlite,
}
#[derive(Debug, Clone)]
pub struct ExportDialogState {
pub board_ids: Vec<Uuid>,
pub board_selections: Vec<bool>,
pub cursor: usize,
pub step: ExportStep,
pub format: ExportFormat,
pub filename: String,
}
impl ExportDialogState {
pub fn new(board_ids: Vec<Uuid>) -> Self {
Self::from_selection(&board_ids, &HashSet::new())
}
pub fn from_selection(all_live_board_ids: &[Uuid], preselected: &HashSet<Uuid>) -> Self {
let board_ids = all_live_board_ids.to_vec();
let board_selections = if preselected.is_empty() {
vec![false; board_ids.len()]
} else {
board_ids
.iter()
.map(|id| preselected.contains(id))
.collect()
};
Self {
board_ids,
board_selections,
cursor: 0,
step: ExportStep::SelectBoards,
format: ExportFormat::default(),
filename: "export.json".to_string(),
}
}
pub fn toggle(&mut self, index: usize) {
if let Some(selected) = self.board_selections.get_mut(index) {
*selected = !*selected;
}
}
pub fn select_all(&mut self) {
let all_selected = self.board_selections.iter().all(|&s| s);
for s in &mut self.board_selections {
*s = !all_selected;
}
}
pub fn any_selected(&self) -> bool {
self.board_selections.iter().any(|&s| s)
}
}
pub enum MigrationState {
Idle,
Migrating {
old_config: Box<AppConfig>,
old_storage_location: String,
result_rx: tokio::sync::oneshot::Receiver<Result<bool, String>>,
},
}
pub enum CardField {
Title,
Description,
}
pub enum BoardField {
Name,
Description,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_swap_known_extension_table() {
let cases = [
("boards.json", ".sqlite", "boards.sqlite"),
("boards.sqlite", ".json", "boards.json"),
("boards.sqlite3", ".json", "boards.json"),
("boards.db", ".json", "boards.json"),
("boards", ".sqlite", "boards.sqlite"),
("", ".json", ".json"),
("foo.tar.json", ".sqlite", "foo.tar.sqlite"),
("FOO.JSON", ".sqlite", "FOO.JSON.sqlite"),
];
for (input, ext, expected) in cases {
assert_eq!(
swap_known_extension(input, ext),
expected,
"swap_known_extension({:?}, {:?})",
input,
ext
);
}
}
}