use std::path::PathBuf;
use serde_json::{Map, Value};
use shep_core::config::DogTable;
use shep_core::protocol::SheepConfigView;
use super::{ConfigPane, PaneRow, PaneTarget};
#[derive(Clone, PartialEq, Eq)]
pub struct SheepDogEntry {
pub name: String,
pub adopted_path: Option<PathBuf>,
pub schema: Option<Value>,
}
impl core::fmt::Debug for SheepDogEntry {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"SheepDogEntry {{ name: {:?}, adopted_path: {:?}, schema: {} }}",
self.name,
self.adopted_path,
self.schema.as_ref().map_or("None", |_| "Some(..)")
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListedDog {
pub entry: SheepDogEntry,
pub asked: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DogTableState {
Set,
Unset,
ReadOnly,
NoSchema,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Asked {
No,
Waiting,
Yes,
}
#[derive(Clone)]
pub struct DogRow {
entry: SheepDogEntry,
asked: Asked,
keys: Option<Vec<String>>,
table_only: bool,
open: bool,
table: Option<Box<ConfigPane>>,
}
impl core::fmt::Debug for DogRow {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"DogRow {{ name: {:?}, state: {:?}, open: {}, keys: {:?} }}",
self.entry.name,
self.state(),
self.open,
self.keys()
)
}
}
impl DogRow {
#[must_use]
pub fn name(&self) -> &str {
&self.entry.name
}
#[must_use]
pub fn state(&self) -> DogTableState {
match (self.asked, self.entry.schema.is_some(), self.keys.is_some()) {
(Asked::Yes, false, true) => DogTableState::ReadOnly,
(Asked::Yes, false, false) => DogTableState::NoSchema,
(_, _, true) => DogTableState::Set,
(_, _, false) => DogTableState::Unset,
}
}
#[must_use]
pub fn keys(&self) -> &[String] {
self.keys.as_deref().unwrap_or_default()
}
#[must_use]
pub fn is_open(&self) -> bool {
self.open
}
#[must_use]
pub fn has_table(&self) -> bool {
self.keys.is_some()
}
#[must_use]
pub fn needs_probe(&self) -> bool {
self.asked == Asked::No
}
#[must_use]
pub fn is_waiting(&self) -> bool {
self.asked == Asked::Waiting
}
#[must_use]
pub fn table(&self) -> Option<&ConfigPane> {
self.table.as_deref()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
enum Listing {
#[default]
Unasked,
Waiting,
Listed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::lookout) struct DogAnchor {
name: String,
inside: bool,
}
#[derive(Clone, Default)]
pub struct DogsTab {
listing: Listing,
rows: Vec<DogRow>,
armed: Option<String>,
}
impl core::fmt::Debug for DogsTab {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"DogsTab {{ listing: {:?}, rows: {:?}, armed: {:?} }}",
self.listing, self.rows, self.armed
)
}
}
impl DogsTab {
#[must_use]
pub fn rows(&self) -> &[DogRow] {
&self.rows
}
#[must_use]
pub fn is_listed(&self) -> bool {
self.listing == Listing::Listed
}
#[must_use]
pub fn armed(&self) -> Option<&str> {
self.armed.as_deref()
}
fn layout(&self) -> Vec<PaneRow> {
let mut rows = Vec::new();
for (dog, row) in self.rows.iter().enumerate() {
rows.push(PaneRow::Dog(dog));
if let (true, Some(table)) = (row.open, row.table.as_deref()) {
rows.extend((0..table.rows().len()).map(|at| PaneRow::Under { dog, at }));
}
}
rows
}
#[must_use]
pub fn needs_listing(&self) -> bool {
self.listing == Listing::Unasked
}
pub(in crate::lookout) fn start_listing(&mut self) {
self.listing = Listing::Waiting;
}
pub(in crate::lookout) fn disarm(&mut self) -> bool {
self.armed.take().is_some()
}
pub(in crate::lookout) fn arm_removal(&mut self, dog: usize) -> bool {
let Some(row) = self.rows.get(dog).filter(|row| row.has_table()) else {
return false;
};
self.armed = Some(row.name().to_owned());
true
}
pub(in crate::lookout) fn edited(&self) -> Vec<usize> {
(0..self.rows.len())
.filter(|&dog| {
self.rows[dog]
.table
.as_deref()
.is_some_and(|table| !table.edits().is_empty())
})
.collect()
}
pub(in crate::lookout) fn take_table(&mut self, dog: usize) -> Option<Box<ConfigPane>> {
self.rows.get_mut(dog)?.table.take()
}
pub(in crate::lookout) fn seat_table(&mut self, dog: usize, table: Box<ConfigPane>) {
if let Some(row) = self.rows.get_mut(dog) {
row.table = Some(table);
}
}
pub(super) fn tables(&self) -> impl Iterator<Item = &ConfigPane> {
self.rows.iter().filter_map(|row| row.table.as_deref())
}
fn refresh(&mut self, tables: &Map<String, Value>) {
self.armed = None;
for row in &mut self.rows {
let table = tables.get(&row.entry.name);
row.keys = table.map(key_names);
if let Some(pane) = row.table.as_deref_mut() {
pane.adopt_table(
table
.and_then(Value::as_object)
.cloned()
.unwrap_or_default(),
);
}
}
self.rows
.retain(|row| !(row.table_only && row.keys.is_none()));
self.add_table_only(tables);
}
fn add_table_only(&mut self, tables: &Map<String, Value>) {
for (name, table) in tables {
if self.rows.iter().any(|row| &row.entry.name == name) {
continue;
}
self.rows.push(DogRow {
entry: SheepDogEntry {
name: name.clone(),
adopted_path: None,
schema: None,
},
asked: Asked::Yes,
keys: Some(key_names(table)),
table_only: true,
open: false,
table: None,
});
}
self.rows.sort_by(|a, b| a.entry.name.cmp(&b.entry.name));
}
}
fn key_names(table: &Value) -> Vec<String> {
table
.as_object()
.map(|table| table.keys().cloned().collect())
.unwrap_or_default()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::lookout) enum DogToggle {
Closed,
Opened,
Ask {
dog: String,
adopted_path: Option<PathBuf>,
},
Waiting,
NoSchema(String),
}
impl ConfigPane {
#[must_use]
pub fn dogs_tab(&self) -> &DogsTab {
&self.dogs_tab
}
pub(in crate::lookout) fn dogs_tab_mut(&mut self) -> &mut DogsTab {
&mut self.dogs_tab
}
#[must_use]
pub fn shows_dogs(&self) -> bool {
matches!(self.target, PaneTarget::Sheep { .. }) && self.group() == "dogs"
}
pub(super) fn dog_rows(&self) -> Vec<PaneRow> {
self.dogs_tab.layout()
}
pub(in crate::lookout) fn list_dogs(&mut self, listed: Vec<ListedDog>) {
let tables = self.sheep_tables();
let mut rows: Vec<DogRow> = listed
.into_iter()
.map(|dog| DogRow {
keys: tables.get(&dog.entry.name).map(key_names),
asked: if dog.asked { Asked::Yes } else { Asked::No },
entry: dog.entry,
table_only: false,
open: false,
table: None,
})
.collect();
rows.retain(|row| row.asked != Asked::Yes || row.entry.schema.is_some() || row.has_table());
let anchor = self.dog_anchor();
self.dogs_tab.rows = rows;
self.dogs_tab.listing = Listing::Listed;
self.dogs_tab.add_table_only(&tables);
self.clamp_view();
if let Some(anchor) = anchor {
self.restore_dog_anchor(&anchor);
}
}
pub(in crate::lookout) fn toggle_dog(&mut self, dog: usize) -> DogToggle {
let Some(row) = self.dogs_tab.rows.get_mut(dog) else {
return DogToggle::Closed;
};
if row.open {
row.open = false;
return DogToggle::Closed;
}
match row.asked {
Asked::No => {
row.asked = Asked::Waiting;
row.open = true;
DogToggle::Ask {
dog: row.entry.name.clone(),
adopted_path: row.entry.adopted_path.clone(),
}
}
Asked::Waiting => {
row.open = true;
DogToggle::Waiting
}
Asked::Yes => self.open_dog(dog),
}
}
fn open_dog(&mut self, dog: usize) -> DogToggle {
let sheep = self.target.name().to_owned();
let Some(row) = self.dogs_tab.rows.get(dog) else {
return DogToggle::Closed;
};
let name = row.entry.name.clone();
let Some(schema) = row.entry.schema.clone() else {
return DogToggle::NoSchema(name);
};
if row.table.is_none() {
let pane = Self::sheep_dog(sheep, name, &schema, self.sheep_table(&row.entry.name));
self.dogs_tab.rows[dog].table = Some(Box::new(pane));
}
self.dogs_tab.rows[dog].open = true;
DogToggle::Opened
}
pub(in crate::lookout) fn answer_dog(&mut self, entry: SheepDogEntry) -> Option<DogToggle> {
let dog = self
.dogs_tab
.rows
.iter()
.position(|row| row.entry.name == entry.name)?;
let row = &mut self.dogs_tab.rows[dog];
row.entry = entry;
row.asked = Asked::Yes;
if !row.open {
return Some(DogToggle::Closed);
}
row.open = false;
let anchor = self.dog_anchor();
let toggled = self.open_dog(dog);
if let Some(anchor) = anchor {
self.restore_dog_anchor(&anchor);
}
Some(toggled)
}
pub(in crate::lookout) fn adopt_dogs_tab(&mut self, previous: DogsTab) {
*self.dogs_tab = previous;
let tables = self.sheep_tables();
self.dogs_tab.refresh(&tables);
self.clamp_view();
}
fn clamp_view(&mut self) {
let len = self.rows().len();
self.view.clamp(len);
}
#[must_use]
pub(in crate::lookout) fn focused_dog(&self) -> Option<usize> {
match self.cursor()? {
PaneRow::Under { dog, .. } => Some(dog),
_ => None,
}
}
#[must_use]
pub fn focus(&self) -> &Self {
self.focused_dog()
.and_then(|dog| self.dogs_tab.rows.get(dog))
.and_then(|row| row.table.as_deref())
.unwrap_or(self)
}
#[must_use]
pub(in crate::lookout) fn owns_text(&self) -> bool {
let pane = self.focus();
pane.typing().is_some() || pane.env_typing().is_some()
}
#[must_use]
pub(in crate::lookout) fn moved_tables(&self, view: &SheepConfigView) -> Vec<String> {
self.dogs_tab
.edited()
.into_iter()
.filter_map(|dog| {
let row = self.dogs_tab.rows.get(dog)?;
let fresh = view.config.dogs.get(row.name()).map(DogTable::as_map);
(!row.table.as_deref()?.holds_table(fresh)).then(|| row.name().to_owned())
})
.collect()
}
pub(in crate::lookout) fn push_focus(&mut self) {
let Some(PaneRow::Under { dog, at }) = self.cursor() else {
return;
};
if let Some(table) = self
.dogs_tab
.rows
.get_mut(dog)
.and_then(|row| row.table.as_deref_mut())
{
let len = table.rows().len();
table.view.move_to(at, len);
}
}
pub(in crate::lookout) fn pull_focus(&mut self, dog: usize) {
let Some(row) = self.dogs_tab.rows.get(dog) else {
return;
};
let Some(table) = row.table.as_deref() else {
return;
};
let within = table.view.cursor();
let Some(head) = self.rows().iter().position(|r| *r == PaneRow::Dog(dog)) else {
return;
};
let len = self.rows().len();
self.view.move_to(head + 1 + within, len);
}
#[must_use]
pub(in crate::lookout) fn dog_anchor(&self) -> Option<DogAnchor> {
let (dog, inside) = match self.cursor()? {
PaneRow::Dog(dog) => (dog, false),
PaneRow::Under { dog, .. } => (dog, true),
_ => return None,
};
Some(DogAnchor {
name: self.dogs_tab.rows.get(dog)?.name().to_owned(),
inside,
})
}
pub(in crate::lookout) fn restore_dog_anchor(&mut self, anchor: &DogAnchor) {
let Some(dog) = self
.dogs_tab
.rows
.iter()
.position(|row| row.name() == anchor.name)
else {
return;
};
let rows = self.rows();
if !rows.contains(&PaneRow::Dog(dog)) {
return;
}
let row = &mut self.dogs_tab.rows[dog];
if anchor.inside
&& row.open
&& let Some(table) = row.table.as_deref_mut()
{
let len = table.rows().len();
table.view.clamp(len);
if len > 0 {
self.pull_focus(dog);
return;
}
}
if let Some(at) = rows.iter().position(|row| *row == PaneRow::Dog(dog)) {
self.view.move_to(at, rows.len());
}
}
#[must_use]
pub(in crate::lookout) fn sheep_table(&self, dog: &str) -> Map<String, Value> {
self.values
.get("dogs")
.and_then(|dogs| dogs.get(dog))
.and_then(Value::as_object)
.cloned()
.unwrap_or_default()
}
fn sheep_tables(&self) -> Map<String, Value> {
self.values
.get("dogs")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default()
}
}
pub(crate) fn dog_names(dogs: Option<&Value>) -> String {
let names: Vec<&str> = dogs
.and_then(Value::as_object)
.map(|tables| tables.keys().map(String::as_str).collect())
.unwrap_or_default();
if names.is_empty() {
"none".to_owned()
} else {
names.join(", ")
}
}
#[cfg(test)]
#[path = "dogs_tests.rs"]
mod tests;