1use anyhow::Result;
14use tokio::sync::mpsc;
15
16use crate::cli::management::{self, ClientForm, ServerForm};
17use crate::llm::actions::ParameterDefinition;
18use crate::state::app_state::AppState;
19use crate::tui::app::{Section, UiKey};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum FieldTarget {
24 Port,
25 Host,
26 Interface,
27 MacAddress,
28 RemoteAddr,
29 SendFirst,
30 Instruction,
31 InitialMemory,
32 FeedbackInstructions,
33 StartupParam(String),
35 EventHandlersJson,
38 DraftCode,
40 DraftInstruction,
42 DraftActions,
44}
45
46#[derive(Debug, Clone)]
47pub struct Field {
48 pub target: FieldTarget,
49 pub label: String,
50 pub value: String,
51 pub original: String,
56 pub placeholder: String,
58 pub default: Option<String>,
63 pub help: String,
64 pub required: bool,
65 pub multiline: bool,
67}
68
69impl Field {
70 fn simple(target: FieldTarget, label: &str, help: &str) -> Self {
71 Self {
72 target,
73 label: label.to_string(),
74 value: String::new(),
75 original: String::new(),
76 placeholder: String::new(),
77 default: None,
78 help: help.to_string(),
79 required: false,
80 multiline: false,
81 }
82 }
83
84 pub fn changed(&self) -> bool {
85 self.value.trim() != self.original.trim()
86 }
87
88 pub fn is_default(&self) -> bool {
90 self.default
91 .as_deref()
92 .is_some_and(|d| d.trim() == self.value.trim())
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum FormMode {
99 Create(Section),
100 Edit(UiKey),
101}
102
103#[derive(Debug, Clone)]
105pub struct FormModel {
106 pub mode: FormMode,
107 pub protocol: String,
108 pub fields: Vec<Field>,
109 pub selected: usize,
110 pub editing: Option<String>,
113 pub error: Option<String>,
114 pub busy: bool,
117 pub focused_button: Option<usize>,
119}
120
121impl FormModel {
122 pub fn for_create(section: Section, protocol: &str, default_port: Option<u16>) -> Self {
124 let mut fields = Vec::new();
125 match section {
126 Section::Servers => {
127 let mut port = Field::simple(
128 FieldTarget::Port,
129 "port",
130 "TCP/UDP port to bind. 0 asks the OS for a free one.",
131 );
132 port.value = default_port
137 .map(|p| p.to_string())
138 .unwrap_or_else(|| "0".to_string());
139 port.placeholder = if default_port.is_none() {
140 "0 = let the OS choose a free port".to_string()
141 } else {
142 String::new()
143 };
144 fields.push(port);
145 fields.push(Field::simple(
146 FieldTarget::Host,
147 "host",
148 "Address to bind. Defaults to the protocol's own default.",
149 ));
150 fields.push(Field::simple(
151 FieldTarget::Interface,
152 "interface",
153 "Network interface, for layer-2/raw protocols (arp, datalink…).",
154 ));
155 fields.push(Field::simple(
156 FieldTarget::MacAddress,
157 "mac_address",
158 "Source MAC, only meaningful together with an interface.",
159 ));
160 let mut send_first = Field::simple(
161 FieldTarget::SendFirst,
162 "send_first",
163 "Speak first on connect, before the peer sends anything. Honoured by the \
164 protocols that declare a `send_first` startup parameter (tcp, telnet, \
165 redis, ldap, mysql, postgresql, …); any other protocol ignores it with a \
166 warning.",
167 );
168 send_first.value = "false".to_string();
169 fields.push(send_first);
170 }
171 Section::Clients => {
172 let mut remote = Field::simple(
173 FieldTarget::RemoteAddr,
174 "remote_addr",
175 "Server to connect to, host:port.",
176 );
177 remote.required = true;
178 remote.placeholder = "required — e.g. 127.0.0.1:2323".to_string();
179 fields.push(remote);
180 }
181 }
182 let mut model = Self::finish(
183 FormMode::Create(section),
184 protocol,
185 fields,
186 declared_params(section, protocol),
187 );
188 model.set_field_value(
208 &FieldTarget::EventHandlersJson,
209 default_event_handlers(section, protocol).to_string(),
210 );
211 model
212 }
213
214 pub fn note_default_port(&mut self, default: &crate::protocol::default_port::DefaultPort) {
217 if let Some(port) = self
218 .fields
219 .iter_mut()
220 .find(|f| f.target == FieldTarget::Port)
221 {
222 port.help = format!(
223 "TCP/UDP port to bind. 0 asks the OS for a free one. Default: {}.",
224 default.describe()
225 );
226 }
227 }
228
229 pub fn for_edit_server(row: &crate::tui::projection::ServerRow) -> Self {
231 let mut fields = Vec::new();
232 let mut port = Field::simple(
233 FieldTarget::Port,
234 "port",
235 "Changing this RESTARTS the server: it gets a new id and drops connections.",
236 );
237 port.value = row
244 .local_addr
245 .as_ref()
246 .and_then(|a| a.rsplit_once(':'))
247 .and_then(|(_, p)| p.parse::<u16>().ok())
248 .filter(|p| *p != 0)
249 .unwrap_or(row.port)
250 .to_string();
251 port.original = port.value.clone();
254 fields.push(port);
255 let mut host = Field::simple(
256 FieldTarget::Host,
257 "host",
258 "Changing this RESTARTS the server.",
259 );
260 host.value = row
261 .local_addr
262 .as_ref()
263 .and_then(|a| a.rsplit_once(':').map(|(h, _)| h.to_string()))
264 .unwrap_or_default();
265 host.original = host.value.clone();
266 fields.push(host);
267 Self::finish_edit(
268 UiKey::Server(row.id),
269 &row.protocol,
270 fields,
271 declared_params(Section::Servers, &row.protocol),
272 &row.startup_params,
273 &row.instruction,
274 row.routing.as_ref(),
275 )
276 }
277
278 pub fn for_edit_client(row: &crate::tui::projection::ClientRow) -> Self {
279 let mut fields = Vec::new();
280 let mut remote = Field::simple(
281 FieldTarget::RemoteAddr,
282 "remote_addr",
283 "Changing this RECONNECTS the client: it gets a new id.",
284 );
285 remote.value = row.remote_addr.clone();
286 remote.original = remote.value.clone();
287 fields.push(remote);
288 Self::finish_edit(
289 UiKey::Client(row.id),
290 &row.protocol,
291 fields,
292 declared_params(Section::Clients, &row.protocol),
293 &row.startup_params,
294 &row.instruction,
295 row.routing.as_ref(),
296 )
297 }
298
299 fn finish(
300 mode: FormMode,
301 protocol: &str,
302 mut fields: Vec<Field>,
303 params: Vec<ParameterDefinition>,
304 ) -> Self {
305 push_param_fields(&mut fields, ¶ms, None);
306 push_text_fields(&mut fields, "", None);
307 Self {
308 mode,
309 protocol: protocol.to_string(),
310 fields,
311 selected: 0,
312 editing: None,
313 error: None,
314 busy: false,
315 focused_button: None,
316 }
317 }
318
319 #[allow(clippy::too_many_arguments)]
320 fn finish_edit(
321 key: UiKey,
322 protocol: &str,
323 mut fields: Vec<Field>,
324 params: Vec<ParameterDefinition>,
325 startup_params: &Option<serde_json::Value>,
326 instruction: &str,
327 routing: Option<&crate::scripting::EventHandlerConfig>,
328 ) -> Self {
329 push_param_fields(&mut fields, ¶ms, startup_params.as_ref());
330 push_text_fields(&mut fields, instruction, routing);
331 Self {
332 mode: FormMode::Edit(key),
333 protocol: protocol.to_string(),
334 fields,
335 selected: 0,
336 editing: None,
337 error: None,
338 busy: false,
339 focused_button: None,
340 }
341 }
342
343 pub fn buttons(&self) -> Vec<crate::tui::hit::ModalAction> {
345 use crate::tui::hit::ModalAction::*;
346 vec![FormApply, FormWireshark, FormCancel]
347 }
348
349 pub fn capture_target(&self) -> crate::tui::wireshark::CaptureTarget {
354 use crate::tui::wireshark::{CaptureTarget, Role};
355 let current = |target: &FieldTarget| -> Option<String> {
356 let field = self.fields.iter().find(|f| &f.target == target)?;
357 let value = match (&self.editing, self.selected == self.position_of(target)) {
358 (Some(buffer), true) => buffer.trim().to_string(),
359 _ => field.value.trim().to_string(),
360 };
361 (!value.is_empty()).then_some(value)
362 };
363 let section = match &self.mode {
364 FormMode::Create(section) => *section,
365 FormMode::Edit(UiKey::Server(_)) => Section::Servers,
366 FormMode::Edit(UiKey::Client(_)) => Section::Clients,
367 };
368 match section {
369 Section::Servers => CaptureTarget {
370 protocol: self.protocol.clone(),
371 role: Role::Server,
372 host: current(&FieldTarget::Host),
373 port: current(&FieldTarget::Port).and_then(|p| p.parse().ok()),
374 interface: current(&FieldTarget::Interface),
375 },
376 Section::Clients => {
377 CaptureTarget::client(&self.protocol, current(&FieldTarget::RemoteAddr).as_deref())
378 }
379 }
380 }
381
382 fn position_of(&self, target: &FieldTarget) -> usize {
383 self.fields
384 .iter()
385 .position(|f| &f.target == target)
386 .unwrap_or(usize::MAX)
387 }
388
389 pub fn focused_action(&self) -> Option<crate::tui::hit::ModalAction> {
390 self.focused_button
391 .and_then(|index| self.buttons().get(index).copied())
392 }
393
394 pub fn cycle_focus(&mut self, backward: bool) {
396 let fields = self.fields.len();
397 let buttons = self.buttons().len();
398 let total = fields + buttons;
399 if total == 0 {
400 return;
401 }
402 let current = match self.focused_button {
403 None => self.selected.min(fields.saturating_sub(1)),
404 Some(index) => fields + index,
405 };
406 let next = if backward {
407 (current + total - 1) % total
408 } else {
409 (current + 1) % total
410 };
411 if next < fields {
412 self.selected = next;
413 self.focused_button = None;
414 } else {
415 self.focused_button = Some(next - fields);
416 }
417 }
418
419 pub fn selected_field(&self) -> Option<&Field> {
420 self.fields.get(self.selected)
421 }
422
423 fn missing_required_index(&self) -> Option<usize> {
425 if let Some(index) = self
426 .fields
427 .iter()
428 .position(|f| f.required && f.value.trim().is_empty())
429 {
430 return Some(index);
431 }
432 if matches!(self.mode, FormMode::Create(_))
436 && self.protocol.eq_ignore_ascii_case("openapi")
437 && !self.fields.iter().any(|field| {
438 matches!(&field.target, FieldTarget::StartupParam(name)
439 if name == "spec" || name == "spec_file")
440 && !field.value.trim().is_empty()
441 })
442 {
443 return self
444 .fields
445 .iter()
446 .position(|field| field.target == FieldTarget::StartupParam("spec".into()));
447 }
448 None
449 }
450
451 pub fn missing_required(&self) -> Option<String> {
453 let index = self.missing_required_index()?;
454 let field = &self.fields[index];
455 if field.label == "spec"
456 && self.protocol.eq_ignore_ascii_case("openapi")
457 && self
458 .fields
459 .iter()
460 .any(|field| field.target == FieldTarget::StartupParam("spec_file".into()))
461 {
462 Some("spec or spec_file".to_string())
463 } else {
464 Some(field.label.clone())
465 }
466 }
467
468 pub fn focus_first_missing_required(&mut self) {
470 if let Some(index) = self.missing_required_index() {
471 self.selected = index;
472 self.focused_button = None;
473 }
474 }
475
476 pub fn move_selection(&mut self, delta: isize) {
477 if self.fields.is_empty() {
478 return;
479 }
480 let len = self.fields.len() as isize;
481 let next = (self.selected as isize + delta).rem_euclid(len);
482 self.selected = next as usize;
483 }
484
485 pub fn begin_edit(&mut self) {
487 if let Some(field) = self.fields.get(self.selected) {
488 if !field.multiline {
489 self.editing = Some(field.value.clone());
490 }
491 }
492 }
493
494 pub fn commit_edit(&mut self) {
495 if let Some(buffer) = self.editing.take() {
496 if let Some(field) = self.fields.get_mut(self.selected) {
497 field.value = buffer;
498 }
499 }
500 }
501
502 pub fn cancel_edit(&mut self) {
503 self.editing = None;
504 }
505
506 pub fn set_field_value(&mut self, target: &FieldTarget, value: String) {
507 if let Some(field) = self.fields.iter_mut().find(|f| &f.target == target) {
508 field.value = value;
509 }
510 }
511
512 fn value_of(&self, target: &FieldTarget) -> Option<String> {
516 let field = self.fields.iter().find(|f| &f.target == target)?;
517 let editing = matches!(self.mode, FormMode::Edit(_));
518 if editing && !field.changed() {
519 return None;
520 }
521 let value = field.value.trim().to_string();
522 (!value.is_empty()).then_some(value)
523 }
524
525 fn startup_params(&self) -> Option<serde_json::Value> {
534 let section = match &self.mode {
535 FormMode::Create(section) => *section,
536 FormMode::Edit(UiKey::Server(_)) => Section::Servers,
537 FormMode::Edit(UiKey::Client(_)) => Section::Clients,
538 };
539 let params = declared_params(section, &self.protocol);
540 let mut map = serde_json::Map::new();
541 for field in &self.fields {
542 if let FieldTarget::StartupParam(name) = &field.target {
543 if !field.changed() {
544 continue;
545 }
546 let raw = field.value.trim();
547 if raw.is_empty() {
548 continue;
549 }
550 let value = if params
553 .iter()
554 .any(|p| p.name == *name && p.type_hint == "string")
555 {
556 serde_json::Value::String(raw.to_string())
557 } else {
558 serde_json::from_str::<serde_json::Value>(raw)
559 .unwrap_or_else(|_| serde_json::Value::String(raw.to_string()))
560 };
561 map.insert(name.clone(), value);
562 }
563 }
564 (!map.is_empty()).then(|| serde_json::Value::Object(map))
565 }
566
567 fn event_handlers(&self) -> Result<Option<Vec<serde_json::Value>>> {
568 let Some(raw) = self.value_of(&FieldTarget::EventHandlersJson) else {
569 return Ok(None);
570 };
571 let parsed: serde_json::Value = serde_json::from_str(&raw)
572 .map_err(|e| anyhow::anyhow!("event_handlers is not valid JSON: {e}"))?;
573 match parsed {
574 serde_json::Value::Array(items) => Ok(Some(items)),
575 _ => anyhow::bail!("event_handlers must be a JSON array of handler objects"),
576 }
577 }
578
579 pub fn to_server_form(&self) -> Result<ServerForm> {
580 let port = match self.value_of(&FieldTarget::Port) {
581 Some(text) => Some(
582 text.parse::<u16>()
583 .map_err(|_| anyhow::anyhow!("port must be a number 0-65535"))?,
584 ),
585 None => None,
586 };
587 Ok(ServerForm {
588 protocol: self.protocol.clone(),
589 port,
590 host: self.value_of(&FieldTarget::Host),
591 interface: self.value_of(&FieldTarget::Interface),
592 mac_address: self.value_of(&FieldTarget::MacAddress),
593 send_first: self
594 .value_of(&FieldTarget::SendFirst)
595 .map(|v| v == "true")
596 .unwrap_or(false),
597 instruction: self.value_of(&FieldTarget::Instruction),
598 initial_memory: self.value_of(&FieldTarget::InitialMemory),
599 startup_params: self.startup_params(),
600 event_handlers: self.event_handlers()?,
601 scheduled_tasks: None,
602 feedback_instructions: self.value_of(&FieldTarget::FeedbackInstructions),
603 })
604 }
605
606 pub fn to_client_form(&self) -> Result<ClientForm> {
607 Ok(ClientForm {
608 protocol: self.protocol.clone(),
609 remote_addr: self.value_of(&FieldTarget::RemoteAddr),
610 instruction: self.value_of(&FieldTarget::Instruction),
611 initial_memory: self.value_of(&FieldTarget::InitialMemory),
612 startup_params: self.startup_params(),
613 event_handlers: self.event_handlers()?,
614 scheduled_tasks: None,
615 feedback_instructions: self.value_of(&FieldTarget::FeedbackInstructions),
616 })
617 }
618
619 pub async fn apply(
621 &self,
622 state: &AppState,
623 llm_client: crate::llm::OllamaClient,
624 status_tx: &mpsc::UnboundedSender<String>,
625 ) -> Result<String> {
626 if let Some(missing) = self.missing_required() {
627 anyhow::bail!("{} requires {missing}", self.protocol);
628 }
629 match &self.mode {
630 FormMode::Create(Section::Servers) => {
631 let form = self.to_server_form()?;
632 let id = form.create(state, status_tx.clone()).await?;
633 Ok(format!(
634 "Started server #{} ({})",
635 id.as_u32(),
636 self.protocol
637 ))
638 }
639 FormMode::Create(Section::Clients) => {
640 let form = self.to_client_form()?;
641 let id = form.create(state, llm_client, status_tx.clone()).await?;
642 Ok(format!(
643 "Connected client #{} ({})",
644 id.as_u32(),
645 self.protocol
646 ))
647 }
648 FormMode::Edit(UiKey::Server(server_id)) => {
649 let form = self.to_server_form()?;
650 let outcome =
651 management::update_server(state, *server_id, form, status_tx.clone()).await?;
652 Ok(outcome.summary)
653 }
654 FormMode::Edit(UiKey::Client(client_id)) => {
655 let form = self.to_client_form()?;
656 let outcome = management::update_client(
657 state,
658 *client_id,
659 form,
660 llm_client,
661 status_tx.clone(),
662 )
663 .await?;
664 Ok(outcome.summary)
665 }
666 }
667 }
668}
669
670pub fn default_event_handlers(section: Section, protocol: &str) -> serde_json::Value {
691 let manual_fallback = serde_json::json!({
692 "event_pattern": "*",
693 "handler": {"type": "manual"}
694 });
695 let mut rules = Vec::new();
696 if section == Section::Servers {
697 if let Ok(server) = crate::protocol::server_registry::registry().resolve(protocol) {
698 for event_type in server.get_event_types() {
699 if event_type.on_every_connection {
700 rules.push(serde_json::json!({
701 "event_pattern": event_type.id,
702 "handler": {"type": "static", "actions": []}
703 }));
704 }
705 }
706 }
707 }
708 if section == Section::Clients {
709 if let Ok(client) = crate::protocol::CLIENT_REGISTRY.resolve(protocol) {
712 for event_type in client.get_event_types() {
713 if event_type.id.ends_with("_connected") || event_type.id == "oidc_discovered" {
716 rules.push(serde_json::json!({
717 "event_pattern": event_type.id,
718 "handler": {"type": "static", "actions": []}
722 }));
723 }
724 }
725 }
726 }
727 rules.push(manual_fallback);
728 serde_json::Value::Array(rules)
729}
730
731fn declared_params(section: Section, protocol: &str) -> Vec<ParameterDefinition> {
732 match section {
733 Section::Servers => management::server_declared_params(protocol),
734 Section::Clients => management::client_declared_params(protocol),
735 }
736 .unwrap_or_default()
737}
738
739fn push_param_fields(
740 fields: &mut Vec<Field>,
741 params: &[ParameterDefinition],
742 current: Option<&serde_json::Value>,
743) {
744 for param in params {
745 if fields.iter().any(|f| f.label == param.name) {
749 continue;
750 }
751 let existing = current
752 .and_then(|v| v.get(¶m.name))
753 .map(|v| match v {
754 serde_json::Value::String(s) => s.clone(),
755 other => other.to_string(),
756 })
757 .unwrap_or_default();
758 let as_text = |v: &serde_json::Value| match v {
759 serde_json::Value::Null => String::new(),
760 serde_json::Value::String(s) => s.clone(),
761 other => other.to_string(),
762 };
763 let example = as_text(¶m.example);
764 let default = param.default.as_ref().map(as_text);
765 let value = match (&existing, &default) {
768 (e, Some(d)) if e.is_empty() => d.clone(),
769 _ => existing,
770 };
771 let help = match &default {
772 Some(d) => format!("{} ({}) Default: {d}.", param.description, param.type_hint),
773 None => format!("{} ({})", param.description, param.type_hint),
774 };
775 fields.push(Field {
776 target: FieldTarget::StartupParam(param.name.clone()),
777 label: param.name.clone(),
778 original: value.clone(),
779 value,
780 placeholder: if example.is_empty() {
781 String::new()
782 } else {
783 format!("e.g. {example}")
784 },
785 default,
786 help,
787 required: param.required,
788 multiline: param.name == "spec",
789 });
790 }
791}
792
793fn push_text_fields(
794 fields: &mut Vec<Field>,
795 instruction: &str,
796 routing: Option<&crate::scripting::EventHandlerConfig>,
797) {
798 let mut instruction_field = Field::simple(
799 FieldTarget::Instruction,
800 "instruction",
801 "Natural-language instruction for the LLM fallback path.",
802 );
803 instruction_field.value = instruction.to_string();
804 instruction_field.original = instruction_field.value.clone();
805 instruction_field.multiline = true;
806 fields.push(instruction_field);
807
808 let mut memory = Field::simple(
809 FieldTarget::InitialMemory,
810 "initial_memory",
811 "Seed the instance's LLM memory.",
812 );
813 memory.multiline = true;
814 fields.push(memory);
815
816 let mut feedback = Field::simple(
817 FieldTarget::FeedbackInstructions,
818 "feedback_instructions",
819 "Instructions for the automatic feedback loop.",
820 );
821 feedback.multiline = true;
822 fields.push(feedback);
823
824 let mut handlers = Field::simple(
825 FieldTarget::EventHandlersJson,
826 "event_handlers",
827 "Routing as raw JSON. Press r on the band for the guided editor.",
828 );
829 handlers.value = routing
830 .map(|r| serde_json::to_string_pretty(&r.handlers).unwrap_or_default())
831 .unwrap_or_default();
832 handlers.original = handlers.value.clone();
833 handlers.multiline = true;
834 fields.push(handlers);
835}