Skip to main content

rustyclaw_tui/components/
secrets_dialog.rs

1// ── Secrets dialog — interactive vault management overlay ────────────────────
2
3use crate::theme;
4use iocraft::prelude::*;
5
6#[derive(Debug, Clone, Default)]
7pub struct SecretInfo {
8    pub name: String,
9    pub label: String,
10    pub kind: String,
11    pub policy: String,
12    pub disabled: bool,
13}
14
15#[derive(Default, Props)]
16pub struct SecretsDialogProps {
17    pub secrets: Vec<SecretInfo>,
18    pub agent_access: bool,
19    pub has_totp: bool,
20    pub selected: Option<usize>,
21    pub scroll_offset: usize,
22    /// 0 = normal, 1 = entering name, 2 = entering value
23    pub add_step: u8,
24    pub add_name: String,
25    pub add_value: String,
26}
27
28#[component]
29pub fn SecretsDialog(props: &SecretsDialogProps) -> impl Into<AnyElement<'static>> {
30    let access_label = if props.agent_access {
31        "Enabled"
32    } else {
33        "Disabled"
34    };
35    let access_color = if props.agent_access {
36        theme::SUCCESS
37    } else {
38        theme::WARN
39    };
40    let totp_label = if props.has_totp { "On" } else { "Off" };
41    let totp_color = if props.has_totp {
42        theme::SUCCESS
43    } else {
44        theme::MUTED
45    };
46    let count = props.secrets.len();
47    let sel = props.selected.unwrap_or(0);
48
49    element! {
50        View(
51            width: 100pct,
52            height: 100pct,
53            justify_content: JustifyContent::Center,
54            align_items: AlignItems::Center,
55        ) {
56            View(
57                width: 70pct,
58                max_height: 80pct,
59                flex_direction: FlexDirection::Column,
60                border_style: BorderStyle::Round,
61                border_color: theme::ACCENT_BRIGHT,
62                background_color: theme::BG_SURFACE,
63                padding_left: 2,
64                padding_right: 2,
65                padding_top: 1,
66                padding_bottom: 1,
67                overflow: Overflow::Hidden,
68            ) {
69                // Title
70                Text(
71                    content: "🔐 Secrets Vault",
72                    color: theme::ACCENT_BRIGHT,
73                    weight: Weight::Bold,
74                )
75
76                View(height: 1)
77
78                // Summary line
79                View(flex_direction: FlexDirection::Row) {
80                    Text(content: "Agent Access: ", color: theme::TEXT_DIM)
81                    Text(content: access_label, color: access_color)
82                    Text(content: "  │  ", color: theme::MUTED)
83                    Text(content: format!("{} credential{}", count, if count == 1 { "" } else { "s" }), color: theme::TEXT_DIM)
84                    Text(content: "  │  2FA: ", color: theme::TEXT_DIM)
85                    Text(content: totp_label, color: totp_color)
86                }
87
88                View(height: 1)
89
90                // Credential list
91                #(if props.secrets.is_empty() {
92                    element! {
93                        Text(content: "  No credentials stored.  Press 'a' to add one.", color: theme::MUTED)
94                    }.into_any()
95                } else {
96                    element! {
97                        View(
98                            flex_direction: FlexDirection::Column,
99                            width: 100pct,
100                            overflow: Overflow::Hidden,
101                        ) {
102                            #(props.secrets.iter().enumerate().skip(props.scroll_offset).take(20).map(|(i, s)| {
103                                let is_selected = i == sel;
104                                let bg = if is_selected { Some(theme::ACCENT_BRIGHT) } else { None };
105                                let pointer = if is_selected { "▸ " } else { "  " };
106                                let fg = if is_selected {
107                                    theme::BG_MAIN
108                                } else if s.disabled {
109                                    theme::MUTED
110                                } else {
111                                    theme::TEXT
112                                };
113                                let status = if s.disabled { "OFF".to_string() } else { s.policy.clone() };
114                                let suffix = if !s.name.is_empty() && s.name != s.label {
115                                    format!(" — {}", s.name)
116                                } else {
117                                    String::new()
118                                };
119                                let line = format!("{}{:10}  {:5}  {}{}", pointer, s.kind, status, s.label, suffix);
120                                element! {
121                                    View(
122                                        key: i as u64,
123                                        width: 100pct,
124                                        background_color: bg.unwrap_or(Color::Reset),
125                                    ) {
126                                        Text(content: line, color: fg, wrap: TextWrap::NoWrap)
127                                    }
128                                }
129                            }))
130                        }
131                    }.into_any()
132                })
133
134                View(height: 1)
135
136                // Add-secret inline input
137                #(if props.add_step > 0 {
138                    let (label, input_text) = if props.add_step == 1 {
139                        ("Name: ", props.add_name.as_str())
140                    } else {
141                        ("Value: ", props.add_value.as_str())
142                    };
143                    let cursor_display = format!("{}{}█", label, input_text);
144                    element! {
145                        View(
146                            flex_direction: FlexDirection::Column,
147                            width: 100pct,
148                        ) {
149                            Text(content: "Add Secret", color: theme::ACCENT_BRIGHT, weight: Weight::Bold)
150                            View(
151                                width: 100pct,
152                                border_style: BorderStyle::Round,
153                                border_color: theme::ACCENT_BRIGHT,
154                                padding_left: 1,
155                                padding_right: 1,
156                            ) {
157                                Text(content: cursor_display, color: theme::TEXT, wrap: TextWrap::NoWrap)
158                            }
159                            Text(
160                                content: if props.add_step == 1 {
161                                    "Enter name, then press Enter for value  │  Esc cancel"
162                                } else {
163                                    "Enter value, then press Enter to save   │  Esc cancel"
164                                },
165                                color: theme::MUTED,
166                            )
167                        }
168                    }.into_any()
169                } else {
170                    element! { View() }.into_any()
171                })
172
173                // Legend (hide when in add mode)
174                #(if props.add_step == 0 {
175                    element! {
176                        View(flex_direction: FlexDirection::Row) {
177                            View(background_color: theme::SUCCESS) {
178                                Text(content: " OPEN ", color: theme::BG_MAIN)
179                            }
180                            Text(content: " anytime  ", color: theme::TEXT_DIM)
181                            View(background_color: theme::WARN) {
182                                Text(content: " ASK ", color: theme::BG_MAIN)
183                            }
184                            Text(content: " per-use  ", color: theme::TEXT_DIM)
185                            View(background_color: theme::ERROR) {
186                                Text(content: " AUTH ", color: theme::BG_MAIN)
187                            }
188                            Text(content: " re-auth  ", color: theme::TEXT_DIM)
189                            View(background_color: theme::INFO) {
190                                Text(content: " SKILL ", color: theme::BG_MAIN)
191                            }
192                            Text(content: " gated", color: theme::TEXT_DIM)
193                        }
194                    }.into_any()
195                } else {
196                    element! { View() }.into_any()
197                })
198
199                View(height: 1)
200
201                // Hint
202                #(if props.add_step == 0 {
203                    element! {
204                        View(flex_direction: FlexDirection::Row) {
205                            Text(content: "↑↓ ", color: theme::ACCENT_BRIGHT)
206                            Text(content: "navigate  ", color: theme::MUTED)
207                            Text(content: "Enter ", color: theme::ACCENT_BRIGHT)
208                            Text(content: "cycle policy  ", color: theme::MUTED)
209                            Text(content: "a ", color: theme::ACCENT_BRIGHT)
210                            Text(content: "add  ", color: theme::MUTED)
211                            Text(content: "d ", color: theme::ACCENT_BRIGHT)
212                            Text(content: "delete  ", color: theme::MUTED)
213                            Text(content: "Esc ", color: theme::ACCENT_BRIGHT)
214                            Text(content: "close", color: theme::MUTED)
215                        }
216                    }.into_any()
217                } else {
218                    element! { View() }.into_any()
219                })
220            }
221        }
222    }
223}