Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,25 +110,31 @@ async fn run_app(
terminal.draw(|f| ui::draw(f, app))?;

if event::poll(std::time::Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
// Only handle key press events (ignore release/repeat)
if key.kind != KeyEventKind::Press {
continue;
match event::read()? {
Event::Key(key) => {
// Only handle key press events (ignore release/repeat)
if key.kind != KeyEventKind::Press {
continue;
}

// Global quit: Ctrl+Q or Ctrl+D
if (key.code == KeyCode::Char('q') || key.code == KeyCode::Char('d'))
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
return Ok(());
}

// Handle input based on current focus
app.handle_input(key).await?;

if app.should_quit {
return Ok(());
}
}

// Global quit: Ctrl+Q or Ctrl+D
if (key.code == KeyCode::Char('q') || key.code == KeyCode::Char('d'))
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
return Ok(());
}

// Handle input based on current focus
app.handle_input(key).await?;

if app.should_quit {
return Ok(());
Event::Mouse(mouse) => {
app.handle_mouse(mouse.kind, mouse.column, mouse.row);
}
_ => {}
}
}

Expand Down
117 changes: 117 additions & 0 deletions src/ui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ use crate::db::{
use crate::editor::{HistoryEntry, QueryHistory, TextBuffer};
use crate::ui::Theme;

use ratatui::layout::Rect;

pub const SPINNER_FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];

#[derive(Debug, Clone, Copy, Default)]
pub struct LayoutRects {
pub sidebar: Rect,
pub editor: Rect,
pub results: Rect,
pub status_bar: Rect,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Focus {
Sidebar,
Expand Down Expand Up @@ -126,6 +136,9 @@ pub struct App {
// Help
pub show_help: bool,

// Layout rects (updated each draw for mouse support)
pub layout: LayoutRects,

// Export
pub export_selected: usize,

Expand Down Expand Up @@ -288,6 +301,7 @@ impl App {
loading_message: String::new(),
spinner_frame: 0,
show_help: false,
layout: LayoutRects::default(),
export_selected: 0,
pending_connection: None,
}
Expand Down Expand Up @@ -1448,6 +1462,109 @@ impl App {

Ok(())
}

pub fn handle_mouse(&mut self, kind: crossterm::event::MouseEventKind, column: u16, row: u16) {
use crossterm::event::MouseEventKind;

// Don't handle mouse when in dialog/help overlays
if self.connection_dialog.active || self.show_help {
return;
}

match kind {
MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
self.handle_mouse_click(column, row);
}
MouseEventKind::ScrollUp => {
self.handle_mouse_scroll(column, row, -3);
}
MouseEventKind::ScrollDown => {
self.handle_mouse_scroll(column, row, 3);
}
_ => {}
}
}

fn handle_mouse_click(&mut self, column: u16, row: u16) {
let layout = self.layout;

if rect_contains(layout.sidebar, column, row) {
self.focus = Focus::Sidebar;
// Map click to sidebar item (subtract border + tab area)
let local_y = row.saturating_sub(layout.sidebar.y + 3) as usize;
let idx = local_y + self.sidebar_scroll;
let max = match self.sidebar_tab {
SidebarTab::Databases => self.databases.len(),
SidebarTab::Tables => self.tables.len() + self.schemas.len(),
SidebarTab::History => self.query_history.entries().len(),
};
if idx < max {
self.sidebar_selected = idx;
}
} else if rect_contains(layout.editor, column, row) {
self.focus = Focus::Editor;
// Map click to editor position (inside border)
let inner_x = column.saturating_sub(layout.editor.x + 1) as usize;
let inner_y = row.saturating_sub(layout.editor.y + 1) as usize;
let target_y = inner_y + self.editor.scroll_offset;
if target_y < self.editor.lines.len() {
self.editor.cursor_y = target_y;
self.editor.cursor_x = inner_x.min(self.editor.lines[target_y].len());
self.editor.clear_selection();
}
} else if rect_contains(layout.results, column, row) {
self.focus = Focus::Results;
// Map click to result row (inside border + header)
let inner_y = row.saturating_sub(layout.results.y + 2) as usize;
let target_row = inner_y + self.result_scroll_y;
if let Some(result) = self.results.get(self.current_result) {
if target_row < result.rows.len() {
self.result_selected_row = target_row;
}
}
}
}

fn handle_mouse_scroll(&mut self, column: u16, row: u16, delta: i32) {
let layout = self.layout;

if rect_contains(layout.editor, column, row) {
if delta < 0 {
self.editor.scroll_offset =
self.editor.scroll_offset.saturating_sub((-delta) as usize);
} else {
let max_scroll = self.editor.lines.len().saturating_sub(1);
self.editor.scroll_offset =
(self.editor.scroll_offset + delta as usize).min(max_scroll);
}
} else if rect_contains(layout.results, column, row) {
if let Some(result) = self.results.get(self.current_result) {
if delta < 0 {
self.result_selected_row =
self.result_selected_row.saturating_sub((-delta) as usize);
} else {
self.result_selected_row = (self.result_selected_row + delta as usize)
.min(result.rows.len().saturating_sub(1));
}
}
} else if rect_contains(layout.sidebar, column, row) {
let max = match self.sidebar_tab {
SidebarTab::Databases => self.databases.len(),
SidebarTab::Tables => self.tables.len() + self.schemas.len(),
SidebarTab::History => self.query_history.entries().len(),
};
if delta < 0 {
self.sidebar_selected = self.sidebar_selected.saturating_sub((-delta) as usize);
} else {
self.sidebar_selected =
(self.sidebar_selected + delta as usize).min(max.saturating_sub(1));
}
}
}
}

fn rect_contains(rect: Rect, x: u16, y: u16) -> bool {
x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height
}

fn dialog_field_len(config: &ConnectionConfig, field_index: usize) -> usize {
Expand Down
16 changes: 15 additions & 1 deletion src/ui/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::ui::{
SPINNER_FRAMES,
};

pub fn draw(frame: &mut Frame, app: &App) {
pub fn draw(frame: &mut Frame, app: &mut App) {
// Create main layout
let chunks = Layout::default()
.direction(Direction::Vertical)
Expand All @@ -33,6 +33,20 @@ pub fn draw(frame: &mut Frame, app: &App) {
.constraints([Constraint::Length(app.sidebar_width), Constraint::Min(0)])
.split(chunks[1]);

// Store layout rects for mouse support
let panel_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(40), // Editor
Constraint::Min(0), // Results
])
.split(main_chunks[1]);

app.layout.sidebar = main_chunks[0];
app.layout.editor = panel_chunks[0];
app.layout.results = panel_chunks[1];
app.layout.status_bar = chunks[2];

draw_sidebar(frame, app, main_chunks[0]);
draw_main_panel(frame, app, main_chunks[1]);

Expand Down
Loading