use crate::{
data::{Data, Page, Table},
model::{
article::{self, Article},
board::{self, Board},
session::ChatSession,
watch::Watch,
workspace::Workspace,
},
};
use bezel::gpui::Context;
use std::{
collections::HashMap,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
const DIR: &str = ".cydonia";
const PAGE: i64 = 200;
pub struct Project {
pub path: PathBuf,
pub sessions: Vec<ChatSession>,
pub active: Option<u64>,
pub boards: Vec<Board>,
pub board: Option<usize>,
pub articles: Vec<Article>,
pub article: Option<usize>,
pub data: Option<Data>,
pub tables: Vec<Table>,
pub table: Option<usize>,
pub page: Option<Page>,
pub expanded: bool,
pub archive_open: bool,
pub watch: Option<Watch>,
}
impl Project {
pub fn new(path: PathBuf) -> Self {
let mut this = Self {
boards: board::list(&path),
articles: article::list(&path),
data: Data::attach(&path),
path,
sessions: Vec::new(),
active: None,
board: None,
article: None,
tables: Vec::new(),
table: None,
page: None,
expanded: true,
archive_open: false,
watch: None,
};
this.reload_tables();
this
}
pub fn reload(&mut self, cx: &mut Context<Workspace>) -> bool {
if self.data.is_none() {
self.data = Data::attach(&self.path);
}
let articles = self.reload_articles(cx);
let boards = self.reload_boards();
let before = self.shape();
self.reload_tables();
articles || boards || before != self.shape()
}
fn shape(&self) -> (Vec<String>, Vec<String>) {
let keys = self.tables.iter().map(|table| table.key.clone()).collect();
let columns = self
.page
.as_ref()
.map(|page| page.columns.iter().map(|col| col.name.clone()).collect())
.unwrap_or_default();
(keys, columns)
}
fn reload_articles(&mut self, cx: &mut Context<Workspace>) -> bool {
let open = self.open_article().map(Path::to_path_buf);
let mut held: HashMap<PathBuf, Article> = self
.articles
.drain(..)
.map(|article| (article.path.clone(), article))
.collect();
let mut rebuilt = false;
self.articles = article::list(&self.path)
.into_iter()
.map(|fresh| match held.remove(&fresh.path) {
Some(mut article) => {
rebuilt |= article.adopt(&fresh, cx);
article
}
None => fresh,
})
.collect();
self.article = open.and_then(|path| self.articles.iter().position(|at| at.path == path));
rebuilt
}
fn open_article(&self) -> Option<&Path> {
let article = self.articles.get(self.article?)?;
Some(&article.path)
}
fn reload_boards(&mut self) -> bool {
let open = self
.board
.and_then(|ix| self.boards.get(ix))
.map(|board| board.path.clone());
let mut held: HashMap<PathBuf, Board> = self
.boards
.drain(..)
.map(|board| (board.path.clone(), board))
.collect();
let mut moved = false;
self.boards = board::list(&self.path)
.into_iter()
.map(|fresh| match held.remove(&fresh.path) {
Some(mut board) => {
moved |= board.adopt(fresh);
board
}
None => fresh,
})
.collect();
self.board = open.and_then(|path| self.boards.iter().position(|at| at.path == path));
moved
}
pub fn reload_tables(&mut self) {
let open = self
.table
.and_then(|ix| self.tables.get(ix))
.map(|table| table.key.clone());
self.tables = self
.data
.as_ref()
.and_then(|data| data.list().ok())
.unwrap_or_default();
self.table = match open.and_then(|key| self.position(&key)) {
found @ Some(_) => found,
None => self.table.filter(|ix| *ix < self.tables.len()),
};
self.reload_page();
}
fn position(&self, key: &str) -> Option<usize> {
self.tables.iter().position(|table| table.key == key)
}
pub fn reload_page(&mut self) {
let key = self
.table
.and_then(|ix| self.tables.get(ix))
.map(|table| table.key.clone());
self.page = match (key, self.data.as_ref()) {
(Some(key), Some(data)) => data.read(&key, None, false, PAGE, 0).ok(),
_ => None,
};
}
pub fn name(&self) -> String {
self.path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| self.path.display().to_string())
}
pub fn session(&self, id: u64) -> Option<&ChatSession> {
self.sessions.iter().find(|chat| chat.id == id)
}
pub fn session_mut(&mut self, id: u64) -> Option<&mut ChatSession> {
self.sessions.iter_mut().find(|chat| chat.id == id)
}
pub fn active_session(&self) -> Option<&ChatSession> {
self.active.and_then(|id| self.session(id))
}
}
pub fn stamp() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|since| since.as_millis())
.unwrap_or_default()
}
pub fn written(path: &Path) -> u128 {
std::fs::metadata(path)
.and_then(|meta| meta.modified())
.ok()
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map_or_else(stamp, |since| since.as_millis())
}
pub fn dir(project: &Path) -> PathBuf {
project.join(DIR)
}
pub fn init(project: &Path) -> std::io::Result<PathBuf> {
let dir = dir(project);
std::fs::create_dir_all(&dir)?;
let ignore = dir.join(".gitignore");
if !ignore.exists() {
std::fs::write(&ignore, "*\n")?;
}
Ok(dir)
}