use super::*;
impl App {
pub(super) fn handle_board_navigation(&mut self, key: KeyEvent) -> Result<()> {
match key.code {
KeyCode::Esc => self.tree_mode = false,
KeyCode::Char('/') => {
self.search_mode = true;
self.search_query.clear();
}
KeyCode::Char('n') => self.move_search(1),
KeyCode::Char('N') => self.move_search(-1),
KeyCode::Char('r') => {
self.reload_tasks()?;
self.last_task_refresh = Instant::now();
}
KeyCode::Down | KeyCode::Char('j') => self.move_visible(1),
KeyCode::Up | KeyCode::Char('k') => self.move_visible(-1),
KeyCode::Left | KeyCode::Char('h') => self.jump_group(-1),
KeyCode::Right | KeyCode::Char('l') => self.jump_group(1),
KeyCode::Char(' ') => self.toggle_selected_group(),
KeyCode::Home | KeyCode::Char('g') => self.select_first_row(),
KeyCode::End | KeyCode::Char('G') => self.select_last_row(),
KeyCode::Enter => self.open_selected_row()?,
_ => {}
}
Ok(())
}
pub(super) fn handle_search_key(&mut self, key: KeyEvent) -> Result<()> {
match key.code {
KeyCode::Esc => self.search_mode = false,
KeyCode::Char('n') => self.move_search(1),
KeyCode::Char('N') => self.move_search(-1),
KeyCode::Enter => {
self.search_mode = false;
self.move_search(1);
}
KeyCode::Backspace => {
self.search_query.pop();
}
KeyCode::Char(character) => self.search_query.push(character),
_ => {}
}
Ok(())
}
fn visible_nodes(&self) -> Vec<crate::tui::tree_data::TreeNode> {
crate::tui::tree_data::build_task_tree_with_state(
&self.tasks,
&self.wg_creators,
&self.collapsed_projects,
)
}
fn move_visible(&mut self, direction: i8) {
let nodes = self.visible_nodes();
if nodes.is_empty() {
return;
}
let current = self.tree_selected.min(nodes.len() - 1);
let next = if direction > 0 {
current.saturating_add(1).min(nodes.len() - 1)
} else {
current.saturating_sub(1)
};
self.select_row(next, &nodes);
}
fn jump_group(&mut self, direction: i8) {
let nodes = self.visible_nodes();
let headers: Vec<usize> = nodes
.iter()
.enumerate()
.filter(|(_, node)| node.is_group_header)
.map(|(index, _)| index)
.collect();
if headers.is_empty() {
return;
}
let current_group = nodes
.get(self.tree_selected)
.and_then(|node| node.project_id.as_ref());
let current = headers
.iter()
.position(|index| nodes[*index].project_id.as_ref() == current_group)
.unwrap_or(0);
let next = if direction > 0 {
(current + 1) % headers.len()
} else if current == 0 {
headers.len() - 1
} else {
current - 1
};
self.select_row(headers[next], &nodes);
}
fn toggle_selected_group(&mut self) {
let nodes = self.visible_nodes();
let Some(node) = nodes.get(self.tree_selected) else {
return;
};
let project_id = node.project_id.clone();
if !self.collapsed_projects.insert(project_id.clone()) {
self.collapsed_projects.remove(&project_id);
}
let nodes = self.visible_nodes();
self.tree_selected = nodes
.iter()
.position(|candidate| candidate.is_group_header && candidate.project_id == project_id)
.unwrap_or(0);
}
fn select_first_row(&mut self) {
let nodes = self.visible_nodes();
if !nodes.is_empty() {
self.select_row(0, &nodes);
}
}
pub(super) fn select_first_task_if_header(&mut self) {
let nodes = self.visible_nodes();
if nodes
.get(self.tree_selected)
.is_some_and(|node| node.is_group_header)
{
self.move_visible(1);
}
}
fn select_last_row(&mut self) {
let nodes = self.visible_nodes();
if let Some(index) = nodes.len().checked_sub(1) {
self.select_row(index, &nodes);
}
}
fn move_search(&mut self, direction: i8) {
if self.search_query.trim().is_empty() {
return;
}
let matches: Vec<usize> = self
.tasks
.iter()
.enumerate()
.filter(|(_, task)| self.matches_search(task))
.map(|(index, _)| index)
.collect();
if matches.is_empty() {
return;
}
let current_task = self.selected_task().map(|task| task.id.as_str());
let current = matches
.iter()
.position(|index| Some(self.tasks[*index].id.as_str()) == current_task);
let next = if current.is_none() {
0
} else if direction > 0 {
(current.unwrap_or(0) + 1) % matches.len()
} else if current == Some(0) {
matches.len() - 1
} else {
current.unwrap_or(0) - 1
};
let project_id = self.tasks[matches[next]].project_id.clone();
self.collapsed_projects.remove(&project_id);
let nodes = self.visible_nodes();
if let Some(node_index) = nodes
.iter()
.position(|node| !node.is_group_header && node.task.id == self.tasks[matches[next]].id)
{
self.select_row(node_index, &nodes);
}
}
fn matches_search(&self, task: &Task) -> bool {
let query = self.search_query.to_lowercase();
task.id.as_str().to_lowercase().contains(&query)
|| task.prompt.to_lowercase().contains(&query)
|| crate::project::project_display(task.project_id.as_deref())
.to_lowercase()
.contains(&query)
}
fn select_row(&mut self, index: usize, nodes: &[crate::tui::tree_data::TreeNode]) {
self.tree_selected = index;
if let Some(node) = nodes.get(index)
&& !node.is_group_header
&& let Some(task_index) = self.tasks.iter().position(|task| task.id == node.task.id)
{
self.selected = task_index;
}
}
fn open_selected_row(&mut self) -> Result<()> {
let nodes = self.visible_nodes();
let Some(node) = nodes.get(self.tree_selected) else {
return Ok(());
};
let task_id = if node.is_group_header {
self.tasks
.iter()
.find(|task| task.project_id == node.project_id)
.map(|task| task.id.clone())
} else {
Some(node.task.id.clone())
};
if let Some(task_id) = task_id
&& let Some(index) = self.tasks.iter().position(|task| task.id == task_id)
{
self.selected = index;
self.enter_detail_mode()?;
}
Ok(())
}
}