use anyhow::Result;
use tokio::sync::mpsc;
use crate::cli::management::{self, ClientForm, ServerForm};
use crate::llm::actions::ParameterDefinition;
use crate::state::app_state::AppState;
use crate::tui::app::{Section, UiKey};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldTarget {
Port,
Host,
Interface,
MacAddress,
RemoteAddr,
SendFirst,
Instruction,
InitialMemory,
FeedbackInstructions,
StartupParam(String),
EventHandlersJson,
DraftCode,
DraftInstruction,
DraftActions,
}
#[derive(Debug, Clone)]
pub struct Field {
pub target: FieldTarget,
pub label: String,
pub value: String,
pub original: String,
pub placeholder: String,
pub default: Option<String>,
pub help: String,
pub required: bool,
pub multiline: bool,
}
impl Field {
fn simple(target: FieldTarget, label: &str, help: &str) -> Self {
Self {
target,
label: label.to_string(),
value: String::new(),
original: String::new(),
placeholder: String::new(),
default: None,
help: help.to_string(),
required: false,
multiline: false,
}
}
pub fn changed(&self) -> bool {
self.value.trim() != self.original.trim()
}
pub fn is_default(&self) -> bool {
self.default
.as_deref()
.is_some_and(|d| d.trim() == self.value.trim())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormMode {
Create(Section),
Edit(UiKey),
}
#[derive(Debug, Clone)]
pub struct FormModel {
pub mode: FormMode,
pub protocol: String,
pub fields: Vec<Field>,
pub selected: usize,
pub editing: Option<String>,
pub error: Option<String>,
pub busy: bool,
pub focused_button: Option<usize>,
}
impl FormModel {
pub fn for_create(section: Section, protocol: &str, default_port: Option<u16>) -> Self {
let mut fields = Vec::new();
match section {
Section::Servers => {
let mut port = Field::simple(
FieldTarget::Port,
"port",
"TCP/UDP port to bind. 0 asks the OS for a free one.",
);
port.value = default_port
.map(|p| p.to_string())
.unwrap_or_else(|| "0".to_string());
port.placeholder = if default_port.is_none() {
"0 = let the OS choose a free port".to_string()
} else {
String::new()
};
fields.push(port);
fields.push(Field::simple(
FieldTarget::Host,
"host",
"Address to bind. Defaults to the protocol's own default.",
));
fields.push(Field::simple(
FieldTarget::Interface,
"interface",
"Network interface, for layer-2/raw protocols (arp, datalink…).",
));
fields.push(Field::simple(
FieldTarget::MacAddress,
"mac_address",
"Source MAC, only meaningful together with an interface.",
));
let mut send_first = Field::simple(
FieldTarget::SendFirst,
"send_first",
"Speak first on connect, before the peer sends anything. Honoured by the \
protocols that declare a `send_first` startup parameter (tcp, telnet, \
redis, ldap, mysql, postgresql, …); any other protocol ignores it with a \
warning.",
);
send_first.value = "false".to_string();
fields.push(send_first);
}
Section::Clients => {
let mut remote = Field::simple(
FieldTarget::RemoteAddr,
"remote_addr",
"Server to connect to, host:port.",
);
remote.required = true;
remote.placeholder = "required — e.g. 127.0.0.1:2323".to_string();
fields.push(remote);
}
}
let mut model = Self::finish(
FormMode::Create(section),
protocol,
fields,
declared_params(section, protocol),
);
model.set_field_value(
&FieldTarget::EventHandlersJson,
default_event_handlers(section, protocol).to_string(),
);
model
}
pub fn note_default_port(&mut self, default: &crate::protocol::default_port::DefaultPort) {
if let Some(port) = self
.fields
.iter_mut()
.find(|f| f.target == FieldTarget::Port)
{
port.help = format!(
"TCP/UDP port to bind. 0 asks the OS for a free one. Default: {}.",
default.describe()
);
}
}
pub fn for_edit_server(row: &crate::tui::projection::ServerRow) -> Self {
let mut fields = Vec::new();
let mut port = Field::simple(
FieldTarget::Port,
"port",
"Changing this RESTARTS the server: it gets a new id and drops connections.",
);
port.value = row
.local_addr
.as_ref()
.and_then(|a| a.rsplit_once(':'))
.and_then(|(_, p)| p.parse::<u16>().ok())
.filter(|p| *p != 0)
.unwrap_or(row.port)
.to_string();
port.original = port.value.clone();
fields.push(port);
let mut host = Field::simple(
FieldTarget::Host,
"host",
"Changing this RESTARTS the server.",
);
host.value = row
.local_addr
.as_ref()
.and_then(|a| a.rsplit_once(':').map(|(h, _)| h.to_string()))
.unwrap_or_default();
host.original = host.value.clone();
fields.push(host);
Self::finish_edit(
UiKey::Server(row.id),
&row.protocol,
fields,
declared_params(Section::Servers, &row.protocol),
&row.startup_params,
&row.instruction,
row.routing.as_ref(),
)
}
pub fn for_edit_client(row: &crate::tui::projection::ClientRow) -> Self {
let mut fields = Vec::new();
let mut remote = Field::simple(
FieldTarget::RemoteAddr,
"remote_addr",
"Changing this RECONNECTS the client: it gets a new id.",
);
remote.value = row.remote_addr.clone();
remote.original = remote.value.clone();
fields.push(remote);
Self::finish_edit(
UiKey::Client(row.id),
&row.protocol,
fields,
declared_params(Section::Clients, &row.protocol),
&row.startup_params,
&row.instruction,
row.routing.as_ref(),
)
}
fn finish(
mode: FormMode,
protocol: &str,
mut fields: Vec<Field>,
params: Vec<ParameterDefinition>,
) -> Self {
push_param_fields(&mut fields, ¶ms, None);
push_text_fields(&mut fields, "", None);
Self {
mode,
protocol: protocol.to_string(),
fields,
selected: 0,
editing: None,
error: None,
busy: false,
focused_button: None,
}
}
#[allow(clippy::too_many_arguments)]
fn finish_edit(
key: UiKey,
protocol: &str,
mut fields: Vec<Field>,
params: Vec<ParameterDefinition>,
startup_params: &Option<serde_json::Value>,
instruction: &str,
routing: Option<&crate::scripting::EventHandlerConfig>,
) -> Self {
push_param_fields(&mut fields, ¶ms, startup_params.as_ref());
push_text_fields(&mut fields, instruction, routing);
Self {
mode: FormMode::Edit(key),
protocol: protocol.to_string(),
fields,
selected: 0,
editing: None,
error: None,
busy: false,
focused_button: None,
}
}
pub fn buttons(&self) -> Vec<crate::tui::hit::ModalAction> {
use crate::tui::hit::ModalAction::*;
vec![FormApply, FormWireshark, FormCancel]
}
pub fn capture_target(&self) -> crate::tui::wireshark::CaptureTarget {
use crate::tui::wireshark::{CaptureTarget, Role};
let current = |target: &FieldTarget| -> Option<String> {
let field = self.fields.iter().find(|f| &f.target == target)?;
let value = match (&self.editing, self.selected == self.position_of(target)) {
(Some(buffer), true) => buffer.trim().to_string(),
_ => field.value.trim().to_string(),
};
(!value.is_empty()).then_some(value)
};
let section = match &self.mode {
FormMode::Create(section) => *section,
FormMode::Edit(UiKey::Server(_)) => Section::Servers,
FormMode::Edit(UiKey::Client(_)) => Section::Clients,
};
match section {
Section::Servers => CaptureTarget {
protocol: self.protocol.clone(),
role: Role::Server,
host: current(&FieldTarget::Host),
port: current(&FieldTarget::Port).and_then(|p| p.parse().ok()),
interface: current(&FieldTarget::Interface),
},
Section::Clients => {
CaptureTarget::client(&self.protocol, current(&FieldTarget::RemoteAddr).as_deref())
}
}
}
fn position_of(&self, target: &FieldTarget) -> usize {
self.fields
.iter()
.position(|f| &f.target == target)
.unwrap_or(usize::MAX)
}
pub fn focused_action(&self) -> Option<crate::tui::hit::ModalAction> {
self.focused_button
.and_then(|index| self.buttons().get(index).copied())
}
pub fn cycle_focus(&mut self, backward: bool) {
let fields = self.fields.len();
let buttons = self.buttons().len();
let total = fields + buttons;
if total == 0 {
return;
}
let current = match self.focused_button {
None => self.selected.min(fields.saturating_sub(1)),
Some(index) => fields + index,
};
let next = if backward {
(current + total - 1) % total
} else {
(current + 1) % total
};
if next < fields {
self.selected = next;
self.focused_button = None;
} else {
self.focused_button = Some(next - fields);
}
}
pub fn selected_field(&self) -> Option<&Field> {
self.fields.get(self.selected)
}
fn missing_required_index(&self) -> Option<usize> {
if let Some(index) = self
.fields
.iter()
.position(|f| f.required && f.value.trim().is_empty())
{
return Some(index);
}
if matches!(self.mode, FormMode::Create(_))
&& self.protocol.eq_ignore_ascii_case("openapi")
&& !self.fields.iter().any(|field| {
matches!(&field.target, FieldTarget::StartupParam(name)
if name == "spec" || name == "spec_file")
&& !field.value.trim().is_empty()
})
{
return self
.fields
.iter()
.position(|field| field.target == FieldTarget::StartupParam("spec".into()));
}
None
}
pub fn missing_required(&self) -> Option<String> {
let index = self.missing_required_index()?;
let field = &self.fields[index];
if field.label == "spec"
&& self.protocol.eq_ignore_ascii_case("openapi")
&& self
.fields
.iter()
.any(|field| field.target == FieldTarget::StartupParam("spec_file".into()))
{
Some("spec or spec_file".to_string())
} else {
Some(field.label.clone())
}
}
pub fn focus_first_missing_required(&mut self) {
if let Some(index) = self.missing_required_index() {
self.selected = index;
self.focused_button = None;
}
}
pub fn move_selection(&mut self, delta: isize) {
if self.fields.is_empty() {
return;
}
let len = self.fields.len() as isize;
let next = (self.selected as isize + delta).rem_euclid(len);
self.selected = next as usize;
}
pub fn begin_edit(&mut self) {
if let Some(field) = self.fields.get(self.selected) {
if !field.multiline {
self.editing = Some(field.value.clone());
}
}
}
pub fn commit_edit(&mut self) {
if let Some(buffer) = self.editing.take() {
if let Some(field) = self.fields.get_mut(self.selected) {
field.value = buffer;
}
}
}
pub fn cancel_edit(&mut self) {
self.editing = None;
}
pub fn set_field_value(&mut self, target: &FieldTarget, value: String) {
if let Some(field) = self.fields.iter_mut().find(|f| &f.target == target) {
field.value = value;
}
}
fn value_of(&self, target: &FieldTarget) -> Option<String> {
let field = self.fields.iter().find(|f| &f.target == target)?;
let editing = matches!(self.mode, FormMode::Edit(_));
if editing && !field.changed() {
return None;
}
let value = field.value.trim().to_string();
(!value.is_empty()).then_some(value)
}
fn startup_params(&self) -> Option<serde_json::Value> {
let section = match &self.mode {
FormMode::Create(section) => *section,
FormMode::Edit(UiKey::Server(_)) => Section::Servers,
FormMode::Edit(UiKey::Client(_)) => Section::Clients,
};
let params = declared_params(section, &self.protocol);
let mut map = serde_json::Map::new();
for field in &self.fields {
if let FieldTarget::StartupParam(name) = &field.target {
if !field.changed() {
continue;
}
let raw = field.value.trim();
if raw.is_empty() {
continue;
}
let value = if params
.iter()
.any(|p| p.name == *name && p.type_hint == "string")
{
serde_json::Value::String(raw.to_string())
} else {
serde_json::from_str::<serde_json::Value>(raw)
.unwrap_or_else(|_| serde_json::Value::String(raw.to_string()))
};
map.insert(name.clone(), value);
}
}
(!map.is_empty()).then(|| serde_json::Value::Object(map))
}
fn event_handlers(&self) -> Result<Option<Vec<serde_json::Value>>> {
let Some(raw) = self.value_of(&FieldTarget::EventHandlersJson) else {
return Ok(None);
};
let parsed: serde_json::Value = serde_json::from_str(&raw)
.map_err(|e| anyhow::anyhow!("event_handlers is not valid JSON: {e}"))?;
match parsed {
serde_json::Value::Array(items) => Ok(Some(items)),
_ => anyhow::bail!("event_handlers must be a JSON array of handler objects"),
}
}
pub fn to_server_form(&self) -> Result<ServerForm> {
let port = match self.value_of(&FieldTarget::Port) {
Some(text) => Some(
text.parse::<u16>()
.map_err(|_| anyhow::anyhow!("port must be a number 0-65535"))?,
),
None => None,
};
Ok(ServerForm {
protocol: self.protocol.clone(),
port,
host: self.value_of(&FieldTarget::Host),
interface: self.value_of(&FieldTarget::Interface),
mac_address: self.value_of(&FieldTarget::MacAddress),
send_first: self
.value_of(&FieldTarget::SendFirst)
.map(|v| v == "true")
.unwrap_or(false),
instruction: self.value_of(&FieldTarget::Instruction),
initial_memory: self.value_of(&FieldTarget::InitialMemory),
startup_params: self.startup_params(),
event_handlers: self.event_handlers()?,
scheduled_tasks: None,
feedback_instructions: self.value_of(&FieldTarget::FeedbackInstructions),
})
}
pub fn to_client_form(&self) -> Result<ClientForm> {
Ok(ClientForm {
protocol: self.protocol.clone(),
remote_addr: self.value_of(&FieldTarget::RemoteAddr),
instruction: self.value_of(&FieldTarget::Instruction),
initial_memory: self.value_of(&FieldTarget::InitialMemory),
startup_params: self.startup_params(),
event_handlers: self.event_handlers()?,
scheduled_tasks: None,
feedback_instructions: self.value_of(&FieldTarget::FeedbackInstructions),
})
}
pub async fn apply(
&self,
state: &AppState,
llm_client: crate::llm::OllamaClient,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<String> {
if let Some(missing) = self.missing_required() {
anyhow::bail!("{} requires {missing}", self.protocol);
}
match &self.mode {
FormMode::Create(Section::Servers) => {
let form = self.to_server_form()?;
let id = form.create(state, status_tx.clone()).await?;
Ok(format!(
"Started server #{} ({})",
id.as_u32(),
self.protocol
))
}
FormMode::Create(Section::Clients) => {
let form = self.to_client_form()?;
let id = form.create(state, llm_client, status_tx.clone()).await?;
Ok(format!(
"Connected client #{} ({})",
id.as_u32(),
self.protocol
))
}
FormMode::Edit(UiKey::Server(server_id)) => {
let form = self.to_server_form()?;
let outcome =
management::update_server(state, *server_id, form, status_tx.clone()).await?;
Ok(outcome.summary)
}
FormMode::Edit(UiKey::Client(client_id)) => {
let form = self.to_client_form()?;
let outcome = management::update_client(
state,
*client_id,
form,
llm_client,
status_tx.clone(),
)
.await?;
Ok(outcome.summary)
}
}
}
}
pub fn default_event_handlers(section: Section, protocol: &str) -> serde_json::Value {
let manual_fallback = serde_json::json!({
"event_pattern": "*",
"handler": {"type": "manual"}
});
let mut rules = Vec::new();
if section == Section::Servers {
if let Ok(server) = crate::protocol::server_registry::registry().resolve(protocol) {
for event_type in server.get_event_types() {
if event_type.on_every_connection {
rules.push(serde_json::json!({
"event_pattern": event_type.id,
"handler": {"type": "static", "actions": []}
}));
}
}
}
}
if section == Section::Clients {
if let Ok(client) = crate::protocol::CLIENT_REGISTRY.resolve(protocol) {
for event_type in client.get_event_types() {
if event_type.id.ends_with("_connected") || event_type.id == "oidc_discovered" {
rules.push(serde_json::json!({
"event_pattern": event_type.id,
"handler": {"type": "static", "actions": []}
}));
}
}
}
}
rules.push(manual_fallback);
serde_json::Value::Array(rules)
}
fn declared_params(section: Section, protocol: &str) -> Vec<ParameterDefinition> {
match section {
Section::Servers => management::server_declared_params(protocol),
Section::Clients => management::client_declared_params(protocol),
}
.unwrap_or_default()
}
fn push_param_fields(
fields: &mut Vec<Field>,
params: &[ParameterDefinition],
current: Option<&serde_json::Value>,
) {
for param in params {
if fields.iter().any(|f| f.label == param.name) {
continue;
}
let existing = current
.and_then(|v| v.get(¶m.name))
.map(|v| match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
})
.unwrap_or_default();
let as_text = |v: &serde_json::Value| match v {
serde_json::Value::Null => String::new(),
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
let example = as_text(¶m.example);
let default = param.default.as_ref().map(as_text);
let value = match (&existing, &default) {
(e, Some(d)) if e.is_empty() => d.clone(),
_ => existing,
};
let help = match &default {
Some(d) => format!("{} ({}) Default: {d}.", param.description, param.type_hint),
None => format!("{} ({})", param.description, param.type_hint),
};
fields.push(Field {
target: FieldTarget::StartupParam(param.name.clone()),
label: param.name.clone(),
original: value.clone(),
value,
placeholder: if example.is_empty() {
String::new()
} else {
format!("e.g. {example}")
},
default,
help,
required: param.required,
multiline: param.name == "spec",
});
}
}
fn push_text_fields(
fields: &mut Vec<Field>,
instruction: &str,
routing: Option<&crate::scripting::EventHandlerConfig>,
) {
let mut instruction_field = Field::simple(
FieldTarget::Instruction,
"instruction",
"Natural-language instruction for the LLM fallback path.",
);
instruction_field.value = instruction.to_string();
instruction_field.original = instruction_field.value.clone();
instruction_field.multiline = true;
fields.push(instruction_field);
let mut memory = Field::simple(
FieldTarget::InitialMemory,
"initial_memory",
"Seed the instance's LLM memory.",
);
memory.multiline = true;
fields.push(memory);
let mut feedback = Field::simple(
FieldTarget::FeedbackInstructions,
"feedback_instructions",
"Instructions for the automatic feedback loop.",
);
feedback.multiline = true;
fields.push(feedback);
let mut handlers = Field::simple(
FieldTarget::EventHandlersJson,
"event_handlers",
"Routing as raw JSON. Press r on the band for the guided editor.",
);
handlers.value = routing
.map(|r| serde_json::to_string_pretty(&r.handlers).unwrap_or_default())
.unwrap_or_default();
handlers.original = handlers.value.clone();
handlers.multiline = true;
fields.push(handlers);
}