use trusty_mpm::client::ManagedSessionSummary;
use trusty_mpm::session_manager::rename::validate_session_name;
use super::super::picker_delete::{confirm_is_force, confirm_is_yes, delete_route_flags};
use super::new_session::{NewSessionFlow, NewSessionRequest, Step};
const PAGE: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Input {
Char(char),
Enter,
Backspace,
Escape,
Up,
Down,
Home,
End,
PageUp,
PageDown,
Cancel,
NameNew,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Action {
Redraw,
Ignore,
Quit,
Open(usize),
Delete {
index: usize,
force: bool,
stop_first: bool,
},
Rename {
index: usize,
name: String,
},
Refresh,
NewSession,
Create(NewSessionRequest),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Mode {
Browse,
Confirm {
index: usize,
force: bool,
stop_first: bool,
typed: String,
},
Rename {
index: usize,
typed: String,
},
Help,
New(NewSessionFlow),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Severity {
Info,
Error,
}
#[derive(Debug, Clone)]
pub(crate) struct TuiState {
mode: Mode,
selected: usize,
scroll: usize,
pinned_id: Option<String>,
message: Option<(String, Severity)>,
self_session_id: Option<String>,
self_tmux_name: Option<String>,
use_color: bool,
}
impl TuiState {
pub(crate) fn new(self_session_id: Option<String>, self_tmux_name: Option<String>) -> Self {
Self {
mode: Mode::Browse,
selected: 0,
scroll: 0,
pinned_id: None,
message: None,
self_session_id,
self_tmux_name,
use_color: true,
}
}
pub(crate) fn with_color(mut self, use_color: bool) -> Self {
self.use_color = use_color;
self
}
pub(crate) fn use_color(&self) -> bool {
self.use_color
}
pub(crate) fn mode(&self) -> &Mode {
&self.mode
}
pub(crate) fn selected(&self) -> usize {
self.selected
}
pub(crate) fn scroll(&self) -> usize {
self.scroll
}
pub(crate) fn set_scroll(&mut self, scroll: usize) {
self.scroll = scroll;
}
pub(crate) fn message(&self) -> Option<(&str, Severity)> {
self.message.as_ref().map(|(t, s)| (t.as_str(), *s))
}
pub(crate) fn set_message(&mut self, text: impl Into<String>, severity: Severity) {
self.message = Some((text.into(), severity));
}
pub(crate) fn sync(&mut self, sessions: &[ManagedSessionSummary]) {
if sessions.is_empty() {
self.selected = 0;
self.pinned_id = None;
self.mode = Mode::Browse;
return;
}
let by_pin = self
.pinned_id
.as_deref()
.and_then(|id| sessions.iter().position(|s| s.id == id));
self.selected = by_pin.unwrap_or_else(|| self.selected.min(sessions.len() - 1));
self.pinned_id = Some(sessions[self.selected].id.clone());
let target = match &self.mode {
Mode::Confirm { index, .. } | Mode::Rename { index, .. } => Some(*index),
_ => None,
};
if target.is_some_and(|i| i >= sessions.len()) {
self.mode = Mode::Browse;
}
}
pub(crate) fn apply(&mut self, input: Input, sessions: &[ManagedSessionSummary]) -> Action {
if input == Input::Cancel {
return Action::Quit;
}
if matches!(self.mode, Mode::New(_)) {
return self.apply_new_session(input);
}
match self.mode.clone() {
Mode::Browse => self.apply_browse(input, sessions),
Mode::Help => {
self.mode = Mode::Browse;
Action::Redraw
}
Mode::Confirm {
index,
force,
stop_first,
typed,
} => self.apply_confirm(input, index, force, stop_first, typed),
Mode::Rename { index, typed } => self.apply_rename(input, index, typed, sessions),
Mode::New(_) => Action::Ignore,
}
}
pub(crate) fn open_new_session(&mut self, flow: NewSessionFlow) {
self.mode = Mode::New(flow);
}
fn apply_new_session(&mut self, input: Input) -> Action {
let step = match &mut self.mode {
Mode::New(flow) => flow.apply(input),
_ => return Action::Ignore,
};
match step {
Step::Ignore => Action::Ignore,
Step::Redraw => Action::Redraw,
Step::Reject(msg) => {
self.set_message(msg, Severity::Error);
Action::Redraw
}
Step::Cancel => {
self.mode = Mode::Browse;
Action::Redraw
}
Step::Create(request) => {
self.mode = Mode::Browse;
Action::Create(request)
}
}
}
fn apply_browse(&mut self, input: Input, sessions: &[ManagedSessionSummary]) -> Action {
let last = sessions.len().saturating_sub(1);
match input {
Input::Char('q') | Input::Escape => Action::Quit,
Input::Char('?') => {
self.mode = Mode::Help;
Action::Redraw
}
Input::Up | Input::Char('k') => self.move_to(self.selected.saturating_sub(1), sessions),
Input::Down | Input::Char('j') => self.move_to((self.selected + 1).min(last), sessions),
Input::Home | Input::Char('g') => self.move_to(0, sessions),
Input::End | Input::Char('G') => self.move_to(last, sessions),
Input::PageUp => self.move_to(self.selected.saturating_sub(PAGE), sessions),
Input::PageDown => self.move_to((self.selected + PAGE).min(last), sessions),
Input::Char('R') => Action::Refresh,
Input::Enter => match sessions.is_empty() {
true => Action::Ignore,
false => Action::Open(self.selected),
},
Input::Char('d') => self.begin_delete(sessions),
Input::Char('r') => self.begin_rename(sessions),
Input::Char('n') => Action::NewSession,
_ => Action::Ignore,
}
}
fn move_to(&mut self, index: usize, sessions: &[ManagedSessionSummary]) -> Action {
if sessions.is_empty() || index == self.selected {
return Action::Ignore;
}
self.selected = index;
self.pinned_id = sessions.get(index).map(|s| s.id.clone());
Action::Redraw
}
fn begin_delete(&mut self, sessions: &[ManagedSessionSummary]) -> Action {
let Some(session) = sessions.get(self.selected) else {
return Action::Ignore;
};
if is_self_session(
session,
self.self_session_id.as_deref(),
self.self_tmux_name.as_deref(),
) {
self.set_message(
format!(
"'{}' is the session this terminal is attached to — detach first, \
then delete it",
session.name
),
Severity::Error,
);
return Action::Redraw;
}
let (force, stop_first) = delete_route_flags(&session.state);
self.mode = Mode::Confirm {
index: self.selected,
force,
stop_first,
typed: String::new(),
};
self.message = None;
Action::Redraw
}
fn begin_rename(&mut self, sessions: &[ManagedSessionSummary]) -> Action {
let Some(session) = sessions.get(self.selected) else {
return Action::Ignore;
};
self.mode = Mode::Rename {
index: self.selected,
typed: session.name.clone(),
};
self.message = None;
Action::Redraw
}
fn apply_confirm(
&mut self,
input: Input,
index: usize,
force: bool,
stop_first: bool,
mut typed: String,
) -> Action {
match input {
Input::Escape => {
self.mode = Mode::Browse;
self.set_message("delete cancelled", Severity::Info);
Action::Redraw
}
Input::Char(c) => {
typed.push(c);
self.mode = Mode::Confirm {
index,
force,
stop_first,
typed,
};
Action::Redraw
}
Input::Backspace => {
typed.pop();
self.mode = Mode::Confirm {
index,
force,
stop_first,
typed,
};
Action::Redraw
}
Input::Enter => {
let confirmed = match force {
true => confirm_is_force(&typed),
false => confirm_is_yes(&typed),
};
self.mode = Mode::Browse;
if !confirmed {
self.set_message("delete cancelled", Severity::Info);
return Action::Redraw;
}
Action::Delete {
index,
force,
stop_first,
}
}
_ => Action::Ignore,
}
}
fn apply_rename(
&mut self,
input: Input,
index: usize,
mut typed: String,
sessions: &[ManagedSessionSummary],
) -> Action {
match input {
Input::Escape => {
self.mode = Mode::Browse;
self.set_message("rename cancelled", Severity::Info);
Action::Redraw
}
Input::Char(c) => {
typed.push(c);
self.mode = Mode::Rename { index, typed };
Action::Redraw
}
Input::Backspace => {
typed.pop();
self.mode = Mode::Rename { index, typed };
Action::Redraw
}
Input::Enter => match validate_session_name(&typed) {
Err(msg) => {
self.mode = Mode::Rename { index, typed };
self.set_message(msg, Severity::Error);
Action::Redraw
}
Ok(name) if sessions.get(index).is_some_and(|s| s.name == name) => {
self.mode = Mode::Browse;
self.set_message("name unchanged", Severity::Info);
Action::Redraw
}
Ok(name) => {
self.mode = Mode::Browse;
Action::Rename { index, name }
}
},
_ => Action::Ignore,
}
}
}
pub(crate) fn is_self_session(
session: &ManagedSessionSummary,
self_id: Option<&str>,
self_tmux_name: Option<&str>,
) -> bool {
self_id.is_some_and(|id| id == session.id)
|| self_tmux_name.is_some_and(|name| name == session.name)
}