use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use tokio::sync::{mpsc::UnboundedSender, Semaphore};
use crate::app::{ActivePane, App, InputMode, InspectorView};
use crate::gitlab::{spawn_mr_fetch, CachedMrData, FetchContext};
use crate::models::{AppEvent, MrStatus, TrackedMr};
use crate::storage::save_state_async;
pub fn handle_mouse_event(mouse: MouseEvent, term_width: u16, app: &mut App) {
let inspector_start_col = term_width * 65 / 100;
match mouse.kind {
MouseEventKind::Moved | MouseEventKind::Drag(_) => {
if mouse.column >= inspector_start_col {
app.active_pane = ActivePane::Inspector;
} else {
app.active_pane = ActivePane::Dashboard;
}
}
MouseEventKind::ScrollDown => {
if mouse.column >= inspector_start_col {
app.inspector_scroll_down(3);
} else {
app.next_row();
}
}
MouseEventKind::ScrollUp => {
if mouse.column >= inspector_start_col {
app.inspector_scroll_up(3);
} else {
app.prev_row();
}
}
_ => {}
}
}
pub async fn handle_key_event(
key: KeyEvent,
app: &mut App,
api_semaphore: &Arc<Semaphore>,
tx: &UnboundedSender<AppEvent>,
last_known_branches: &mut HashMap<String, HashSet<String>>,
) -> bool {
match app.input_mode {
InputMode::Editing => match key.code {
KeyCode::Esc => {
app.input.clear();
app.input_mode = InputMode::Normal;
}
KeyCode::Enter => {
handle_enter(app, api_semaphore, tx, last_known_branches).await;
app.input_mode = InputMode::Normal;
}
KeyCode::Backspace => {
app.input.pop();
}
KeyCode::Char(c) => app.input.push(c),
_ => {}
},
InputMode::Normal => match key.code {
KeyCode::Esc => return true,
KeyCode::Char('/') | KeyCode::Char('i') => {
app.input_mode = InputMode::Editing;
}
KeyCode::Tab => {
app.active_pane = app.active_pane.next();
}
KeyCode::Down | KeyCode::Char('j') => match app.active_pane {
ActivePane::Inspector => app.inspector_scroll_down(1),
ActivePane::Dashboard => app.next_row(),
},
KeyCode::Up | KeyCode::Char('k') => match app.active_pane {
ActivePane::Inspector => app.inspector_scroll_up(1),
ActivePane::Dashboard => app.prev_row(),
},
KeyCode::Char('o') | KeyCode::Char('O') => {
if let Some(selected) = app.table_state.selected() {
if let Some(mr) = app.mrs.get(selected) {
let target_url = if !mr.web_url.is_empty() {
mr.web_url.clone()
} else {
format!(
"{}/projects/{}/merge_requests/{}",
app.base_url, app.project_id, mr.id
)
};
let _ = open::that(target_url);
}
}
}
KeyCode::Char('r') | KeyCode::Char('R') => {
app.time_left = app.refresh_interval_secs;
let ctx = build_fetch_context(app);
for mr in &mut app.mrs {
mr.status = MrStatus::Loading;
let cached = cached_from_mr(mr);
spawn_mr_fetch(
ctx.clone(),
mr.id.clone(),
cached,
api_semaphore.clone(),
tx.clone(),
);
}
}
KeyCode::Char('p') | KeyCode::Char('P') => {
app.inspector_view = match app.inspector_view {
InspectorView::MrInfo => InspectorView::Pipelines,
InspectorView::Pipelines => InspectorView::MrInfo,
};
app.reset_inspector_scroll();
}
KeyCode::Char('s') => app.cycle_sort_column(),
KeyCode::Char('S') => app.toggle_sort_order(),
KeyCode::Delete | KeyCode::Char('x') => {
if let Some(selected) = app.table_state.selected() {
if selected < app.mrs.len() {
app.mrs.remove(selected);
if app.mrs.is_empty() {
app.table_state.select(None);
} else if selected >= app.mrs.len() {
app.table_state.select(Some(app.mrs.len() - 1));
}
save_state_async(&app.mrs, &app.branches, last_known_branches).await;
}
}
}
_ => {}
},
}
false
}
async fn handle_enter(
app: &mut App,
api_semaphore: &Arc<Semaphore>,
tx: &UnboundedSender<AppEvent>,
last_known_branches: &mut HashMap<String, HashSet<String>>,
) {
let value = app.input.trim().to_string();
if value.is_empty() {
return;
}
if value.starts_with('-') {
let to_remove = value.trim_start_matches('-').to_string();
if to_remove.chars().all(|c| c.is_numeric()) {
app.mrs.retain(|m| m.id != to_remove);
} else {
app.branches.retain(|b| b != &to_remove);
}
save_state_async(&app.mrs, &app.branches, last_known_branches).await;
if app.mrs.is_empty() {
app.table_state.select(None);
}
} else if value.chars().all(|c| c.is_numeric()) {
if !app.mrs.iter().any(|m| m.id == value) {
app.mrs.push(TrackedMr {
id: value.clone(),
title: "Loading...".to_string(),
status: MrStatus::Loading,
state: crate::models::GitlabMrState::Opened,
mergeability: crate::models::MergeabilityStatus::Unknown,
sha: None,
description: String::new(),
author: "Loading".to_string(),
assignee: "Loading".to_string(),
milestone: "Loading".to_string(),
web_url: String::new(),
labels: vec![],
updated_at: None,
pipelines: vec![],
});
app.table_state.select(Some(app.mrs.len() - 1));
save_state_async(&app.mrs, &app.branches, last_known_branches).await;
let ctx = build_fetch_context(app);
spawn_mr_fetch(
ctx,
value,
CachedMrData::default(),
api_semaphore.clone(),
tx.clone(),
);
}
} else {
if !app.branches.contains(&value) {
app.branches.push(value.clone());
save_state_async(&app.mrs, &app.branches, last_known_branches).await;
let ctx = build_fetch_context(app);
for mr in &mut app.mrs {
if mr.status != MrStatus::Loading {
mr.status = MrStatus::Loading;
let cached = cached_from_mr(mr);
spawn_mr_fetch(
ctx.clone(),
mr.id.clone(),
cached,
api_semaphore.clone(),
tx.clone(),
);
}
}
}
}
app.input.clear();
}
pub fn handle_key_event_demo(key: KeyEvent, app: &mut App) -> bool {
match key.code {
KeyCode::Esc => return true,
KeyCode::Tab => {
app.active_pane = app.active_pane.next();
}
KeyCode::Down | KeyCode::Char('j') => match app.active_pane {
ActivePane::Inspector => app.inspector_scroll_down(1),
ActivePane::Dashboard => app.next_row(),
},
KeyCode::Up | KeyCode::Char('k') => match app.active_pane {
ActivePane::Inspector => app.inspector_scroll_up(1),
ActivePane::Dashboard => app.prev_row(),
},
KeyCode::Char('o') | KeyCode::Char('O') => {
if let Some(selected) = app.table_state.selected() {
if let Some(mr) = app.mrs.get(selected) {
let target_url = if !mr.web_url.is_empty() {
mr.web_url.clone()
} else {
format!(
"{}/projects/{}/merge_requests/{}",
app.base_url, app.project_id, mr.id
)
};
let _ = open::that(target_url);
}
}
}
KeyCode::Char('r') | KeyCode::Char('R') => {
app.time_left = app.refresh_interval_secs;
}
KeyCode::Char('s') => app.cycle_sort_column(),
KeyCode::Char('S') => app.toggle_sort_order(),
KeyCode::Char('p') | KeyCode::Char('P') => {
app.inspector_view = match app.inspector_view {
InspectorView::MrInfo => InspectorView::Pipelines,
InspectorView::Pipelines => InspectorView::MrInfo,
};
app.reset_inspector_scroll();
}
_ => {}
}
false
}
fn build_fetch_context(app: &App) -> FetchContext {
FetchContext {
base_url: app.base_url.clone(),
token: app.token.clone(),
project_id: app.project_id.clone(),
branches: app.branches.clone(),
}
}
fn cached_from_mr(mr: &TrackedMr) -> CachedMrData {
CachedMrData {
title: Some(mr.title.clone()),
sha: mr.sha.clone(),
description: Some(mr.description.clone()),
author: Some(mr.author.clone()),
assignee: Some(mr.assignee.clone()),
milestone: Some(mr.milestone.clone()),
web_url: Some(mr.web_url.clone()),
labels: Some(mr.labels.clone()),
updated_at: mr.updated_at.clone(),
pipelines: mr.pipelines.clone(),
}
}