use serde_json::Value;
use super::super::edits::EditKey;
use super::super::field::{FieldKind, ListItem};
use super::super::viewport::Viewport;
use super::{ConfigPane, PaneEdit, PaneRow};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListRow {
Item(usize),
New,
}
#[derive(Clone, PartialEq, Eq)]
pub struct ListPane {
key: String,
item: ListItem,
elements: Vec<String>,
view: Viewport,
typing: Option<(Option<usize>, String)>,
}
impl ListPane {
#[must_use]
pub fn new(key: String, item: ListItem, elements: Vec<String>) -> Self {
Self {
key,
item,
elements,
view: Viewport::new(),
typing: None,
}
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
#[must_use]
pub fn item(&self) -> ListItem {
self.item
}
#[must_use]
pub fn elements(&self) -> &[String] {
&self.elements
}
#[must_use]
pub fn rows(&self) -> Vec<ListRow> {
let mut rows: Vec<ListRow> = (0..self.elements.len()).map(ListRow::Item).collect();
rows.push(ListRow::New);
rows
}
#[must_use]
pub fn cursor(&self) -> Option<ListRow> {
self.rows().get(self.view.cursor()).copied()
}
#[must_use]
pub fn view(&self) -> &Viewport {
&self.view
}
#[must_use]
pub fn typing(&self) -> Option<(Option<usize>, &str)> {
self.typing
.as_ref()
.map(|(index, buffer)| (*index, buffer.as_str()))
}
pub fn set_rows(&mut self, rows: usize) {
let len = self.rows().len();
self.view.set_rows(rows, len);
}
pub(in crate::lookout) fn move_by(&mut self, delta: isize) {
let len = self.rows().len();
self.view.move_by(delta, len);
}
pub(in crate::lookout) fn move_to(&mut self, index: usize) {
let len = self.rows().len();
self.view.move_to(index, len);
}
pub(in crate::lookout) fn move_to_first(&mut self) {
self.move_to(0);
}
pub(in crate::lookout) fn move_to_last(&mut self) {
let len = self.rows().len();
self.move_to(len.saturating_sub(1));
}
pub(in crate::lookout) fn adopt_view(&mut self, view: Viewport) {
self.view = view;
let len = self.rows().len();
self.view.clamp(len);
}
pub(in crate::lookout) fn set_elements(&mut self, elements: Vec<String>) {
self.elements = elements;
let len = self.rows().len();
self.view.clamp(len);
}
pub fn begin_typing(&mut self) {
self.typing = match self.cursor() {
Some(ListRow::Item(index)) => self
.elements
.get(index)
.map(|element| (Some(index), element.clone())),
Some(ListRow::New) => Some((None, String::new())),
None => None,
};
}
pub fn type_char(&mut self, typed: char) {
if let Some((_, buffer)) = self.typing.as_mut() {
buffer.push(typed);
}
}
pub fn type_backspace(&mut self) {
if let Some((_, buffer)) = self.typing.as_mut() {
buffer.pop();
}
}
pub fn abandon_typing(&mut self) {
self.typing = None;
}
pub fn apply_typing(&mut self) -> Option<String> {
let (_, buffer) = self.typing.as_ref()?;
if self.item == ListItem::Integer && buffer.parse::<i64>().is_err() && !buffer.is_empty() {
return None;
}
let (_, buffer) = self.typing.take()?;
(!buffer.is_empty()).then_some(buffer)
}
pub(in crate::lookout) fn with_element(&self, text: String) -> Option<Vec<String>> {
let mut elements = self.elements.clone();
match self.cursor()? {
ListRow::Item(index) => *elements.get_mut(index)? = text,
ListRow::New => elements.push(text),
}
Some(elements)
}
pub(in crate::lookout) fn without_element(&self) -> Option<Vec<String>> {
let ListRow::Item(index) = self.cursor()? else {
return None;
};
let mut elements = self.elements.clone();
(index < elements.len()).then(|| {
elements.remove(index);
elements
})
}
pub(in crate::lookout) fn reordered(&self, delta: isize) -> Option<Vec<String>> {
let ListRow::Item(index) = self.cursor()? else {
return None;
};
let target = usize::try_from(isize::try_from(index).ok()? + delta).ok()?;
if target >= self.elements.len() {
return None;
}
let mut elements = self.elements.clone();
elements.swap(index, target);
Some(elements)
}
}
fn array_elements(value: &Value) -> Vec<String> {
let Value::Array(values) = value else {
return Vec::new();
};
values
.iter()
.map(|value| match value {
Value::String(text) => text.clone(),
other => other.to_string(),
})
.collect()
}
fn list_value(item: ListItem, elements: &[String]) -> Value {
let element = |text: &String| match item {
ListItem::Text => Value::String(text.clone()),
ListItem::Integer => text
.parse::<i64>()
.map_or_else(|_| Value::String(text.clone()), Value::from),
};
Value::Array(elements.iter().map(element).collect())
}
impl core::fmt::Debug for ListPane {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"ListPane {{ key: {:?}, item: {:?}, elements: {}, typing: {} }}",
self.key,
self.item,
self.elements.len(),
if self.typing.is_some() {
"some"
} else {
"none"
}
)
}
}
impl ConfigPane {
#[must_use]
pub fn list(&self) -> Option<&ListPane> {
self.list.as_ref()
}
pub(in crate::lookout) fn list_mut(&mut self) -> Option<&mut ListPane> {
self.list.as_mut()
}
pub(in crate::lookout) fn open_list(&mut self) {
let Some(PaneRow::Field(index)) = self.cursor() else {
return;
};
let Some(field) = self.fields.fields().get(index) else {
return;
};
let FieldKind::List(item) = field.kind else {
return;
};
let key = field.key.clone();
let elements = self.filed_elements_of(&key);
self.list = Some(ListPane::new(key, item, elements));
}
pub(in crate::lookout) fn close_list(&mut self) {
self.list = None;
}
fn elements_of(&self, key: &str) -> Vec<String> {
self.values.get(key).map(array_elements).unwrap_or_default()
}
pub(super) fn filed_elements_of(&self, key: &str) -> Vec<String> {
let filed = match self.edits.get(&EditKey::Field(key.to_owned())) {
Some(entry) => match entry.edit() {
PaneEdit::Set { value, .. } => Some(value.as_value()),
PaneEdit::SetEnv { .. } => None,
},
None => None,
};
match filed {
Some(value) => array_elements(value),
None => self.elements_of(key),
}
}
pub(in crate::lookout) fn file_list_element(&mut self, text: String) {
let Some(elements) = self.list.as_ref().and_then(|list| list.with_element(text)) else {
return;
};
self.file_list(elements);
}
pub(in crate::lookout) fn file_list_removal(&mut self) {
let Some(elements) = self.list.as_ref().and_then(ListPane::without_element) else {
return;
};
self.file_list(elements);
}
pub(in crate::lookout) fn file_list_reorder(&mut self, delta: isize) {
let Some(elements) = self.list.as_ref().and_then(|list| list.reordered(delta)) else {
return;
};
self.file_list(elements);
}
fn file_list(&mut self, elements: Vec<String>) {
let Some(list) = self.list.as_ref() else {
return;
};
let key = list.key().to_owned();
let value = list_value(list.item(), &elements);
self.file_field(key, value);
if let Some(list) = self.list.as_mut() {
list.set_elements(elements);
}
}
pub(in crate::lookout) fn adopt_list_view(&mut self, key: &str, view: Viewport) {
let Some(item) = self.fields.by_key(key).and_then(|field| match field.kind {
FieldKind::List(item) => Some(item),
_ => None,
}) else {
return;
};
let mut list = ListPane::new(key.to_owned(), item, self.filed_elements_of(key));
list.adopt_view(view);
self.list = Some(list);
}
}
#[cfg(test)]
mod tests {
use super::super::Lock;
use super::super::fixtures::{filed, web_with_args};
use super::*;
#[test]
fn editing_one_element_sends_the_whole_array() {
let mut pane = ConfigPane::sheep(web_with_args(&["--port", "8080"]));
pane.move_to_key("args");
pane.open_list();
pane.list_mut().expect("open").move_to(1);
pane.file_list_element("9090".into());
assert_eq!(
filed(&pane, "args"),
Some(serde_json::json!(["--port", "9090"]))
);
}
#[test]
fn a_list_panes_debug_names_no_element() {
let list = ListPane::new(
"args".into(),
ListItem::Text,
vec!["--token".into(), "hunter2".into()],
);
assert_eq!(
format!("{list:?}"),
r#"ListPane { key: "args", item: Text, elements: 2, typing: none }"#
);
}
#[test]
fn an_integer_element_that_does_not_parse_keeps_the_editor_open() {
let mut pane = ConfigPane::sheep(web_with_args(&[]));
pane.move_to_key("stop_exit_codes");
pane.open_list();
let list = pane.list_mut().expect("open");
list.move_to_last();
list.begin_typing();
list.type_char('-');
assert_eq!(list.apply_typing(), None);
assert!(list.typing().is_some(), "the editor is still open");
list.type_char('1');
assert_eq!(list.apply_typing().as_deref(), Some("-1"));
}
#[test]
fn removing_and_reordering_file_the_whole_array_too() {
let mut pane = ConfigPane::sheep(web_with_args(&["a", "b", "c"]));
pane.move_to_key("args");
pane.open_list();
pane.list_mut().expect("open").move_to(1);
pane.file_list_removal();
assert_eq!(filed(&pane, "args"), Some(serde_json::json!(["a", "c"])));
pane.file_list_reorder(-1);
assert_eq!(
filed(&pane, "args"),
Some(serde_json::json!(["c", "a"])),
"the cursor is on `c` now, and moving it up files one entry, not two"
);
assert_eq!(pane.edits().len(), 1, "one field, one entry");
}
#[test]
fn a_second_list_keystroke_builds_on_the_first() {
let mut pane = ConfigPane::sheep(web_with_args(&[]));
pane.move_to_key("args");
pane.open_list();
pane.list_mut().expect("open").move_to_last();
pane.file_list_element("abc".into());
pane.list_mut().expect("open").move_to_last();
pane.file_list_element("def".into());
assert_eq!(
filed(&pane, "args"),
Some(serde_json::json!(["abc", "def"]))
);
assert_eq!(pane.edits().len(), 1, "one field, one entry");
}
#[test]
fn re_opening_the_sub_screen_shows_what_was_filed() {
let mut pane = ConfigPane::sheep(web_with_args(&["a", "b"]));
pane.move_to_key("args");
pane.open_list();
pane.list_mut().expect("open").move_to(0);
pane.file_list_removal();
assert_eq!(pane.list().expect("open").elements(), ["b"]);
pane.close_list();
pane.open_list();
assert_eq!(pane.list().expect("re-opened").elements(), ["b"]);
}
#[test]
fn moving_an_element_down_files_the_whole_array() {
let mut pane = ConfigPane::sheep(web_with_args(&["a", "b", "c"]));
pane.move_to_key("args");
pane.open_list();
pane.list_mut().expect("open").move_to(0);
pane.file_list_reorder(1);
assert_eq!(
filed(&pane, "args"),
Some(serde_json::json!(["b", "a", "c"]))
);
}
#[test]
fn a_removal_or_a_move_with_nothing_to_act_on_files_nothing() {
let mut pane = ConfigPane::sheep(web_with_args(&["a", "b"]));
pane.move_to_key("args");
pane.open_list();
pane.list_mut().expect("open").move_to_last();
pane.file_list_removal();
assert!(pane.edits().is_empty(), "the `+ new` row holds no element");
pane.list_mut().expect("open").move_to_first();
pane.file_list_reorder(-1);
assert!(pane.edits().is_empty(), "the first element cannot move up");
}
#[test]
fn an_integer_array_travels_as_numbers() {
let mut pane = ConfigPane::sheep(web_with_args(&[]));
pane.move_to_key("stop_exit_codes");
pane.open_list();
assert_eq!(pane.list().expect("open").elements(), ["0", "143"]);
pane.list_mut().expect("open").move_to(0);
pane.file_list_element("2".into());
assert_eq!(
filed(&pane, "stop_exit_codes"),
Some(serde_json::json!([2, 143]))
);
}
#[test]
fn a_dogs_array_field_has_no_widget_here() {
let schema = serde_json::json!({
"properties": { "sinks": { "type": "array", "items": { "type": "string" } } }
});
let pane = ConfigPane::dog("bark".into(), None, schema, String::new());
assert!(!pane.fields().by_key("sinks").expect("declared").editable);
assert_eq!(pane.lock("sinks"), Some(Lock::NoWidget));
}
}