use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use parking_lot::Mutex;
use tokio::sync::mpsc::UnboundedSender;
use super::{FormField, IssueActionRequest, IssueFormState, IssuesPanelMode};
use crate::tui_vt::main_loop::RenderState;
pub(crate) fn handle_issues_panel_key(
state: &Arc<Mutex<RenderState>>,
issue_action_tx: &UnboundedSender<IssueActionRequest>,
key: KeyEvent,
) -> bool {
let code = key.code;
let mut s = state.lock();
let Some(panel) = s.issues_panel.as_mut() else {
return false;
};
match &mut panel.mode {
IssuesPanelMode::List => match code {
KeyCode::Char('j') | KeyCode::Down => {
if !panel.rows.is_empty() {
panel.selected = (panel.selected + 1).min(panel.rows.len() - 1);
}
true
}
KeyCode::Char('k') | KeyCode::Up => {
panel.selected = panel.selected.saturating_sub(1);
true
}
KeyCode::Char('f') => {
panel.status_filter = if panel.status_filter.is_some() {
None
} else {
Some(oxicode_sdk::Status::Open)
};
let store = s.issue_store.clone();
if let (Some(store), Some(panel)) = (store, s.issues_panel.as_mut()) {
panel.refresh(&store, &store.issues_dir());
}
true
}
KeyCode::Char('/') => {
panel.mode = IssuesPanelMode::FilterInput(String::new());
true
}
KeyCode::Enter => {
if let Some(row) = panel.rows.get(panel.selected) {
let id = row.id;
let store = s.issue_store.clone();
if let Some(store) = store
&& let Ok((issue, _hash)) = store.read(id)
&& let Some(panel) = s.issues_panel.as_mut()
{
panel.detail_body_cache = Some(issue.body);
panel.mode = IssuesPanelMode::Detail { id, scroll: 0 };
}
}
true
}
KeyCode::Esc => {
s.issues_panel = None;
true
}
KeyCode::Char('c') if !panel.pending => {
if let Some(row) = panel.rows.get(panel.selected) {
let id = row.id;
s.confirmation = Some(crate::tui_vt::main_loop::ModalConfirmation {
title: "Close issue".into(),
message: format!(" y \u{2014} close #{id} n / x \u{2014} cancel"),
action: crate::tui_vt::main_loop::ConfirmationAction::CloseIssue(id),
});
}
true
}
KeyCode::Char('c') => true,
KeyCode::Char('r') if !panel.pending => {
if let Some(row) = panel.rows.get(panel.selected) {
let id = row.id;
let hash = s
.issue_store
.as_ref()
.and_then(|store| store.read(id).ok())
.map(|(_, h)| h);
let _ = issue_action_tx.send(IssueActionRequest::Reopen { id, hash });
if let Some(panel) = s.issues_panel.as_mut() {
panel.pending = true;
}
}
true
}
KeyCode::Char('r') => true,
KeyCode::Char('n') => {
panel.mode = IssuesPanelMode::Form(Box::default());
true
}
KeyCode::Char('e') => {
if let Some(row) = panel.rows.get(panel.selected) {
let id = row.id;
start_edit(&mut s, id);
}
true
}
_ => true,
},
IssuesPanelMode::FilterInput(buf) => match code {
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
buf.clear();
true
}
KeyCode::Char(c) => {
buf.push(c);
true
}
KeyCode::Backspace => {
buf.pop();
true
}
KeyCode::Enter => {
let buf = buf.clone();
let extra = super::parse_issue_filter(&buf, panel.status_filter);
panel.extra_filter = extra;
panel.mode = IssuesPanelMode::List;
let store = s.issue_store.clone();
if let (Some(store), Some(panel)) = (store, s.issues_panel.as_mut()) {
panel.refresh(&store, &store.issues_dir());
}
true
}
KeyCode::Esc => {
panel.mode = IssuesPanelMode::List;
true
}
_ => true, },
IssuesPanelMode::Detail { id, scroll } => {
let id = *id;
match code {
KeyCode::Esc => {
panel.mode = IssuesPanelMode::List;
true
}
KeyCode::Char('j') | KeyCode::Down | KeyCode::PageDown => {
let step = if matches!(code, KeyCode::PageDown) {
10usize
} else {
1usize
};
*scroll = scroll.saturating_add(step);
true
}
KeyCode::Char('k') | KeyCode::Up | KeyCode::PageUp => {
let step = if matches!(code, KeyCode::PageUp) {
10usize
} else {
1usize
};
*scroll = scroll.saturating_sub(step);
true
}
KeyCode::Char('c') if !panel.pending => {
s.confirmation = Some(crate::tui_vt::main_loop::ModalConfirmation {
title: "Close issue".into(),
message: format!(" y \u{2014} close #{id} n / x \u{2014} cancel"),
action: crate::tui_vt::main_loop::ConfirmationAction::CloseIssue(id),
});
true
}
KeyCode::Char('c') => true,
KeyCode::Char('r') if !panel.pending => {
let hash = s
.issue_store
.as_ref()
.and_then(|store| store.read(id).ok())
.map(|(_, h)| h);
let _ = issue_action_tx.send(IssueActionRequest::Reopen { id, hash });
if let Some(panel) = s.issues_panel.as_mut() {
panel.pending = true;
}
true
}
KeyCode::Char('r') => true,
KeyCode::Char('e') => {
start_edit(&mut s, id);
true
}
_ => true,
}
}
IssuesPanelMode::Form(form) => match code {
KeyCode::Esc => {
panel.mode = IssuesPanelMode::List;
true
}
KeyCode::BackTab => {
form.focus = match form.focus {
FormField::Title => FormField::Body,
FormField::Priority => FormField::Title,
FormField::Labels => FormField::Priority,
FormField::Body => FormField::Labels,
};
true
}
KeyCode::Tab => {
form.focus = match form.focus {
FormField::Title => FormField::Priority,
FormField::Priority => FormField::Labels,
FormField::Labels => FormField::Body,
FormField::Body => FormField::Title,
};
true
}
KeyCode::Left if form.focus == FormField::Priority => {
form.priority = super::cycle_priority(form.priority, false);
true
}
KeyCode::Right if form.focus == FormField::Priority => {
form.priority = super::cycle_priority(form.priority, true);
true
}
KeyCode::Char(c) if form.focus == FormField::Title => {
form.title.push(c);
true
}
KeyCode::Backspace if form.focus == FormField::Title => {
form.title.pop();
true
}
KeyCode::Char(c) if form.focus == FormField::Labels => {
form.labels_input.push(c);
true
}
KeyCode::Backspace if form.focus == FormField::Labels => {
form.labels_input.pop();
true
}
KeyCode::Enter if key.modifiers.contains(KeyModifiers::CONTROL) && !panel.pending => {
submit_form(&mut s, issue_action_tx);
true
}
KeyCode::Enter if key.modifiers.contains(KeyModifiers::CONTROL) => true,
_ if form.focus == FormField::Body => {
form.body.input(key);
true
}
_ => true,
},
}
}
fn start_edit(s: &mut parking_lot::MutexGuard<'_, crate::tui_vt::main_loop::RenderState>, id: u32) {
let Some(store) = s.issue_store.clone() else {
return;
};
let Ok((issue, hash)) = store.read(id) else {
return;
};
let Some(panel) = s.issues_panel.as_mut() else {
return;
};
if let Some(a) = &issue.meta.assigned_to
&& a.session != oxicode_sdk::liveness::TUI_OWNERSHIP_ID
&& oxicode_sdk::liveness::is_session_alive(&store.issues_dir(), &a.session)
{
panel.error = Some(format!(
"issue #{id} is being worked on by session {}",
a.session
));
return;
}
let mut form = IssueFormState {
editing_id: Some(id),
content_hash: Some(hash),
title: issue.meta.title.clone(),
priority: issue.meta.priority,
labels_input: issue.meta.labels.join(", "),
..IssueFormState::default()
};
form.body.set_text(&issue.body);
panel.mode = IssuesPanelMode::Form(Box::new(form));
}
fn submit_form(
s: &mut parking_lot::MutexGuard<'_, crate::tui_vt::main_loop::RenderState>,
issue_action_tx: &tokio::sync::mpsc::UnboundedSender<IssueActionRequest>,
) {
let (editing_id, content_hash, title, priority, labels, body) = {
let Some(panel) = s.issues_panel.as_mut() else {
return;
};
let IssuesPanelMode::Form(form) = &panel.mode else {
return;
};
(
form.editing_id,
form.content_hash.clone(),
form.title.clone(),
form.priority,
super::parse_labels(&form.labels_input),
form.body.text().to_string(),
)
};
match editing_id {
None => {
let Some(store) = s.issue_store.clone() else {
if let Some(panel) = s.issues_panel.as_mut() {
panel.error = Some("issue store unavailable".into());
}
return;
};
match store.create(
title,
body,
priority,
labels,
Some(oxicode_sdk::liveness::TUI_OWNERSHIP_ID),
) {
Ok(_issue) => {
if let Some(panel) = s.issues_panel.as_mut() {
panel.mode = IssuesPanelMode::List;
panel.error = None;
panel.refresh(&store, &store.issues_dir());
}
}
Err(e) => {
if let Some(panel) = s.issues_panel.as_mut() {
panel.error = Some(e.to_string());
}
}
}
}
Some(id) => {
let patch = oxicode_sdk::IssuePatch {
title: Some(title),
body: Some(body),
priority: Some(priority),
labels: Some(labels),
..Default::default()
};
let _ = issue_action_tx.send(IssueActionRequest::ApplyPatch {
id,
patch,
caller: Some(oxicode_sdk::liveness::TUI_OWNERSHIP_ID.to_string()),
hash: content_hash,
});
if let Some(panel) = s.issues_panel.as_mut() {
panel.pending = true;
panel.mode = IssuesPanelMode::List;
}
}
}
}