rustyclaw_tui/components/
secrets_dialog.rs1use 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 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 Text(
71 content: "🔐 Secrets Vault",
72 color: theme::ACCENT_BRIGHT,
73 weight: Weight::Bold,
74 )
75
76 View(height: 1)
77
78 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 #(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 #(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 #(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 #(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}