use anyhow::Context;
use crossterm::event::Event;
use ratatui::prelude::*;
use ratatui_textarea::TextArea;
use super::screens::ButtonScreen;
use super::widgets::{self, KeyResult};
use crate::model::changeset::ChangeType;
use crate::package_manager::Project;
mod enter_message;
mod select_projects;
mod single_package;
#[derive(Debug, Clone)]
pub struct ChangeResult {
pub projects: Vec<(Project, ChangeType)>,
pub message: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ChangeOptions {
pub change_type: Option<ChangeType>,
pub projects: Option<Vec<usize>>,
}
#[derive(Debug)]
pub(crate) struct SelectProjectsState {
pub(crate) selected: Vec<bool>,
pub(crate) levels: Vec<ChangeType>,
pub(crate) cursor: usize,
pub(crate) error: bool,
pub(crate) changed_count: usize,
}
pub(crate) enum BackState {
MultiPackage(SelectProjectsState),
SinglePackage { level: ChangeType },
}
pub(crate) enum Screen {
SelectProjects(SelectProjectsState),
SinglePackage {
level: ChangeType,
},
EnterMessage {
textarea: Box<TextArea<'static>>,
projects: Vec<(Project, ChangeType)>,
back: BackState,
},
}
pub(super) type HandleResult = KeyResult<Screen, ChangeResult>;
pub(crate) struct ReorderedProjects {
pub(crate) projects: Vec<Project>,
pub(crate) changed_count: usize,
pub(crate) orig_to_new: Vec<usize>,
}
pub(crate) fn reorder_projects(projects: &[Project], changed_flags: &[bool]) -> ReorderedProjects {
let changed_count = changed_flags.iter().filter(|&&c| c).count();
let mut changed_pairs: Vec<(usize, Project)> = projects
.iter()
.enumerate()
.filter(|(i, _)| changed_flags[*i])
.map(|(i, p)| (i, p.clone()))
.collect();
let mut unchanged_pairs: Vec<(usize, Project)> = projects
.iter()
.enumerate()
.filter(|(i, _)| !changed_flags[*i])
.map(|(i, p)| (i, p.clone()))
.collect();
changed_pairs.sort_by(|a, b| a.1.name().cmp(b.1.name()));
unchanged_pairs.sort_by(|a, b| a.1.name().cmp(b.1.name()));
let reordered_pairs: Vec<(usize, Project)> =
changed_pairs.into_iter().chain(unchanged_pairs).collect();
let mut orig_to_new = vec![0usize; projects.len()];
for (new_idx, (orig_idx, _)) in reordered_pairs.iter().enumerate() {
orig_to_new[*orig_idx] = new_idx;
}
let reordered = reordered_pairs.into_iter().map(|(_, p)| p).collect();
ReorderedProjects {
projects: reordered,
changed_count,
orig_to_new,
}
}
pub(super) fn handle_event(
screen: Screen,
event: Event,
area: Rect,
projects: &[Project],
) -> anyhow::Result<HandleResult> {
match screen {
Screen::SelectProjects(state) => Ok(select_projects::handle_event_select_projects(
state, event, area, projects,
)),
Screen::SinglePackage { level } => {
let project = projects
.first()
.context("SinglePackage screen requires at least one project")?
.clone();
let buttons = single_package::SinglePackageButtons { level };
match buttons.handle_event(vec![project], event, area)? {
KeyResult::Continue((_, screen)) => Ok(KeyResult::Continue(screen)),
KeyResult::Complete(cr) => Ok(KeyResult::Complete(cr)),
KeyResult::Cancelled => Ok(KeyResult::Cancelled),
}
}
Screen::EnterMessage {
textarea,
projects: proj,
back,
} => enter_message::handle_event_enter_message(textarea, proj, back, event),
}
}
pub(crate) fn ui(frame: &mut Frame, screen: &Screen, project_names: &[&str]) {
let area = frame.area();
match screen {
Screen::SelectProjects(state) => {
select_projects::render_select_projects(frame, area, project_names, state);
}
Screen::SinglePackage { level } => {
single_package::SinglePackageButtons { level: *level }.render(frame, area);
}
Screen::EnterMessage { textarea, .. } => {
enter_message::render_enter_message(frame, area, textarea);
}
}
}
pub(crate) fn build_initial_screen(
ro: &ReorderedProjects,
project_indices: &[usize],
have_projects: bool,
) -> Screen {
if ro.projects.len() == 1 {
return Screen::SinglePackage {
level: ChangeType::Patch,
};
}
if have_projects {
let mut selected = vec![false; ro.projects.len()];
for &i in project_indices {
selected[i] = true;
}
Screen::SelectProjects(SelectProjectsState {
selected,
levels: vec![ChangeType::Patch; ro.projects.len()],
cursor: 0,
error: false,
changed_count: ro.changed_count,
})
} else {
let selected = (0..ro.projects.len())
.map(|i| i < ro.changed_count)
.collect();
Screen::SelectProjects(SelectProjectsState {
selected,
levels: vec![ChangeType::Patch; ro.projects.len()],
cursor: 0,
error: false,
changed_count: ro.changed_count,
})
}
}
pub fn run(
projects: &[Project],
options: &ChangeOptions,
changed: &[bool],
) -> anyhow::Result<Option<ChangeResult>> {
let changed_flags: Vec<bool> = if changed.len() == projects.len() {
changed.to_vec()
} else {
vec![true; projects.len()] };
let ro = reorder_projects(projects, &changed_flags);
let project_indices: Vec<usize> = match &options.projects {
Some(indices) => indices.iter().map(|&i| ro.orig_to_new[i]).collect(),
None if ro.projects.len() == 1 => vec![0],
_ => vec![], };
let have_projects = !project_indices.is_empty();
if let Some(change_type) = options.change_type {
let indices = if have_projects {
project_indices
} else if ro.changed_count > 0 {
(0..ro.changed_count).collect()
} else {
(0..ro.projects.len()).collect()
};
return Ok(Some(ChangeResult {
projects: indices
.into_iter()
.map(|i| (ro.projects[i].clone(), change_type))
.collect(),
message: None,
}));
}
let project_names: Vec<&str> = ro.projects.iter().map(|p| p.name()).collect();
let initial_screen = build_initial_screen(&ro, &project_indices, have_projects);
let result = widgets::run_tui(
initial_screen,
|frame, screen| ui(frame, screen, &project_names),
|screen, event, area| handle_event(screen, event, area, &ro.projects),
)?;
Ok(result)
}
#[cfg(test)]
pub(super) fn handle_key(
screen: Screen,
key: crossterm::event::KeyCode,
projects: &[Project],
) -> anyhow::Result<HandleResult> {
use crossterm::event::{KeyEvent, KeyModifiers};
handle_event(
screen,
Event::Key(KeyEvent::new(key, KeyModifiers::NONE)),
Rect::new(0, 0, 80, 24),
projects,
)
}
#[cfg(test)]
pub(crate) mod test_helpers {
use crate::package_manager::Project;
pub(crate) fn dummy_projects(n: usize) -> Vec<Project> {
(0..n)
.map(|i| {
Project::new_test(
&format!("project-{i}"),
&format!("/nonexistent/projects/project-{i}"),
)
})
.collect()
}
}
#[cfg(test)]
mod tests;