use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
use std::time::{Duration, Instant};
use crate::app::state::{FocusedPane, Tab};
use crate::app::{App, AppAction};
use crate::ui::layout::LayoutAreas;
use crate::ui::pane::Pane;
use crate::ui::panes::list_pane;
use crate::ui::widgets::{progress_bar, tab_bar};
use crate::ui::widgets::playlist_modal::PlaylistModalMode;
use crate::ui::Ui;
pub const MIN_PANE_WIDTH: u16 = 10;
fn split_widths(boundary_pct: u16, fixed: u16, min_w: u16) -> Option<(u16, u16)> {
let budget = 100u16.checked_sub(fixed)?;
if budget < min_w * 2 {
return None;
}
let first = boundary_pct.clamp(min_w, budget - min_w);
Some((first, budget - first))
}
fn is_text_input(key: KeyEvent) -> bool {
!key.modifiers.contains(KeyModifiers::CONTROL)
}
pub fn handle_key_event(key: KeyEvent, app: &App, ui: &mut Ui) -> Vec<AppAction> {
let mut actions = Vec::new();
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
actions.push(AppAction::Quit);
return actions;
}
if ui.show_about_modal {
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
ui.show_about_modal = false;
}
KeyCode::Char('g') => {
let _ = std::process::Command::new("xdg-open")
.arg("https://github.com/sqzer-x/ommp")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
KeyCode::Char('s') => {
let _ = std::process::Command::new("xdg-open")
.arg("https://github.com/sponsors/sqzer-x")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
_ => {}
}
return actions;
}
if ui.show_help_modal {
if matches!(key.code, KeyCode::Esc | KeyCode::Char('q')) {
ui.show_help_modal = false;
}
return actions;
}
if ui.show_playlist_modal {
match ui.playlist_modal_mode {
PlaylistModalMode::Create | PlaylistModalMode::Rename => {
match key.code {
KeyCode::Esc => {
ui.playlist_modal_mode = PlaylistModalMode::List;
ui.playlist_modal_input.clear();
}
KeyCode::Enter => {
let name = ui.playlist_modal_input.trim().to_string();
if !name.is_empty() {
if ui.playlist_modal_mode == PlaylistModalMode::Create {
actions.push(AppAction::CreatePlaylist(name));
} else {
actions.push(AppAction::RenamePlaylist {
idx: ui.playlist_modal_selected,
name,
});
}
}
ui.playlist_modal_mode = PlaylistModalMode::List;
ui.playlist_modal_input.clear();
}
KeyCode::Backspace => {
ui.playlist_modal_input.pop();
}
KeyCode::Char(c) if is_text_input(key) => {
ui.playlist_modal_input.push(c);
}
_ => {}
}
}
PlaylistModalMode::List => {
match key.code {
KeyCode::Esc => {
ui.show_playlist_modal = false;
ui.playlist_modal_selected = 0;
}
KeyCode::Up | KeyCode::Char('k') => {
if ui.playlist_modal_selected > 0 {
ui.playlist_modal_selected -= 1;
}
}
KeyCode::Down | KeyCode::Char('j') => {
if !app.playlists.is_empty()
&& ui.playlist_modal_selected < app.playlists.len() - 1
{
ui.playlist_modal_selected += 1;
}
}
KeyCode::Enter => {
if let Some(track_idx) = app.queue.current_index
.and_then(|qi| app.queue.tracks.get(qi).copied())
{
let pl_idx = ui.playlist_modal_selected;
if pl_idx < app.playlists.len() {
if app.playlists[pl_idx].tracks.contains(&track_idx) {
actions.push(AppAction::RemoveFromPlaylist {
playlist_idx: pl_idx,
track_idx,
});
} else {
actions.push(AppAction::AddToPlaylist {
playlist_idx: pl_idx,
track_idx,
});
}
}
}
}
KeyCode::Char('a') => {
ui.playlist_modal_mode = PlaylistModalMode::Create;
ui.playlist_modal_input.clear();
}
KeyCode::Char('d') => {
if !app.playlists.is_empty() {
actions.push(AppAction::DeletePlaylist(ui.playlist_modal_selected));
if ui.playlist_modal_selected > 0
&& ui.playlist_modal_selected >= app.playlists.len() - 1
{
ui.playlist_modal_selected -= 1;
}
}
}
KeyCode::Char('r')
if !app.playlists.is_empty() => {
ui.playlist_modal_mode = PlaylistModalMode::Rename;
ui.playlist_modal_input =
app.playlists[ui.playlist_modal_selected].name.clone();
}
_ => {}
}
}
}
return actions;
}
if ui.show_search_modal {
match key.code {
KeyCode::Esc => {
ui.show_search_modal = false;
ui.search_modal_input.clear();
ui.search_modal_results.clear();
ui.search_modal_selected = 0;
ui.search_modal_scroll = 0;
}
KeyCode::Enter => {
if !ui.search_modal_results.is_empty() {
let track_idx = ui.search_modal_results[ui.search_modal_selected];
actions.push(AppAction::AddToQueue(vec![track_idx]));
ui.show_search_modal = false;
ui.search_modal_input.clear();
ui.search_modal_results.clear();
ui.search_modal_selected = 0;
ui.search_modal_scroll = 0;
}
}
KeyCode::Up | KeyCode::BackTab => {
if ui.search_modal_selected > 0 {
ui.search_modal_selected -= 1;
if ui.search_modal_selected < ui.search_modal_scroll {
ui.search_modal_scroll = ui.search_modal_selected;
}
}
}
KeyCode::Down | KeyCode::Tab => {
if !ui.search_modal_results.is_empty()
&& ui.search_modal_selected < ui.search_modal_results.len() - 1
{
ui.search_modal_selected += 1;
let h = ui.search_modal_result_height;
if h > 0 && ui.search_modal_selected >= ui.search_modal_scroll + h {
ui.search_modal_scroll = ui.search_modal_selected - h + 1;
}
}
}
KeyCode::Backspace => {
ui.search_modal_input.pop();
ui.search_modal_results = app.library.search(&ui.search_modal_input);
ui.search_modal_selected = 0;
ui.search_modal_scroll = 0;
}
KeyCode::Char(c) if is_text_input(key) => {
ui.search_modal_input.push(c);
ui.search_modal_results = app.library.search(&ui.search_modal_input);
ui.search_modal_selected = 0;
ui.search_modal_scroll = 0;
}
_ => {}
}
return actions;
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('s') => ui.show_search_modal = true,
KeyCode::Char('h') => ui.show_help_modal = true,
KeyCode::Char('a') => ui.show_about_modal = true,
KeyCode::Char('l') => actions.push(AppAction::LibrarySync),
_ => {}
}
return actions;
}
match (key.modifiers, key.code) {
(_, KeyCode::Char('q')) => {
actions.push(AppAction::Quit);
return actions;
}
(_, KeyCode::Char(' ')) => {
actions.push(AppAction::PauseResume);
return actions;
}
(_, KeyCode::Char('n')) => {
actions.push(AppAction::NextTrack);
return actions;
}
(KeyModifiers::SHIFT, KeyCode::Char('N')) => {
actions.push(AppAction::PrevTrack);
return actions;
}
(_, KeyCode::Char('+')) | (_, KeyCode::Char('=')) => {
actions.push(AppAction::VolumeUp);
return actions;
}
(_, KeyCode::Char('-')) => {
actions.push(AppAction::VolumeDown);
return actions;
}
(_, KeyCode::Right) => {
actions.push(AppAction::SeekForward);
return actions;
}
(_, KeyCode::Left) => {
actions.push(AppAction::SeekBackward);
return actions;
}
(_, KeyCode::Char('s')) => {
actions.push(AppAction::ToggleShuffle);
return actions;
}
(_, KeyCode::Char('r')) => {
actions.push(AppAction::CycleRepeat);
return actions;
}
(_, KeyCode::Char('b')) => {
if app.queue.current_index.is_some() {
ui.show_playlist_modal = true;
ui.playlist_modal_selected = 0;
}
return actions;
}
(_, KeyCode::Char('p')) => {
ui.info_view = ui.info_view.next();
return actions;
}
(_, KeyCode::Tab) => {
actions.push(AppAction::FocusNext);
return actions;
}
(KeyModifiers::SHIFT, KeyCode::BackTab) => {
actions.push(AppAction::FocusPrev);
return actions;
}
(_, KeyCode::Char('1')) => {
actions.push(AppAction::SwitchTab(Tab::Queue));
return actions;
}
(_, KeyCode::Char('2')) => {
actions.push(AppAction::SwitchTab(Tab::Directories));
return actions;
}
(_, KeyCode::Char('3')) => {
actions.push(AppAction::SwitchTab(Tab::Artists));
return actions;
}
(_, KeyCode::Char('4')) => {
actions.push(AppAction::SwitchTab(Tab::Albums));
return actions;
}
(_, KeyCode::Char('5')) => {
actions.push(AppAction::SwitchTab(Tab::Genre));
return actions;
}
(_, KeyCode::Char('6')) => {
actions.push(AppAction::SwitchTab(Tab::Format));
return actions;
}
(_, KeyCode::Char('7')) => {
actions.push(AppAction::SwitchTab(Tab::Playlists));
return actions;
}
(_, KeyCode::Char('h')) => {
actions.push(AppAction::FocusPrev);
return actions;
}
(_, KeyCode::Char('l')) => {
actions.push(AppAction::FocusNext);
return actions;
}
_ => {}
}
let action = match app.focus {
FocusedPane::Library => library_key(key, app, ui),
FocusedPane::Playlist => {
match key.code {
KeyCode::Char('j') | KeyCode::Down => None,
KeyCode::Char('k') | KeyCode::Up => None,
_ => ui.queue_pane.handle_key(key, app),
}
}
FocusedPane::Lyrics => ui.track_info_pane.handle_key(key, app),
};
if let Some(ref a) = action {
if matches!(a, AppAction::AddToQueue(_)) && app.focus == FocusedPane::Library {
actions.push(AppAction::FocusPane(FocusedPane::Playlist));
}
}
if let Some(a) = action {
actions.push(a);
}
actions
}
pub fn handle_mouse_event(
mouse: MouseEvent,
app: &App,
ui: &mut Ui,
terminal_area: ratatui::layout::Rect,
) -> Vec<AppAction> {
let mut actions = Vec::new();
let areas = LayoutAreas::compute(terminal_area, ui.pane_widths, ui.right_split);
let x = mouse.column;
let y = mouse.row;
ui.mouse_pos = Some((x, y));
if ui.show_search_modal {
let ra = ui.search_modal_result_area;
let in_results = x >= ra.x && x < ra.x + ra.width
&& y >= ra.y && y < ra.y + ra.height;
if in_results && !ui.search_modal_results.is_empty() {
let row = ui.search_modal_scroll + (y - ra.y) as usize;
if row < ui.search_modal_results.len() {
ui.search_modal_hover_row = Some(row);
} else {
ui.search_modal_hover_row = None;
}
} else {
ui.search_modal_hover_row = None;
}
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
if in_results && !ui.search_modal_results.is_empty() {
let clicked = ui.search_modal_scroll + (y - ra.y) as usize;
if clicked < ui.search_modal_results.len() {
let is_double = is_double_click(ui.last_click, x, y);
ui.last_click = if is_double {
None
} else {
Some((Instant::now(), x, y))
};
if is_double {
let track_idx = ui.search_modal_results[clicked];
actions.push(AppAction::AddToQueue(vec![track_idx]));
ui.show_search_modal = false;
ui.search_modal_input.clear();
ui.search_modal_results.clear();
ui.search_modal_selected = 0;
ui.search_modal_scroll = 0;
} else {
ui.search_modal_selected = clicked;
}
}
}
}
MouseEventKind::ScrollDown => {
if in_results && !ui.search_modal_results.is_empty() {
let max_scroll = ui.search_modal_results.len()
.saturating_sub(ui.search_modal_result_height);
if ui.search_modal_scroll < max_scroll {
ui.search_modal_scroll += 1;
}
}
}
MouseEventKind::ScrollUp
if in_results && ui.search_modal_scroll > 0 => {
ui.search_modal_scroll -= 1;
}
_ => {}
}
return actions;
}
if ui.show_about_modal || ui.show_help_modal || ui.show_playlist_modal {
return actions;
}
let hit = PaneHit::at(&areas, x, y);
let in_library = hit.library;
let in_playlist = hit.playlist;
let in_lyrics = hit.lyrics;
update_hover(ui, &areas, app, x, y, in_library, in_playlist);
update_tab_hover(ui, &areas, x, y);
if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
if let Some(pane) = hit.pane() {
if app.focus != pane {
actions.push(AppAction::FocusPane(pane));
}
}
}
let border0_x = areas.library.x + areas.library.width; let border1_x = areas.playlist.x + areas.playlist.width; let in_dashboard_y = y >= areas.library.y && y < areas.library.y + areas.library.height;
if ui.dragging_border.is_some() {
match mouse.kind {
MouseEventKind::Drag(MouseButton::Left) | MouseEventKind::Moved => {
if let Some(border_idx) = ui.dragging_border {
let dashboard_x = areas.library.x;
let dashboard_w = areas.library.width + areas.playlist.width + areas.lyrics.width;
if dashboard_w > 0 {
let rel_x = x.saturating_sub(dashboard_x);
let pct = ((rel_x as u32 * 100) / dashboard_w as u32) as u16;
let min_w: u16 = MIN_PANE_WIDTH;
if border_idx == 0 {
if let Some((lib, play)) = split_widths(pct, ui.pane_widths[2], min_w) {
ui.pane_widths[0] = lib;
ui.pane_widths[1] = play;
}
} else if border_idx == 1 {
let boundary = pct.saturating_sub(ui.pane_widths[0]);
if let Some((play, right)) =
split_widths(boundary, ui.pane_widths[0], min_w)
{
ui.pane_widths[1] = play;
ui.pane_widths[2] = right;
}
} else if border_idx == 2 {
let right_top = areas.info_pane.y;
let right_h = areas.info_pane.height + areas.lyrics.height;
if right_h > 0 {
let rel_y = y.saturating_sub(right_top);
let pct_v = ((rel_y as u32 * 100) / right_h as u32).clamp(10, 90) as u16;
ui.right_split = pct_v;
}
}
}
}
return actions;
}
MouseEventKind::Up(MouseButton::Left) => {
ui.dragging_border = None;
return actions;
}
_ => {
ui.dragging_border = None;
}
}
}
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
if in_dashboard_y && mouse.modifiers.contains(KeyModifiers::CONTROL) {
if x.abs_diff(border0_x) <= 1 {
ui.dragging_border = Some(0);
return actions;
}
if x.abs_diff(border1_x) <= 1 {
ui.dragging_border = Some(1);
return actions;
}
let border2_y = areas.info_pane.y + areas.info_pane.height;
if x >= areas.info_pane.x
&& x < areas.info_pane.x + areas.info_pane.width
&& y.abs_diff(border2_y) <= 1
{
ui.dragging_border = Some(2);
return actions;
}
}
let is_double_click = is_double_click(ui.last_click, x, y);
ui.last_click = if is_double_click {
None
} else {
Some((Instant::now(), x, y))
};
if y >= areas.tab_bar.y && y < areas.tab_bar.y + areas.tab_bar.height {
if let Some(tab_idx) = tab_bar::tab_hit_test(areas.tab_bar, x, y) {
actions.push(AppAction::SwitchTab(Tab::from_index(tab_idx)));
}
return actions;
}
if y >= areas.progress_bar.y && y < areas.progress_bar.y + areas.progress_bar.height {
let gauge_area = progress_bar::progress_gauge_area(areas.progress_bar);
if x >= gauge_area.x && x < gauge_area.x + gauge_area.width {
let ratio = (x - gauge_area.x) as f64 / gauge_area.width as f64;
let seek_pos = ratio * app.playback.duration_secs;
actions.push(AppAction::Seek(seek_pos));
}
return actions;
}
if is_double_click && in_playlist {
let block = ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL);
let inner = block.inner(areas.playlist);
if y >= inner.y && y < inner.y + inner.height {
let clicked = ui.queue_pane.scroll_offset + (y - inner.y) as usize;
if clicked < app.queue.tracks.len() {
actions.push(AppAction::PlayQueueIndex(clicked));
return actions;
}
}
}
let clicked_library_row = clicked_row_in(areas.library, y)
.map(|row| ui.library_pane_scroll(app) + row)
.filter(|&row| row < library_row_count(app, ui));
if in_library && clicked_library_row.is_some() {
library_mouse(mouse, areas.library, app, ui);
let enter_key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
if let Some(action) = library_key(enter_key, app, ui) {
if matches!(action, AppAction::AddToQueue(_)) {
actions.push(AppAction::FocusPane(FocusedPane::Playlist));
}
actions.push(action);
}
} else if in_playlist {
if let Some(a) = ui.queue_pane.handle_mouse(mouse, areas.playlist, app) {
actions.push(a);
}
let block = ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL);
let inner = block.inner(areas.playlist);
if y >= inner.y && y < inner.y + inner.height {
let clicked = ui.queue_pane.scroll_offset + (y - inner.y) as usize;
if clicked < app.queue.tracks.len() {
actions.push(AppAction::SetQueueSelection(clicked));
}
}
} else if in_lyrics {
if let Some(a) = ui.track_info_pane.handle_mouse(mouse, areas.lyrics, app) {
actions.push(a);
}
}
}
MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => {
if in_library {
if let Some(a) = library_mouse(mouse, areas.library, app, ui) {
actions.push(a);
}
} else if in_playlist {
if let Some(a) = ui.queue_pane.handle_mouse(mouse, areas.playlist, app) {
actions.push(a);
}
} else if in_lyrics {
if let Some(a) = ui.track_info_pane.handle_mouse(mouse, areas.lyrics, app) {
actions.push(a);
}
}
}
_ => {
}
}
actions
}
const DOUBLE_CLICK: Duration = Duration::from_millis(400);
fn clicked_row_in(area: ratatui::layout::Rect, y: u16) -> Option<usize> {
let inner = ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL)
.inner(area);
(y >= inner.y && y < inner.y + inner.height).then(|| (y - inner.y) as usize)
}
fn library_row_count(app: &App, ui: &Ui) -> usize {
match app.tab {
Tab::Queue => ui.library_pane.row_count(app),
Tab::Directories => ui.dir_browser_pane.entries.len(),
tab => list_pane::row_count(app, tab),
}
}
fn library_key(key: KeyEvent, app: &App, ui: &mut Ui) -> Option<AppAction> {
match app.tab {
Tab::Queue => ui.library_pane.handle_key(key, app),
Tab::Directories => ui.dir_browser_pane.handle_key(key, app),
tab => {
let count = list_pane::row_count(app, tab);
let pane = ui.list_pane_mut(tab);
pane.handle_key(key, count)
.then(|| list_pane::activate(app, tab, pane.selected))
.flatten()
}
}
}
fn library_mouse(mouse: MouseEvent, area: Rect, app: &App, ui: &mut Ui) -> Option<AppAction> {
match app.tab {
Tab::Queue => ui.library_pane.handle_mouse(mouse, area, app),
Tab::Directories => ui.dir_browser_pane.handle_mouse(mouse, area, app),
tab => {
let count = list_pane::row_count(app, tab);
ui.list_pane_mut(tab).handle_mouse(mouse, area, count);
None
}
}
}
fn is_double_click(last: Option<(Instant, u16, u16)>, x: u16, y: u16) -> bool {
last.is_some_and(|(at, lx, ly)| lx == x && ly == y && at.elapsed() < DOUBLE_CLICK)
}
struct PaneHit {
library: bool,
playlist: bool,
lyrics: bool,
}
impl PaneHit {
fn at(areas: &LayoutAreas, x: u16, y: u16) -> Self {
let inside = |r: ratatui::layout::Rect| {
x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height
};
Self {
library: inside(areas.library),
playlist: inside(areas.playlist),
lyrics: inside(areas.lyrics),
}
}
fn pane(&self) -> Option<FocusedPane> {
if self.library {
Some(FocusedPane::Library)
} else if self.playlist {
Some(FocusedPane::Playlist)
} else if self.lyrics {
Some(FocusedPane::Lyrics)
} else {
None
}
}
}
fn update_tab_hover(ui: &mut Ui, areas: &LayoutAreas, x: u16, y: u16) {
ui.hovered_tab = if y >= areas.tab_bar.y && y < areas.tab_bar.y + areas.tab_bar.height {
tab_bar::tab_hit_test(areas.tab_bar, x, y)
} else {
None
};
}
fn clear_all_hovers(ui: &mut Ui) {
ui.queue_pane.hover_row = None;
ui.library_pane.hover_row = None;
ui.dir_browser_pane.hover_row = None;
for pane in &mut ui.list_panes {
pane.hover_row = None;
}
}
fn update_hover(
ui: &mut Ui,
areas: &LayoutAreas,
app: &App,
x: u16,
y: u16,
in_library: bool,
in_playlist: bool,
) {
clear_all_hovers(ui);
if in_playlist {
let block = ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL);
let inner = block.inner(areas.playlist);
if x >= inner.x && x < inner.x + inner.width
&& y >= inner.y && y < inner.y + inner.height
{
let row = ui.queue_pane.scroll_offset + (y - inner.y) as usize;
if row < app.queue.tracks.len() {
ui.queue_pane.hover_row = Some(row);
}
}
} else if in_library {
let block = ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL);
let inner = block.inner(areas.library);
let row_count = library_row_count(app, ui);
let scroll = ui.library_pane_scroll(app);
if x >= inner.x && x < inner.x + inner.width
&& y >= inner.y && y < inner.y + inner.height
&& (scroll + (y - inner.y) as usize) < row_count
{
let row = scroll + (y - inner.y) as usize;
match app.tab {
Tab::Queue => ui.library_pane.hover_row = Some(row),
Tab::Directories => ui.dir_browser_pane.hover_row = Some(row),
tab => ui.list_pane_mut(tab).hover_row = Some(row),
}
}
}
}
pub fn refresh_hover(app: &App, ui: &mut Ui, terminal_area: ratatui::layout::Rect) {
if ui.show_about_modal || ui.show_help_modal || ui.show_search_modal || ui.show_playlist_modal {
return;
}
let Some((x, y)) = ui.mouse_pos else { return };
let areas = LayoutAreas::compute(terminal_area, ui.pane_widths, ui.right_split);
let hit = PaneHit::at(&areas, x, y);
update_hover(ui, &areas, app, x, y, hit.library, hit.playlist);
update_tab_hover(ui, &areas, x, y);
}
pub fn update_queue_selection(app: &mut App, key: KeyEvent) {
let count = app.queue.tracks.len();
if count == 0 {
return;
}
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
if app.queue.selected_index < count - 1 {
app.queue.selected_index += 1;
}
}
KeyCode::Char('k') | KeyCode::Up
if app.queue.selected_index > 0 => {
app.queue.selected_index -= 1;
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn new_ui() -> Ui {
Ui::new(
PathBuf::from("."),
ratatui_image::picker::Picker::from_fontsize((8, 16)),
)
}
fn ctrl(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
}
fn plain(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
}
#[test]
fn ctrl_commands_open_their_modals() {
let app = App::new(PathBuf::from("."));
let mut ui = new_ui();
handle_key_event(ctrl('s'), &app, &mut ui);
assert!(ui.show_search_modal);
let mut ui = new_ui();
handle_key_event(ctrl('h'), &app, &mut ui);
assert!(ui.show_help_modal);
let mut ui = new_ui();
handle_key_event(ctrl('a'), &app, &mut ui);
assert!(ui.show_about_modal);
let mut ui = new_ui();
let actions = handle_key_event(ctrl('l'), &app, &mut ui);
assert!(matches!(actions.as_slice(), [AppAction::LibrarySync]));
}
#[test]
fn ctrl_shortcuts_do_not_fall_through_to_the_unmodified_binding() {
let app = App::new(PathBuf::from("."));
let mut ui = new_ui();
let actions = handle_key_event(ctrl('s'), &app, &mut ui);
assert!(actions.is_empty(), "Ctrl+S must not emit ToggleShuffle");
assert!(ui.show_search_modal);
let mut ui = new_ui();
let actions = handle_key_event(plain('s'), &app, &mut ui);
assert!(matches!(actions.as_slice(), [AppAction::ToggleShuffle]));
assert!(!ui.show_search_modal);
}
#[test]
fn ctrl_c_quits_even_from_inside_a_text_field() {
let app = App::new(PathBuf::from("."));
let mut ui = new_ui();
ui.show_search_modal = true;
let actions = handle_key_event(ctrl('c'), &app, &mut ui);
assert!(matches!(actions.as_slice(), [AppAction::Quit]));
assert!(ui.search_modal_input.is_empty());
}
#[test]
fn split_widths_keeps_the_total_at_100_and_both_panes_above_the_minimum() {
let (a, b) = split_widths(50, 20, MIN_PANE_WIDTH).unwrap();
assert_eq!((a, b), (50, 30));
assert_eq!(a + b + 20, 100);
}
#[test]
fn split_widths_survives_dragging_past_either_edge() {
let (a, b) = split_widths(0, 20, MIN_PANE_WIDTH).unwrap();
assert_eq!((a, b), (MIN_PANE_WIDTH, 80 - MIN_PANE_WIDTH));
let (a, b) = split_widths(500, 20, MIN_PANE_WIDTH).unwrap();
assert_eq!((a, b), (80 - MIN_PANE_WIDTH, MIN_PANE_WIDTH));
}
#[test]
fn split_widths_refuses_when_the_third_pane_leaves_no_room() {
assert_eq!(split_widths(50, 85, MIN_PANE_WIDTH), None);
assert_eq!(split_widths(50, 120, MIN_PANE_WIDTH), None);
assert_eq!(split_widths(50, 80, MIN_PANE_WIDTH), Some((10, 10)));
}
}