steer_tui/tui/widgets/setup/
authentication.rs1use crate::tui::state::SetupState;
2use crate::tui::theme::{Component, Theme};
3use ratatui::{
4 buffer::Buffer,
5 layout::{Alignment, Constraint, Direction, Layout, Rect},
6 text::{Line, Span},
7 widgets::{Block, Borders, Paragraph, Widget, Wrap},
8};
9use steer_core::api::ProviderKind;
10
11pub struct AuthenticationWidget;
12
13impl AuthenticationWidget {
14 pub fn render(
15 area: Rect,
16 buf: &mut Buffer,
17 state: &SetupState,
18 provider: ProviderKind,
19 theme: &Theme,
20 ) {
21 let chunks = Layout::default()
22 .direction(Direction::Vertical)
23 .constraints([
24 Constraint::Length(4), Constraint::Min(10), Constraint::Length(3), Constraint::Length(3), ])
29 .split(area);
30
31 let provider_name = provider.display_name();
33
34 let header = vec![
35 Line::from(""),
36 Line::from(Span::styled(
37 format!("Authenticate with {provider_name}"),
38 theme.style(Component::SetupHeader),
39 )),
40 ];
41
42 Paragraph::new(header)
43 .block(
44 Block::default()
45 .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
46 .border_style(theme.style(Component::SetupBorder)),
47 )
48 .alignment(Alignment::Center)
49 .render(chunks[0], buf);
50
51 let mut content = vec![];
53
54 if let Some(oauth_state) = &state.oauth_state {
55 content.push(Line::from(""));
57 content.push(Line::from(Span::styled(
58 "OAuth Authentication",
59 theme.style(Component::SetupHeader),
60 )));
61 content.push(Line::from(""));
62
63 if state.oauth_callback_input.is_empty() {
64 content.push(Line::from("Please visit this URL in your browser:"));
65 content.push(Line::from(""));
66 content.push(Line::from(Span::styled(
67 &oauth_state.auth_url,
68 theme.style(Component::SetupUrl),
69 )));
70 content.push(Line::from(""));
71 content.push(Line::from(
72 "After authorizing, you'll be redirected to a page showing a code.",
73 ));
74 content.push(Line::from(
75 "Copy the ENTIRE code (including the part after the #)",
76 ));
77 content.push(Line::from("and paste it below:"));
78 content.push(Line::from(""));
79 content.push(Line::from(vec![
80 Span::styled("Code: ", theme.style(Component::SetupInputLabel)),
81 Span::styled(
82 if state.oauth_callback_input.is_empty() {
83 "_"
84 } else {
85 &state.oauth_callback_input
86 },
87 theme.style(Component::SetupInputValue),
88 ),
89 ]));
90 } else {
91 content.push(Line::from("Processing authorization code..."));
92 content.push(Line::from(""));
93 content.push(Line::from(vec![
94 Span::styled("Code: ", theme.style(Component::SetupInputLabel)),
95 Span::styled(
96 &state.oauth_callback_input,
97 theme.style(Component::SetupInputValue),
98 ),
99 ]));
100 }
101 } else if provider == ProviderKind::Anthropic
102 && state.api_key_input.is_empty()
103 && state.oauth_state.is_none()
104 && state.auth_providers.get(&provider)
105 != Some(&crate::tui::state::AuthStatus::InProgress)
106 {
107 content.push(Line::from(""));
109 content.push(Line::from("Choose authentication method:"));
110 content.push(Line::from(""));
111 content.push(Line::from(vec![
112 Span::styled("1. ", theme.style(Component::SetupKeyBinding)),
113 Span::raw("OAuth Login "),
114 Span::styled(
115 "(Recommended for Claude Pro users)",
116 theme.style(Component::SetupHint),
117 ),
118 ]));
119 content.push(Line::from(vec![
120 Span::styled("2. ", theme.style(Component::SetupKeyBinding)),
121 Span::raw("API Key"),
122 ]));
123 } else {
124 content.push(Line::from(""));
126 content.push(Line::from(format!("Enter your {provider_name} API key:")));
127 content.push(Line::from(""));
128
129 let masked_key = if state.api_key_input.is_empty() {
130 String::from("_")
131 } else {
132 "*".repeat(state.api_key_input.len())
133 };
134
135 content.push(Line::from(vec![
136 Span::styled("API Key: ", theme.style(Component::SetupInputLabel)),
137 Span::styled(masked_key, theme.style(Component::SetupInputValue)),
138 ]));
139
140 if provider == ProviderKind::Anthropic {
141 content.push(Line::from(""));
142 content.push(Line::from(Span::styled(
143 "Tip: Get your API key from console.anthropic.com",
144 theme.style(Component::SetupHint),
145 )));
146 }
147 }
148
149 Paragraph::new(content)
150 .block(
151 Block::default()
152 .borders(Borders::LEFT | Borders::RIGHT)
153 .border_style(theme.style(Component::SetupBorder)),
154 )
155 .alignment(Alignment::Left)
156 .wrap(Wrap { trim: true })
157 .render(chunks[1], buf);
158
159 if let Some(error) = &state.error_message {
161 let error_text = vec![Line::from(Span::styled(
162 format!("Error: {error}"),
163 theme.style(Component::SetupErrorMessage),
164 ))];
165
166 Paragraph::new(error_text)
167 .block(
168 Block::default()
169 .borders(Borders::LEFT | Borders::RIGHT)
170 .border_style(theme.style(Component::SetupBorder)),
171 )
172 .alignment(Alignment::Center)
173 .render(chunks[2], buf);
174 }
175
176 let instructions = if state.oauth_state.is_some() {
178 vec![Line::from(vec![
179 Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
180 Span::raw(" to cancel"),
181 ])]
182 } else if provider == ProviderKind::Anthropic && state.api_key_input.is_empty() {
183 vec![Line::from(vec![
184 Span::raw("Press "),
185 Span::styled("1", theme.style(Component::SetupKeyBinding)),
186 Span::raw(" or "),
187 Span::styled("2", theme.style(Component::SetupKeyBinding)),
188 Span::raw(" to select, "),
189 Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
190 Span::raw(" to go back"),
191 ])]
192 } else {
193 vec![Line::from(vec![
194 Span::raw("Type your API key, "),
195 Span::styled("Enter", theme.style(Component::SetupKeyBinding)),
196 Span::raw(" to submit, "),
197 Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
198 Span::raw(" to go back"),
199 ])]
200 };
201
202 Paragraph::new(instructions)
203 .block(
204 Block::default()
205 .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)
206 .border_style(theme.style(Component::SetupBorder)),
207 )
208 .alignment(Alignment::Center)
209 .render(chunks[3], buf);
210 }
211}