1use anyhow::Result;
6use iocraft::prelude::*;
7use std::sync::Arc;
8
9use crate::ui::components::create_form::CreateForm;
10use crate::ui::components::delete_confirm::DeleteConfirm;
11use crate::ui::components::ip_selector::IpSelector;
12use crate::ui::components::record_list::RecordList;
13use crate::ui::hooks::*;
14use crate::ui::state::{AppProps, AppState, AppView};
15use crate::ui::status::{StatusMessage, generate_contextual_status};
16use crate::utils::format_selector;
17
18#[component]
21pub fn App(props: &AppProps, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
22 let (_width, _height) = hooks.use_terminal_size();
23 let records_display = hooks.use_state(|| "Loading DNS records...".to_string());
24 let status = hooks.use_state(|| "Initializing...".to_string());
25 let should_exit = hooks.use_state(|| false);
26 let mut system = hooks.use_context_mut::<SystemContext>();
27
28 let view = hooks.use_state(|| AppView::List);
30
31 let form_focus = hooks.use_state(|| 1);
33 let form_type = hooks.use_state(|| "A".to_string());
34 let form_name = hooks.use_state(|| "".to_string());
35 let form_content = hooks.use_state(|| "".to_string());
36 let form_ttl = hooks.use_state(|| "1".to_string());
37 let form_proxied = hooks.use_state(|| "false".to_string());
38 let is_submitting = hooks.use_state(|| false);
39 #[allow(clippy::redundant_closure)]
41 let editing_record_id = hooks.use_state(|| String::new()); let ip_sel_idx = hooks.use_state(|| 0);
45 let ip_sel_open = hooks.use_state(|| false);
46
47 let list_sel_idx = hooks.use_state(|| 0);
49 let is_deleting = hooks.use_state(|| false);
50
51 let is_refreshing = hooks.use_state(|| false);
53
54 let ctx = AppCtx {
56 view,
57 should_exit,
58 form_focus,
59 form_type,
60 form_name,
61 form_content,
62 form_ttl,
63 form_proxied,
64 is_submitting,
65 editing_record_id,
66 ip_sel_idx,
67 ip_sel_open,
68 list_sel_idx,
69 is_deleting,
70 is_refreshing,
71 records_display,
72 status,
73 state: props.state.clone(),
74 };
75
76 use_app_events(&mut hooks, &ctx);
78
79 if should_exit.get() {
81 system.exit();
82 }
83
84 let records = props.state.records.lock().unwrap().clone();
86 let ips = props.state.existing_ips.lock().unwrap().clone();
87 let zone_name = props.state.zone_name.lock().unwrap().clone();
88 let domain_suffix = format!(".{}", &zone_name);
89 let sel_text = format_selector(&ips, ip_sel_idx.get());
90
91 let lsi = list_sel_idx.get();
92 let rec_name: String = if lsi < records.len() {
93 format!("{} ({})", records[lsi].name, records[lsi].record_type)
94 } else {
95 "Unknown".to_string()
96 };
97 let is_editing = matches!(view.get(), AppView::Edit);
98
99 let status_val = status.to_string();
101 let is_transient = StatusMessage::is_transient(&status_val);
102
103 let status_text = if is_transient {
104 status_val
105 } else {
106 let rec_name_opt = if lsi < records.len() {
107 Some(records[lsi].name.as_str())
108 } else {
109 None
110 };
111
112 let status_msg = generate_contextual_status(
113 &view.get(),
114 form_focus.get() as usize,
115 &form_type.to_string(),
116 &form_proxied.to_string(),
117 is_editing,
118 records.len(),
119 lsi,
120 rec_name_opt,
121 );
122 status_msg.render()
123 };
124
125 match view.get() {
127 AppView::Delete => element! {
128 DeleteConfirm(rec_name: rec_name, deleting: is_deleting.get(), status: status_text, zone_name: zone_name.clone())
129 }
130 .into_any(),
131 AppView::IpSelect => element! {
132 IpSelector(sel_text: sel_text, status: status_text, zone_name: zone_name.clone())
133 }
134 .into_any(),
135 AppView::Create | AppView::Edit => {
136 let title = if is_editing {
137 " Edit DNS Record "
138 } else {
139 " Create DNS Record "
140 };
141 let hint = "↑↓: navigate | esc: cancel";
142 element! {
143 CreateForm(
144 form_type: form_type,
145 form_name: form_name,
146 form_content: form_content,
147 form_ttl: form_ttl,
148 form_proxied: form_proxied,
149 form_focus: form_focus.get(),
150 status: status_text,
151 title: title.to_string(),
152 hint: hint.to_string(),
153 submit_label: if is_editing { "Save" } else { "Create" },
154 zone_name: zone_name.clone(),
155 domain_suffix: domain_suffix.clone(),
156 is_editing: is_editing,
157 )
158 }
159 .into_any()
160 }
161 AppView::List => element! {
162 RecordList(
163 records: records,
164 selected_idx: lsi as i32,
165 status: status_text,
166 zone_name: zone_name,
167 )
168 }
169 .into_any(),
170 }
171}
172
173pub fn run_app(api_token: String, zone_id: String) -> Result<()> {
175 let state = Arc::new(AppState::new(api_token, zone_id));
176 smol::block_on(element!(App(state: state.clone())).fullscreen())?;
177 Ok(())
178}