use std::sync::Arc;
use serde_json::{Map, Value};
use crate::tui::model::{FieldKind, FieldSchema, KeyValueField};
use super::{
error::FieldCoercionError,
field::{FieldState, components::ComponentPalette},
form_state::FormState,
section::SectionState,
};
#[derive(Debug, Clone)]
pub struct KeyValueState {
pointer: String,
template: KeyValueField,
entries: Vec<KeyValueEntry>,
selected: usize,
counter: usize,
palette: Arc<ComponentPalette>,
}
#[derive(Debug, Clone)]
struct KeyValueEntry {
key: String,
value: Value,
}
#[derive(Debug, Clone)]
pub struct KeyValueEditorSession {
pub form_state: FormState,
pub schema: Value,
}
#[derive(Debug)]
pub struct KeyValueEditorContext {
pub entry_index: usize,
#[allow(dead_code)]
pub entry_label: String,
pub session: KeyValueEditorSession,
}
impl KeyValueState {
pub fn new(
pointer: &str,
template: &KeyValueField,
default: Option<&Value>,
palette: Arc<ComponentPalette>,
) -> Self {
let mut state = Self {
pointer: pointer.to_string(),
template: template.clone(),
entries: Vec::new(),
selected: 0,
counter: 0,
palette,
};
if let Some(Value::Object(map)) = default {
state.seed_entries_from_object(map);
}
state
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn selected_index(&self) -> Option<usize> {
if self.entries.is_empty() {
None
} else {
Some(self.selected.min(self.entries.len() - 1))
}
}
pub fn select(&mut self, delta: i32) -> bool {
if self.entries.is_empty() {
return false;
}
let len = self.entries.len() as i32;
let next = (self.selected as i32 + delta).clamp(0, len - 1);
let changed = next as usize != self.selected;
self.selected = next as usize;
changed
}
pub fn set_selected(&mut self, index: usize) -> bool {
if self.entries.is_empty() {
return false;
}
let len = self.entries.len();
let bounded = index.min(len.saturating_sub(1));
let changed = bounded != self.selected;
self.selected = bounded;
changed
}
pub fn add_entry(&mut self) -> bool {
let placeholder = self.next_placeholder_key();
let entry = KeyValueEntry {
key: placeholder,
value: self.template.value_default.clone().unwrap_or(Value::Null),
};
self.entries.push(entry);
self.selected = self.entries.len().saturating_sub(1);
true
}
pub fn remove_selected(&mut self) -> bool {
if self.entries.is_empty() {
return false;
}
let idx = self.selected.min(self.entries.len() - 1);
self.entries.remove(idx);
if idx >= self.entries.len() {
self.selected = self.entries.len().saturating_sub(1);
}
true
}
pub fn move_selected(&mut self, delta: i32) -> bool {
if self.entries.len() < 2 {
return false;
}
let len = self.entries.len() as i32;
let target = self.selected as i32 + delta;
if target < 0 || target >= len {
return false;
}
self.entries.swap(self.selected, target as usize);
self.selected = target as usize;
true
}
pub fn selected_label(&self) -> Option<String> {
let idx = self.selected_index()?;
let entry = self.entries.get(idx)?;
Some(format!("{} = {}", entry.key, summarize_value(&entry.value)))
}
pub fn summaries(&self) -> Vec<String> {
self.entries
.iter()
.map(|entry| format!("{} = {}", entry.key, summarize_value(&entry.value)))
.collect()
}
pub fn panel(&self) -> Option<(Vec<String>, usize)> {
let idx = self.selected_index()?;
Some((self.summaries(), idx))
}
pub fn build_value(&self, required: bool) -> Result<Option<Value>, FieldCoercionError> {
if self.entries.is_empty() {
return if required {
Ok(Some(Value::Object(Map::new())))
} else {
Ok(None)
};
}
let mut map = Map::new();
for entry in &self.entries {
let key = entry.key.trim();
if key.is_empty() {
return Err(FieldCoercionError {
pointer: self.pointer.clone(),
message: "key cannot be empty".to_string(),
});
}
if map.insert(key.to_string(), entry.value.clone()).is_some() {
return Err(FieldCoercionError {
pointer: pointer_for_key(&self.pointer, key),
message: format!("duplicate key '{key}'"),
});
}
}
Ok(Some(Value::Object(map)))
}
pub fn seed_entries_from_object(&mut self, map: &Map<String, Value>) {
self.entries.clear();
for (key, value) in map {
self.entries.push(KeyValueEntry {
key: key.clone(),
value: value.clone(),
});
}
if self.entries.is_empty() {
self.selected = 0;
} else {
self.selected = self.selected.min(self.entries.len() - 1);
}
}
pub fn open_selected_editor(&mut self) -> Result<KeyValueEditorContext, FieldCoercionError> {
let idx = self.selected_index().ok_or_else(|| FieldCoercionError {
pointer: self.pointer.clone(),
message: "no entry selected".to_string(),
})?;
let entry = self.entries.get(idx).ok_or_else(|| FieldCoercionError {
pointer: self.pointer.clone(),
message: "invalid entry selection".to_string(),
})?;
let form_state = self.build_form_state(Some(entry));
Ok(KeyValueEditorContext {
entry_index: idx,
entry_label: format!("{} = {}", entry.key, summarize_value(&entry.value)),
session: KeyValueEditorSession {
form_state,
schema: self.template.entry_schema.clone(),
},
})
}
pub fn apply_editor_session(
&mut self,
entry_index: usize,
session: &KeyValueEditorSession,
) -> Result<bool, FieldCoercionError> {
let value = session.form_state.try_build_value()?;
let object = value.as_object().ok_or_else(|| FieldCoercionError {
pointer: self.pointer.clone(),
message: "overlay did not produce an object".to_string(),
})?;
let key_value = object.get("key").ok_or_else(|| FieldCoercionError {
pointer: self.pointer.clone(),
message: "missing key field".to_string(),
})?;
let key = if let Some(raw) = key_value.as_str() {
raw.to_string()
} else {
key_value.to_string()
};
if key.trim().is_empty() {
return Err(FieldCoercionError {
pointer: pointer_for_key(&self.pointer, &key),
message: "key cannot be empty".to_string(),
});
}
let value_field = object.get("value").cloned().unwrap_or(Value::Null);
for (idx, existing) in self.entries.iter().enumerate() {
if idx != entry_index && existing.key == key {
return Err(FieldCoercionError {
pointer: pointer_for_key(&self.pointer, &key),
message: format!("duplicate key '{key}'"),
});
}
}
let changed = self
.entries
.get_mut(entry_index)
.map(|entry| {
let mut dirty = false;
if entry.key != key {
entry.key = key;
dirty = true;
}
if entry.value != value_field {
entry.value = value_field;
dirty = true;
}
dirty
})
.unwrap_or(false);
if changed {
self.selected = entry_index;
}
Ok(changed)
}
fn build_form_state(&self, entry: Option<&KeyValueEntry>) -> FormState {
let mut key_schema = self.key_field_schema();
key_schema.default = entry
.map(|item| Value::String(item.key.clone()))
.or_else(|| self.template.key_default.clone());
let mut value_schema = self.value_field_schema();
if let Some(item) = entry {
value_schema.default = Some(item.value.clone());
} else if value_schema.default.is_none() {
value_schema.default = self.template.value_default.clone();
}
let key_field = FieldState::from_schema_with_palette(key_schema, Arc::clone(&self.palette));
let value_field =
FieldState::from_schema_with_palette(value_schema, Arc::clone(&self.palette));
let section = SectionState {
id: "key_value".to_string(),
title: "Key/Value Entry".to_string(),
description: None,
path: vec!["entry".to_string()],
depth: 0,
fields: vec![key_field, value_field],
scroll_offset: 0,
};
FormState::from_sections("key_value", "Key/Value Entry", None, vec![section])
}
fn key_field_schema(&self) -> FieldSchema {
FieldSchema {
name: "key".to_string(),
path: vec!["key".to_string()],
pointer: "/key".to_string(),
title: self.template.key_title.clone(),
description: self.template.key_description.clone(),
kind: FieldKind::String,
required: true,
default: self.template.key_default.clone(),
metadata: Default::default(),
}
}
fn value_field_schema(&self) -> FieldSchema {
FieldSchema {
name: "value".to_string(),
path: vec!["value".to_string()],
pointer: "/value".to_string(),
title: self.template.value_title.clone(),
description: self.template.value_description.clone(),
kind: (*self.template.value_kind).clone(),
required: true,
default: self.template.value_default.clone(),
metadata: Default::default(),
}
}
fn next_placeholder_key(&mut self) -> String {
loop {
let candidate = format!("key-{}", self.counter + 1);
self.counter = self.counter.saturating_add(1);
if !self.entries.iter().any(|entry| entry.key == candidate) {
return candidate;
}
}
}
}
pub(crate) fn summarize_value(value: &Value) -> String {
match value {
Value::Null => "null".to_string(),
Value::Bool(flag) => flag.to_string(),
Value::Number(num) => num.to_string(),
Value::String(text) => summarize_string(text),
Value::Array(items) => format!("array({})", items.len()),
Value::Object(map) => format!("object({})", map.len()),
}
}
fn summarize_string(text: &str) -> String {
use unicode_width::UnicodeWidthStr;
const MAX_VISIBLE: usize = 36;
const TRUNCATE_TO: usize = MAX_VISIBLE - 12;
let char_count = UnicodeWidthStr::width(text);
if char_count > MAX_VISIBLE {
let mut truncated = String::new();
for ch in text.chars().take(TRUNCATE_TO) {
truncated.push(ch);
}
format!("\"{}…\"", truncated)
} else {
format!("\"{text}\"")
}
}
fn pointer_for_key(base: &str, key: &str) -> String {
let mut encoded = String::new();
for ch in key.chars() {
match ch {
'~' => encoded.push_str("~0"),
'/' => encoded.push_str("~1"),
other => encoded.push(other),
}
}
if base.is_empty() {
format!("/{}", encoded)
} else if base.ends_with('/') {
format!("{base}{encoded}")
} else {
format!("{base}/{}", encoded)
}
}