Skip to content

Commit 9b02ea9

Browse files
committed
feat(tui): add config key editing to update provider form
Signed-off-by: Artem Lytvyn <alytvyn@redhat.com>
1 parent 64e0d7d commit 9b02ea9

3 files changed

Lines changed: 325 additions & 55 deletions

File tree

crates/openshell-tui/src/app.rs

Lines changed: 130 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
#![warn(missing_debug_implementations)]
5+
46
use std::collections::HashMap;
57
use std::time::{Duration, Instant};
68

@@ -368,11 +370,13 @@ pub struct CreateProviderForm {
368370
pub credentials: Vec<(String, String)>,
369371
/// Which credential row is focused.
370372
pub cred_cursor: usize,
371-
/// TODO: inline doc for config, possibilities of using IndexMap
373+
/// Provider config key-value pairs (e.g. `ANTHROPIC_BASE_URL`).
372374
pub config: IndexMap<String, String>,
373-
/// Which config row is focused.
375+
/// Which existing config entry is selected (for deletion).
374376
pub config_cursor: usize,
377+
/// Config key being entered.
375378
pub config_key_input: String,
379+
/// Config value being entered.
376380
pub config_value_input: String,
377381
/// For generic / types with no known env vars: custom env var name.
378382
pub generic_env_name: String,
@@ -508,9 +512,22 @@ pub struct UpdateProviderForm {
508512
pub provider_type: String,
509513
pub credential_key: String,
510514
pub new_value: String,
515+
pub config: IndexMap<String, String>,
516+
pub config_key_input: String,
517+
pub config_value_input: String,
518+
pub config_cursor: usize,
519+
pub focus: UpdateProviderField,
511520
pub status: Option<String>,
512521
}
513522

523+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524+
pub enum UpdateProviderField {
525+
CredentialValue,
526+
ConfigKey,
527+
ConfigValue,
528+
Submit,
529+
}
530+
514531
// ---------------------------------------------------------------------------
515532
// App state
516533
// ---------------------------------------------------------------------------
@@ -2358,9 +2375,8 @@ impl App {
23582375
.cloned()
23592376
.unwrap_or_default();
23602377
form.config.shift_remove(&key_to_remove);
2361-
form.config_cursor = form
2362-
.config_cursor
2363-
.min(form.config.len().saturating_sub(1));
2378+
form.config_cursor =
2379+
form.config_cursor.min(form.config.len().saturating_sub(1));
23642380
}
23652381
}
23662382
_ => {
@@ -2503,6 +2519,17 @@ impl App {
25032519
.get(self.provider_selected)
25042520
.cloned()
25052521
.unwrap_or_default();
2522+
let existing_config = self
2523+
.provider_entries
2524+
.get(self.provider_selected)
2525+
.map(|e| {
2526+
e.provider
2527+
.config
2528+
.iter()
2529+
.map(|(k, v)| (k.clone(), v.clone()))
2530+
.collect::<IndexMap<_, _>>()
2531+
})
2532+
.unwrap_or_default();
25062533

25072534
// If we don't know the credential key, derive from registry.
25082535
let key = if cred_key.is_empty() {
@@ -2520,6 +2547,11 @@ impl App {
25202547
provider_type: ptype,
25212548
credential_key: key,
25222549
new_value: String::new(),
2550+
config: existing_config,
2551+
config_key_input: String::new(),
2552+
config_value_input: String::new(),
2553+
config_cursor: 0,
2554+
focus: UpdateProviderField::CredentialValue,
25232555
status: None,
25242556
});
25252557
}
@@ -2533,18 +2565,100 @@ impl App {
25332565
KeyCode::Esc => {
25342566
self.update_provider_form = None;
25352567
}
2536-
KeyCode::Enter => {
2537-
if form.new_value.is_empty() {
2538-
form.status = Some("Value is required.".to_string());
2539-
return;
2568+
KeyCode::Tab => match form.focus {
2569+
UpdateProviderField::CredentialValue => {
2570+
form.focus = UpdateProviderField::ConfigKey;
25402571
}
2541-
self.pending_provider_update = true;
2542-
}
2543-
KeyCode::Char(c) => form.new_value.push(c),
2544-
KeyCode::Backspace => {
2545-
form.new_value.pop();
2546-
}
2547-
_ => {}
2572+
UpdateProviderField::ConfigKey => {
2573+
form.focus = UpdateProviderField::ConfigValue;
2574+
}
2575+
UpdateProviderField::ConfigValue => {
2576+
if !form.config_key_input.is_empty() && !form.config_value_input.is_empty() {
2577+
form.config.insert(
2578+
std::mem::take(&mut form.config_key_input),
2579+
std::mem::take(&mut form.config_value_input),
2580+
);
2581+
form.focus = UpdateProviderField::ConfigKey;
2582+
} else if form.config_key_input.is_empty() && form.config_value_input.is_empty()
2583+
{
2584+
form.focus = UpdateProviderField::Submit;
2585+
} else {
2586+
form.focus = UpdateProviderField::ConfigKey;
2587+
}
2588+
}
2589+
UpdateProviderField::Submit => {
2590+
form.focus = UpdateProviderField::CredentialValue;
2591+
}
2592+
},
2593+
KeyCode::BackTab => match form.focus {
2594+
UpdateProviderField::CredentialValue => {
2595+
form.focus = UpdateProviderField::Submit;
2596+
}
2597+
UpdateProviderField::ConfigKey => {
2598+
form.focus = UpdateProviderField::CredentialValue;
2599+
}
2600+
UpdateProviderField::ConfigValue => {
2601+
form.focus = UpdateProviderField::ConfigKey;
2602+
}
2603+
UpdateProviderField::Submit => {
2604+
form.focus = UpdateProviderField::ConfigValue;
2605+
}
2606+
},
2607+
_ => match form.focus {
2608+
UpdateProviderField::CredentialValue => {
2609+
Self::handle_text_input(&mut form.new_value, key);
2610+
}
2611+
UpdateProviderField::ConfigKey => match key.code {
2612+
KeyCode::Enter => {
2613+
if !form.config_key_input.is_empty() && !form.config_value_input.is_empty()
2614+
{
2615+
form.config.insert(
2616+
std::mem::take(&mut form.config_key_input),
2617+
std::mem::take(&mut form.config_value_input),
2618+
);
2619+
}
2620+
}
2621+
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
2622+
if !form.config.is_empty() {
2623+
let key_to_remove = form
2624+
.config
2625+
.keys()
2626+
.nth(form.config_cursor)
2627+
.cloned()
2628+
.unwrap_or_default();
2629+
form.config.shift_remove(&key_to_remove);
2630+
form.config_cursor =
2631+
form.config_cursor.min(form.config.len().saturating_sub(1));
2632+
}
2633+
}
2634+
_ => {
2635+
Self::handle_text_input(&mut form.config_key_input, key);
2636+
}
2637+
},
2638+
UpdateProviderField::ConfigValue => match key.code {
2639+
KeyCode::Enter => {
2640+
if !form.config_key_input.is_empty() && !form.config_value_input.is_empty()
2641+
{
2642+
form.config.insert(
2643+
std::mem::take(&mut form.config_key_input),
2644+
std::mem::take(&mut form.config_value_input),
2645+
);
2646+
}
2647+
}
2648+
_ => {
2649+
Self::handle_text_input(&mut form.config_value_input, key);
2650+
}
2651+
},
2652+
UpdateProviderField::Submit => {
2653+
if key.code == KeyCode::Enter {
2654+
if form.new_value.is_empty() {
2655+
form.status = Some("Value is required.".to_string());
2656+
return;
2657+
}
2658+
self.pending_provider_update = true;
2659+
}
2660+
}
2661+
},
25482662
}
25492663
}
25502664

crates/openshell-tui/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1699,6 +1699,11 @@ fn spawn_update_provider(app: &App, tx: mpsc::UnboundedSender<Event>) {
16991699
let ptype = form.provider_type.clone();
17001700
let cred_key = form.credential_key.clone();
17011701
let new_value = form.new_value.clone();
1702+
let config: HashMap<String, String> = form
1703+
.config
1704+
.iter()
1705+
.map(|(k, v)| (k.clone(), v.clone()))
1706+
.collect();
17021707

17031708
tokio::spawn(async move {
17041709
let mut credentials = HashMap::new();
@@ -1715,7 +1720,7 @@ fn spawn_update_provider(app: &App, tx: mpsc::UnboundedSender<Event>) {
17151720
}),
17161721
r#type: ptype,
17171722
credentials,
1718-
config: HashMap::default(),
1723+
config,
17191724
credential_expires_at_ms: HashMap::default(),
17201725
}),
17211726
credential_expires_at_ms: HashMap::default(),

0 commit comments

Comments
 (0)