use bmux_text_edit::{SelectionMode, TextEditBuffer, TextMotion};
use bmux_tui::event::Event;
use bmux_tui::geometry::Rect;
use bmux_tui_components::form::{Form, FormFieldItem, FormOutcome, FormPolicy, FormState};
use bmux_tui_components::text_input::{TextInputControl, TextInputPolicy, TextInputState};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorktreeCreateFocus {
Name,
Target,
Base,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeCreateSubmission {
pub name: String,
pub target: WorktreeCreateTarget,
pub base: WorktreeCreateBase,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorktreeCreateDialogOutcome {
Handled,
Create(WorktreeCreateSubmission),
Canceled,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeCreateDialog {
name: TextInputState,
target: WorktreeCreateTarget,
base: WorktreeCreateBase,
focus: WorktreeCreateFocus,
status: String,
current_session_available: bool,
form: FormState,
}
impl WorktreeCreateDialog {
#[must_use]
pub fn new(default_name: &str, current_session_available: bool) -> Self {
let mut name = TextEditBuffer::from_text(default_name);
name.move_cursor_with_selection(TextMotion::Start, SelectionMode::Extend);
let target = if current_session_available {
WorktreeCreateTarget::CurrentSession
} else {
WorktreeCreateTarget::NewSession
};
Self {
name: TextInputState::new(name),
target,
base: WorktreeCreateBase::Head,
focus: WorktreeCreateFocus::Name,
status: "Type a name, review target and base, then confirm to create.".to_owned(),
current_session_available,
form: FormState::new(Some(0)),
}
}
#[must_use]
pub const fn focus(&self) -> WorktreeCreateFocus {
self.focus
}
#[must_use]
pub const fn name(&self) -> &TextInputState {
&self.name
}
pub fn set_name_content_area(&mut self, area: Rect) {
self.name.set_content_area(area, &name_input_policy());
}
#[must_use]
pub const fn target(&self) -> WorktreeCreateTarget {
self.target
}
#[must_use]
pub const fn base(&self) -> WorktreeCreateBase {
self.base
}
#[must_use]
pub fn status(&self) -> &str {
&self.status
}
pub fn set_status(&mut self, status: String) {
self.status = status;
}
#[must_use]
pub fn name_text(&self) -> String {
self.name.buffer().text().trim().to_owned()
}
pub fn handle_event(
&mut self,
event: &Event,
keymap: &super::keymap::BmuxKeyMap,
) -> WorktreeCreateDialogOutcome {
match event {
Event::Paste(text) if self.focus == WorktreeCreateFocus::Name => {
let _ =
TextInputControl::new(&name_input_policy()).handle_paste(&mut self.name, text);
}
Event::Key(stroke) => match keymap.action("worktree_create", *stroke) {
Some("tui.control.cancel") => return WorktreeCreateDialogOutcome::Canceled,
Some("tui.control.focus_next") => self.focus_next(),
Some("tui.control.submit") => {
let name = self.name_text();
if name.is_empty() {
self.set_status("worktree name is required".to_owned());
} else {
return WorktreeCreateDialogOutcome::Create(WorktreeCreateSubmission {
name,
target: self.target,
base: self.base,
});
}
}
Some("tui.control.left") if self.focus != WorktreeCreateFocus::Name => {
self.previous_choice();
}
Some("tui.control.right") if self.focus != WorktreeCreateFocus::Name => {
self.next_choice();
}
_ if self.focus == WorktreeCreateFocus::Name => {
let _ = super::text_input_flow::handle_key_in_scope(
&mut self.name,
keymap,
"worktree_create",
*stroke,
);
}
_ => {}
},
Event::Mouse(mouse) if self.focus == WorktreeCreateFocus::Name => {
let _ = TextInputControl::new(&name_input_policy())
.handle_mouse(&mut self.name, *mouse);
}
Event::Focus(_)
| Event::Resize(_)
| Event::Tick
| Event::User(_)
| Event::Paste(_)
| Event::Mouse(_) => {}
}
WorktreeCreateDialogOutcome::Handled
}
pub fn focus_next(&mut self) {
let fields = form_fields();
let name = self.name.buffer().text().to_owned();
let values = [
Some(name.as_str()),
Some(self.target.label()),
Some(self.base.label()),
];
if let FormOutcome::Focused(index) = Form::new(&fields, &values)
.policy(FormPolicy::wrapping())
.handle_event(&mut self.form, &bmux_tui::event::Event::Key(tab_stroke()))
{
self.focus = match index {
1 => WorktreeCreateFocus::Target,
2 => WorktreeCreateFocus::Base,
_ => WorktreeCreateFocus::Name,
};
}
}
pub const fn previous_choice(&mut self) {
match self.focus {
WorktreeCreateFocus::Name => {}
WorktreeCreateFocus::Target => self.previous_target(),
WorktreeCreateFocus::Base => self.previous_base(),
}
}
pub const fn next_choice(&mut self) {
match self.focus {
WorktreeCreateFocus::Name => {}
WorktreeCreateFocus::Target => self.next_target(),
WorktreeCreateFocus::Base => self.next_base(),
}
}
const fn previous_target(&mut self) {
self.target = self.target.previous(self.current_session_available);
}
const fn next_target(&mut self) {
self.target = self.target.next(self.current_session_available);
}
const fn previous_base(&mut self) {
self.base = self.base.previous();
}
const fn next_base(&mut self) {
self.base = self.base.next();
}
}
#[must_use]
pub const fn name_input_policy() -> TextInputPolicy {
TextInputPolicy::chat_composer()
}
fn form_fields() -> [FormFieldItem; 3] {
[
FormFieldItem::new("name").required(true),
FormFieldItem::new("target"),
FormFieldItem::new("base"),
]
}
const fn tab_stroke() -> bmux_keyboard::KeyStroke {
bmux_keyboard::KeyStroke::simple(bmux_keyboard::KeyCode::Tab)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorktreeCreateTarget {
CurrentSession,
NewSession,
}
impl WorktreeCreateTarget {
const fn previous(self, current_session_available: bool) -> Self {
self.next(current_session_available)
}
const fn next(self, current_session_available: bool) -> Self {
if !current_session_available {
return Self::NewSession;
}
match self {
Self::CurrentSession => Self::NewSession,
Self::NewSession => Self::CurrentSession,
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::CurrentSession => "current_session",
Self::NewSession => "new_session",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorktreeCreateBase {
Auto,
DefaultBranch,
Head,
}
impl WorktreeCreateBase {
const fn previous(self) -> Self {
match self {
Self::Auto => Self::Head,
Self::DefaultBranch => Self::Auto,
Self::Head => Self::DefaultBranch,
}
}
const fn next(self) -> Self {
match self {
Self::Auto => Self::DefaultBranch,
Self::DefaultBranch => Self::Head,
Self::Head => Self::Auto,
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::DefaultBranch => "default_branch",
Self::Head => "head",
}
}
#[must_use]
pub const fn model(self) -> bcode_worktree_models::WorktreeBaseRef {
match self {
Self::Auto => bcode_worktree_models::WorktreeBaseRef::Auto,
Self::DefaultBranch => bcode_worktree_models::WorktreeBaseRef::DefaultBranch,
Self::Head => bcode_worktree_models::WorktreeBaseRef::Head,
}
}
}
#[cfg(test)]
mod tests {
use super::{WorktreeCreateDialog, WorktreeCreateDialogOutcome, WorktreeCreateFocus};
use bmux_keyboard::{KeyCode, KeyStroke};
use bmux_tui::event::Event;
fn key(code: KeyCode) -> Event {
Event::Key(KeyStroke::simple(code))
}
#[test]
fn form_state_drives_wrapping_focus_order() {
let mut dialog = WorktreeCreateDialog::new("work", true);
assert_eq!(dialog.focus(), WorktreeCreateFocus::Name);
dialog.focus_next();
assert_eq!(dialog.focus(), WorktreeCreateFocus::Target);
dialog.focus_next();
assert_eq!(dialog.focus(), WorktreeCreateFocus::Base);
dialog.focus_next();
assert_eq!(dialog.focus(), WorktreeCreateFocus::Name);
}
#[test]
fn event_policy_owns_validation_focus_choices_submission_and_cancel() {
let keymap =
super::super::keymap::BmuxKeyMap::from_config(&bcode_config::TuiConfig::default());
let mut empty = WorktreeCreateDialog::new("", true);
assert_eq!(
empty.handle_event(&key(KeyCode::Enter), &keymap),
WorktreeCreateDialogOutcome::Handled
);
assert_eq!(empty.status(), "worktree name is required");
let mut dialog = WorktreeCreateDialog::new("work", true);
assert_eq!(
dialog.handle_event(&key(KeyCode::Tab), &keymap),
WorktreeCreateDialogOutcome::Handled
);
assert_eq!(dialog.focus(), WorktreeCreateFocus::Target);
assert_eq!(
dialog.handle_event(&key(KeyCode::Right), &keymap),
WorktreeCreateDialogOutcome::Handled
);
assert!(matches!(
dialog.handle_event(&key(KeyCode::Enter), &keymap),
WorktreeCreateDialogOutcome::Create(_)
));
assert_eq!(
dialog.handle_event(&key(KeyCode::Escape), &keymap),
WorktreeCreateDialogOutcome::Canceled
);
}
}