use crate::model::ProjectIndex;
use ratatui::widgets::{ListState, TableState};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum View {
#[default]
Dashboard,
RfcList,
AdrList,
WorkList,
RfcDetail(usize),
AdrDetail(usize),
WorkDetail(usize),
ClauseDetail(usize, usize),
}
pub struct App {
pub index: ProjectIndex,
pub view: View,
pub selected: usize,
pub table_state: TableState,
pub clause_list_state: ListState,
pub scroll: u16,
pub content_height: u16,
pub filter_query: String,
cached_indices: Vec<usize>,
indices_dirty: bool,
pub filter_mode: bool,
pub show_help: bool,
pub should_quit: bool,
}
impl App {
pub fn new(mut index: ProjectIndex) -> Self {
index.rfcs.sort_by(|a, b| a.rfc.rfc_id.cmp(&b.rfc.rfc_id));
index.adrs.sort_by(|a, b| a.meta().id.cmp(&b.meta().id));
index
.work_items
.sort_by(|a, b| a.meta().id.cmp(&b.meta().id));
Self {
index,
view: View::Dashboard,
selected: 0,
table_state: TableState::default().with_selected(Some(0)),
clause_list_state: ListState::default().with_selected(Some(0)),
scroll: 0,
content_height: 0,
filter_query: String::new(),
cached_indices: Vec::new(),
indices_dirty: true,
filter_mode: false,
show_help: false,
should_quit: false,
}
}
pub fn list_total_len(&self) -> usize {
match self.view {
View::RfcList => self.index.rfcs.len(),
View::AdrList => self.index.adrs.len(),
View::WorkList => self.index.work_items.len(),
_ => 0,
}
}
fn invalidate_indices(&mut self) {
self.indices_dirty = true;
}
fn recompute_indices(&mut self) {
if !self.indices_dirty {
return;
}
let query = self.filter_query.trim().to_ascii_lowercase();
let has_query = !query.is_empty();
self.cached_indices = match self.view {
View::RfcList => self
.index
.rfcs
.iter()
.enumerate()
.filter_map(|(idx, rfc)| {
if !has_query {
return Some(idx);
}
let status = rfc.rfc.status.as_ref().to_ascii_lowercase();
let phase = rfc.rfc.phase.as_ref().to_ascii_lowercase();
let id = rfc.rfc.rfc_id.to_ascii_lowercase();
let title = rfc.rfc.title.to_ascii_lowercase();
if id.contains(&query)
|| title.contains(&query)
|| status.contains(&query)
|| phase.contains(&query)
{
Some(idx)
} else {
None
}
})
.collect(),
View::AdrList => self
.index
.adrs
.iter()
.enumerate()
.filter_map(|(idx, adr)| {
if !has_query {
return Some(idx);
}
let meta = adr.meta();
let status = meta.status.as_ref().to_ascii_lowercase();
let id = meta.id.to_ascii_lowercase();
let title = meta.title.to_ascii_lowercase();
if id.contains(&query) || title.contains(&query) || status.contains(&query) {
Some(idx)
} else {
None
}
})
.collect(),
View::WorkList => self
.index
.work_items
.iter()
.enumerate()
.filter_map(|(idx, item)| {
if !has_query {
return Some(idx);
}
let meta = item.meta();
let status = meta.status.as_ref().to_ascii_lowercase();
let id = meta.id.to_ascii_lowercase();
let title = meta.title.to_ascii_lowercase();
if id.contains(&query) || title.contains(&query) || status.contains(&query) {
Some(idx)
} else {
None
}
})
.collect(),
_ => Vec::new(),
};
self.indices_dirty = false;
}
pub fn list_indices(&mut self) -> Vec<usize> {
self.recompute_indices();
self.cached_indices.clone()
}
pub fn list_len(&mut self) -> usize {
self.recompute_indices();
self.cached_indices.len()
}
pub fn filter_active(&self) -> bool {
!self.filter_query.trim().is_empty()
}
pub fn enter_filter_mode(&mut self) {
self.filter_mode = true;
}
pub fn exit_filter_mode(&mut self) {
self.filter_mode = false;
}
pub fn clear_filter(&mut self) {
self.filter_query.clear();
self.invalidate_indices();
self.ensure_selection_in_bounds();
}
pub fn push_filter_char(&mut self, ch: char) {
self.filter_query.push(ch);
self.invalidate_indices();
self.ensure_selection_in_bounds();
}
pub fn pop_filter_char(&mut self) {
self.filter_query.pop();
self.invalidate_indices();
self.ensure_selection_in_bounds();
}
pub fn ensure_selection_in_bounds(&mut self) {
let len = self.list_len();
if len == 0 {
self.selected = 0;
self.table_state.select(None);
return;
}
if self.selected >= len {
self.selected = len - 1;
}
self.table_state.select(Some(self.selected));
}
pub fn select_prev(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
self.table_state.select(Some(self.selected));
}
pub fn select_next(&mut self) {
let len = self.list_len();
if len > 0 && self.selected < len - 1 {
self.selected += 1;
}
self.table_state.select(Some(self.selected));
}
pub fn select_top(&mut self) {
if self.list_len() == 0 {
self.table_state.select(None);
return;
}
self.selected = 0;
self.table_state.select(Some(self.selected));
}
pub fn select_bottom(&mut self) {
let len = self.list_len();
if len == 0 {
self.table_state.select(None);
return;
}
self.selected = len - 1;
self.table_state.select(Some(self.selected));
}
pub fn enter_detail(&mut self) {
let indices = self.list_indices();
if indices.is_empty() {
return;
}
if self.selected >= indices.len() {
self.ensure_selection_in_bounds();
return;
}
let real_idx = indices[self.selected];
self.view = match self.view {
View::RfcList => {
self.clause_list_state = ListState::default().with_selected(Some(0));
View::RfcDetail(real_idx)
}
View::AdrList => View::AdrDetail(real_idx),
View::WorkList => View::WorkDetail(real_idx),
_ => return,
};
self.scroll = 0;
}
pub fn go_back(&mut self) {
self.view = match self.view {
View::ClauseDetail(rfc_idx, _) => View::RfcDetail(rfc_idx),
View::RfcDetail(_) => View::RfcList,
View::AdrDetail(_) => View::AdrList,
View::WorkDetail(_) => View::WorkList,
View::RfcList | View::AdrList | View::WorkList => View::Dashboard,
View::Dashboard => {
self.should_quit = true;
View::Dashboard
}
};
self.scroll = 0;
if self.view == View::Dashboard {
self.filter_mode = false;
self.clear_filter();
}
}
pub fn go_to(&mut self, view: View) {
self.view = view;
self.selected = 0;
self.table_state = TableState::default().with_selected(Some(0));
self.scroll = 0;
self.invalidate_indices();
if matches!(self.view, View::RfcList | View::AdrList | View::WorkList) {
self.filter_mode = false;
self.clear_filter();
} else {
self.filter_mode = false;
}
}
pub fn scroll_down(&mut self) {
self.scroll = self.scroll.saturating_add(1);
}
pub fn scroll_up(&mut self) {
self.scroll = self.scroll.saturating_sub(1);
}
pub fn scroll_half_page_down(&mut self) {
let half = (self.content_height / 2).max(1);
self.scroll = self.scroll.saturating_add(half);
}
pub fn scroll_half_page_up(&mut self) {
let half = (self.content_height / 2).max(1);
self.scroll = self.scroll.saturating_sub(half);
}
pub fn scroll_page_down(&mut self) {
let page = self.content_height.max(1);
self.scroll = self.scroll.saturating_add(page);
}
pub fn scroll_page_up(&mut self) {
let page = self.content_height.max(1);
self.scroll = self.scroll.saturating_sub(page);
}
pub fn select_half_page_down(&mut self) {
let half = (self.content_height / 2).max(1) as usize;
let len = self.list_len();
if len > 0 {
self.selected = (self.selected + half).min(len - 1);
self.table_state.select(Some(self.selected));
}
}
pub fn select_half_page_up(&mut self) {
let half = (self.content_height / 2).max(1) as usize;
self.selected = self.selected.saturating_sub(half);
self.table_state.select(Some(self.selected));
}
pub fn clause_count(&self) -> usize {
match self.view {
View::RfcDetail(idx) => self
.index
.rfcs
.get(idx)
.map(|r| r.clauses.len())
.unwrap_or(0),
_ => 0,
}
}
pub fn clause_prev(&mut self) {
let selected = self.clause_list_state.selected().unwrap_or(0);
if selected > 0 {
self.clause_list_state.select(Some(selected - 1));
}
}
pub fn clause_next(&mut self) {
let len = self.clause_count();
let selected = self.clause_list_state.selected().unwrap_or(0);
if len > 0 && selected < len - 1 {
self.clause_list_state.select(Some(selected + 1));
}
}
pub fn enter_clause_detail(&mut self) {
if let View::RfcDetail(rfc_idx) = self.view {
let clause_idx = self.clause_list_state.selected().unwrap_or(0);
if self.clause_count() > 0 {
self.view = View::ClauseDetail(rfc_idx, clause_idx);
self.scroll = 0;
}
}
}
}