use cursive::Cursive;
use cursive::direction::Orientation;
use cursive::event::Key;
use cursive::view::{Nameable, Resizable, Scrollable};
use cursive::views::{Dialog, DummyView, EditView, LinearLayout, OnEventView, TextView};
use crate::messages::UiAction;
use crate::model::MethodCallState;
use crate::types::{MethodArgument, MethodCallOutcome, MethodSignature};
use super::{TuiState, dispatch_action};
const ID_METHOD_DIALOG: &str = "method_dialog";
const ID_METHOD_BODY: &str = "method_dialog_body";
const ID_METHOD_CALL_ERROR: &str = "method_call_error";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MethodPhase {
Loading,
Failed,
Inputs,
Calling,
Result,
}
pub(super) fn phase_of(state: &MethodCallState) -> MethodPhase {
match state {
MethodCallState::Loading { .. } => MethodPhase::Loading,
MethodCallState::Failed { .. } => MethodPhase::Failed,
MethodCallState::Inputs { .. } => MethodPhase::Inputs,
MethodCallState::Calling { .. } => MethodPhase::Calling,
MethodCallState::Result { .. } => MethodPhase::Result,
}
}
pub fn show(siv: &mut Cursive) {
let body = TextView::new("").with_name(ID_METHOD_BODY);
let dialog = Dialog::around(LinearLayout::new(Orientation::Vertical).child(body))
.title("Call Method")
.with_name(ID_METHOD_DIALOG);
let wrapped = OnEventView::new(dialog).on_pre_event(Key::Esc, |s| {
dispatch_action(s, UiAction::CloseMethodCall);
});
siv.add_layer(wrapped);
if let Some(st) = siv.user_data::<TuiState>() {
st.method_dialog_open = true;
st.method_dialog_phase = None;
}
refresh(siv);
}
pub fn close(siv: &mut Cursive) {
siv.pop_layer();
if let Some(st) = siv.user_data::<TuiState>() {
st.method_dialog_open = false;
st.method_dialog_phase = None;
}
}
pub fn refresh(siv: &mut Cursive) {
let snap = siv
.user_data::<TuiState>()
.and_then(|st| st.engine.model.method_call.clone());
let Some(state) = snap else {
return;
};
let new_phase = phase_of(&state);
let old_phase = siv
.user_data::<TuiState>()
.and_then(|st| st.method_dialog_phase);
if old_phase != Some(new_phase) {
rebuild_dialog(siv, &state);
if let Some(st) = siv.user_data::<TuiState>() {
st.method_dialog_phase = Some(new_phase);
}
} else if let MethodCallState::Inputs { field_errors, call_error, .. } = &state {
update_input_errors(siv, field_errors, call_error.as_deref());
}
}
fn rebuild_dialog(siv: &mut Cursive, state: &MethodCallState) {
let title = build_title(state);
siv.call_on_name(ID_METHOD_DIALOG, |d: &mut Dialog| {
d.set_title(title.clone());
d.clear_buttons();
match state {
MethodCallState::Loading { .. } => {
d.add_button("Close", |s| dispatch_action(s, UiAction::CloseMethodCall));
}
MethodCallState::Failed { .. } => {
d.add_button("Close", |s| dispatch_action(s, UiAction::CloseMethodCall));
}
MethodCallState::Inputs { .. } => {
d.add_button("Call", |s| dispatch_action(s, UiAction::CallMethodConfirmed));
d.add_button("Close", |s| dispatch_action(s, UiAction::CloseMethodCall));
}
MethodCallState::Calling { .. } => {
d.add_button("Close", |s| dispatch_action(s, UiAction::CloseMethodCall));
}
MethodCallState::Result { .. } => {
d.add_button("Call again", |s| dispatch_action(s, UiAction::CallMethodConfirmed));
d.add_button("Close", |s| dispatch_action(s, UiAction::CloseMethodCall));
}
}
});
let layout = build_body(state).min_size((60, 8)).max_size((100, 24)).scrollable();
siv.call_on_name(ID_METHOD_DIALOG, |d: &mut Dialog| {
d.set_content(layout);
});
if matches!(state, MethodCallState::Inputs { .. }) {
siv.focus_name(&arg_input_id(0)).ok();
}
}
fn build_title(state: &MethodCallState) -> String {
let display = match state {
MethodCallState::Loading { node } => node.to_string(),
MethodCallState::Failed { node, .. } => node.to_string(),
MethodCallState::Inputs { signature, .. }
| MethodCallState::Calling { signature, .. }
| MethodCallState::Result { signature, .. } => signature.method_display_name.clone(),
};
format!("Call Method · {display}")
}
fn build_body(state: &MethodCallState) -> LinearLayout {
match state {
MethodCallState::Loading { .. } => single_text("Reading method signature…"),
MethodCallState::Failed { error, .. } => single_text(&format!("Error: {error}")),
MethodCallState::Inputs {
signature,
edited,
field_errors,
call_error,
..
} => build_inputs_body(signature, edited, field_errors, call_error.as_deref()),
MethodCallState::Calling {
signature, edited, ..
} => build_calling_body(signature, edited),
MethodCallState::Result {
signature,
edited,
outcome,
..
} => build_result_body(signature, edited, outcome),
}
}
fn single_text(text: &str) -> LinearLayout {
LinearLayout::new(Orientation::Vertical)
.child(TextView::new(text.to_string()))
}
fn build_inputs_body(
signature: &MethodSignature,
edited: &[String],
field_errors: &[Option<String>],
call_error: Option<&str>,
) -> LinearLayout {
let mut layout = LinearLayout::new(Orientation::Vertical);
layout.add_child(TextView::new(format!(
"Method: {}\nParent object: {}",
signature.method_node, signature.parent_object,
)));
layout.add_child(DummyView.fixed_height(1));
if signature.inputs.is_empty() {
layout.add_child(TextView::new("(no input arguments)"));
} else {
layout.add_child(TextView::new("Inputs:"));
for (i, arg) in signature.inputs.iter().enumerate() {
layout.add_child(input_row(i, arg, edited.get(i).map(String::as_str).unwrap_or("")));
let err_text = field_errors
.get(i)
.and_then(|e| e.as_deref())
.unwrap_or("");
layout.add_child(
TextView::new(err_text.to_string()).with_name(arg_error_id(i)),
);
if !arg.description.is_empty() {
layout.add_child(TextView::new(format!(" {}", arg.description)));
}
}
}
layout.add_child(DummyView.fixed_height(1));
if !signature.outputs.is_empty() {
layout.add_child(TextView::new("Output signature:"));
for arg in &signature.outputs {
layout.add_child(TextView::new(format!(
" {} ({})",
arg.name, arg.type_label
)));
}
layout.add_child(DummyView.fixed_height(1));
}
layout.add_child(
TextView::new(call_error.unwrap_or("").to_string()).with_name(ID_METHOD_CALL_ERROR),
);
layout
}
fn build_calling_body(signature: &MethodSignature, edited: &[String]) -> LinearLayout {
let mut layout = LinearLayout::new(Orientation::Vertical);
layout.add_child(TextView::new(format!(
"Method: {}",
signature.method_display_name
)));
layout.add_child(DummyView.fixed_height(1));
for (i, arg) in signature.inputs.iter().enumerate() {
let val = edited.get(i).map(String::as_str).unwrap_or("");
layout.add_child(TextView::new(format!(
" {} ({}): {}",
arg.name, arg.type_label, val
)));
}
layout.add_child(DummyView.fixed_height(1));
layout.add_child(TextView::new("Calling…"));
layout
}
fn build_result_body(
signature: &MethodSignature,
edited: &[String],
outcome: &MethodCallOutcome,
) -> LinearLayout {
let mut layout = LinearLayout::new(Orientation::Vertical);
layout.add_child(TextView::new(format!(
"Method: {}\nParent object: {}",
signature.method_node, signature.parent_object,
)));
layout.add_child(DummyView.fixed_height(1));
if signature.inputs.is_empty() {
layout.add_child(TextView::new("(no input arguments)"));
} else {
layout.add_child(TextView::new("Inputs:"));
for (i, arg) in signature.inputs.iter().enumerate() {
layout.add_child(input_row(i, arg, edited.get(i).map(String::as_str).unwrap_or("")));
let server_err = outcome
.input_arg_errors
.get(i)
.and_then(|e| e.as_deref())
.unwrap_or("");
layout.add_child(
TextView::new(server_err.to_string()).with_name(arg_error_id(i)),
);
if !arg.description.is_empty() {
layout.add_child(TextView::new(format!(" {}", arg.description)));
}
}
}
layout.add_child(DummyView.fixed_height(1));
layout.add_child(TextView::new(format!("Status: {}", outcome.status)));
layout.add_child(DummyView.fixed_height(1));
if signature.outputs.is_empty() {
layout.add_child(TextView::new("(no output arguments)"));
} else {
layout.add_child(TextView::new("Outputs:"));
for (i, arg) in signature.outputs.iter().enumerate() {
let rendered = outcome
.outputs
.get(i)
.map(|v| v.format_inline())
.unwrap_or_else(|| "<missing>".to_string());
layout.add_child(TextView::new(format!(
" {} ({}): {}",
arg.name, arg.type_label, rendered
)));
}
}
layout.add_child(DummyView.fixed_height(1));
layout.add_child(
TextView::new(String::new()).with_name(ID_METHOD_CALL_ERROR),
);
layout
}
fn input_row(index: usize, arg: &MethodArgument, current: &str) -> LinearLayout {
let label = format!(" {} ({}): ", arg.name, arg.type_label);
let edit = EditView::new()
.content(current.to_string())
.on_edit(move |s, content, _| {
dispatch_action(
s,
UiAction::MethodArgEdited {
index,
value: content.to_string(),
},
);
})
.on_submit(|s, _| dispatch_action(s, UiAction::CallMethodConfirmed))
.with_name(arg_input_id(index))
.min_width(30);
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new(label))
.child(edit)
}
fn update_input_errors(
siv: &mut Cursive,
field_errors: &[Option<String>],
call_error: Option<&str>,
) {
for (i, err) in field_errors.iter().enumerate() {
let text = err.clone().unwrap_or_default();
siv.call_on_name(&arg_error_id(i), |v: &mut TextView| {
v.set_content(text.clone());
});
}
let call_text = call_error.unwrap_or("").to_string();
siv.call_on_name(ID_METHOD_CALL_ERROR, |v: &mut TextView| {
v.set_content(call_text);
});
}
fn arg_input_id(index: usize) -> String {
format!("method_arg_input_{index}")
}
fn arg_error_id(index: usize) -> String {
format!("method_arg_err_{index}")
}