use super::manager::{FocusState, WindowManager};
use crate::app::app_state::AppState;
use crate::app::config_manager::AppConfig;
use crate::input::keybinding_profile::{KeybindingProfile, matches_any};
use crate::input::keyboard_mode::{KeyboardMode, ResizeDirection, SnapPosition, WindowSubMode};
use crate::rendering::RenderBackend;
use crate::ui::info_window::InfoWindow;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::time::{Duration, Instant};
const DOUBLE_BACKTICK_THRESHOLD_MS: u64 = 300;
pub const DIR_LEFT: u8 = 0;
pub const DIR_DOWN: u8 = 1;
pub const DIR_UP: u8 = 2;
pub const DIR_RIGHT: u8 = 3;
fn is_focused_window_locked(window_manager: &WindowManager, auto_tiling_enabled: bool) -> bool {
if let Some(focused_id) = window_manager.get_focused_window_id() {
window_manager.is_window_tiled_locked(focused_id, auto_tiling_enabled)
} else {
false
}
}
#[allow(clippy::too_many_arguments)]
pub fn handle_window_mode_keyboard(
app_state: &mut AppState,
app_config: &mut AppConfig,
key_event: KeyEvent,
window_manager: &mut WindowManager,
backend: &dyn RenderBackend,
profile: &KeybindingProfile,
) -> bool {
let sub_mode = match app_state.keyboard_mode {
KeyboardMode::Normal => return false,
KeyboardMode::WindowMode(sub) => sub,
};
let (cols, rows) = backend.dimensions();
let top_y: u16 = 1;
match sub_mode {
WindowSubMode::Navigation => handle_navigation_mode(
app_state,
app_config,
key_event,
window_manager,
backend,
cols,
rows,
top_y,
profile,
),
WindowSubMode::Move => {
handle_move_mode(app_state, key_event, window_manager, cols, rows, top_y)
}
WindowSubMode::Resize(direction) => {
handle_resize_mode(app_state, key_event, window_manager, direction)
}
}
}
#[allow(clippy::too_many_arguments)]
fn handle_navigation_mode(
app_state: &mut AppState,
app_config: &mut AppConfig,
key_event: KeyEvent,
window_manager: &mut WindowManager,
backend: &dyn RenderBackend,
cols: u16,
rows: u16,
top_y: u16,
profile: &KeybindingProfile,
) -> bool {
let code = key_event.code;
let modifiers = key_event.modifiers;
let has_shift = modifiers.contains(KeyModifiers::SHIFT);
match code {
KeyCode::F(8) | KeyCode::Esc => {
app_state.keyboard_mode.exit_to_normal();
app_state.move_state.reset();
app_state.resize_state.reset();
true
}
KeyCode::Char('`') => {
let now = Instant::now();
let is_double_press = app_state
.last_backtick_time
.map(|t| {
now.duration_since(t) < Duration::from_millis(DOUBLE_BACKTICK_THRESHOLD_MS)
})
.unwrap_or(false);
if is_double_press {
app_state.last_backtick_time = None;
app_state.keyboard_mode.exit_to_normal();
app_state.move_state.reset();
app_state.resize_state.reset();
let _ = window_manager.send_to_focused("`");
} else {
app_state.last_backtick_time = Some(now);
app_state.keyboard_mode.exit_to_normal();
app_state.move_state.reset();
app_state.resize_state.reset();
}
true
}
_ if !has_shift && matches_any(&profile.wm_focus_left, code, modifiers) => {
window_manager.focus_window_in_direction(DIR_LEFT);
true
}
_ if !has_shift && matches_any(&profile.wm_focus_down, code, modifiers) => {
window_manager.focus_window_in_direction(DIR_DOWN);
true
}
_ if !has_shift && matches_any(&profile.wm_focus_up, code, modifiers) => {
window_manager.focus_window_in_direction(DIR_UP);
true
}
_ if !has_shift && matches_any(&profile.wm_focus_right, code, modifiers) => {
window_manager.focus_window_in_direction(DIR_RIGHT);
true
}
_ if matches_any(&profile.wm_snap_left, code, modifiers) => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::FullLeft.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
_ if matches_any(&profile.wm_snap_down, code, modifiers) => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::FullBottom.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
_ if matches_any(&profile.wm_snap_up, code, modifiers) => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::FullTop.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
_ if matches_any(&profile.wm_snap_right, code, modifiers) => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::FullRight.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Tab if !has_shift => {
window_manager.cycle_to_next_window();
true
}
KeyCode::BackTab | KeyCode::Tab if has_shift => {
window_manager.cycle_to_previous_window();
true
}
_ if matches_any(&profile.wm_enter_move, code, modifiers) => {
app_state.keyboard_mode.enter_sub_mode(WindowSubMode::Move);
app_state.move_state.reset();
true
}
_ if matches_any(&profile.wm_enter_resize, code, modifiers) => {
app_state
.keyboard_mode
.enter_sub_mode(WindowSubMode::Resize(ResizeDirection::Default));
app_state.resize_state.reset();
true
}
_ if matches_any(&profile.wm_close, code, modifiers) => {
if code == KeyCode::Char('q') {
let focus = window_manager.get_focus();
if matches!(focus, FocusState::Desktop | FocusState::Topbar) {
return false;
}
}
let closed = window_manager.request_close_focused_window();
if closed && window_manager.window_count() == 0 {
app_state.keyboard_mode.exit_to_normal();
}
true
}
_ if matches_any(&profile.wm_maximize, code, modifiers) => {
window_manager.toggle_focused_window_maximize(cols, rows, app_config.tiling_gaps);
true
}
_ if matches_any(&profile.wm_minimize, code, modifiers) => {
window_manager.toggle_focused_window_minimize();
true
}
KeyCode::Char('t') => {
crate::input::keyboard_handlers::create_terminal_window(
app_state,
window_manager,
backend,
false,
app_config.tiling_gaps,
);
true
}
KeyCode::Char('T') => {
crate::input::keyboard_handlers::create_terminal_window(
app_state,
window_manager,
backend,
true,
app_config.tiling_gaps,
);
true
}
_ if matches_any(&profile.wm_toggle_auto_tiling, code, modifiers) => {
app_config.toggle_auto_tiling_on_startup();
app_state.auto_tiling_enabled = app_config.auto_tiling_on_startup;
let auto_tiling_text = if app_state.auto_tiling_enabled {
"â–ˆ on] Auto Tiling"
} else {
"off â–‘] Auto Tiling"
};
let bar_y = rows - 1;
app_state.auto_tiling_button =
crate::ui::button::Button::new(1, bar_y, auto_tiling_text.to_string());
if app_state.auto_tiling_enabled {
window_manager.auto_position_windows(cols, rows, app_config.tiling_gaps);
}
true
}
KeyCode::Char('1') => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::BottomLeft.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Char('2') => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::BottomCenter.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Char('3') => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::BottomRight.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Char('4') => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::MiddleLeft.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Char('5') => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::Center.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Char('6') => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::MiddleRight.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Char('7') => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::TopLeft.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Char('8') => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::TopCenter.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Char('9') => {
if !is_focused_window_locked(window_manager, app_state.auto_tiling_enabled) {
let (x, y, w, h) = SnapPosition::TopRight.calculate_rect(cols, rows, top_y);
window_manager.snap_focused_window(x, y, w, h);
}
true
}
KeyCode::Char('?') => {
show_winmode_help_window(app_state, cols, rows);
true
}
_ => true,
}
}
fn handle_move_mode(
app_state: &mut AppState,
key_event: KeyEvent,
window_manager: &mut WindowManager,
cols: u16,
rows: u16,
top_y: u16,
) -> bool {
if let Some(focused_id) = window_manager.get_focused_window_id() {
if window_manager.is_window_tiled_locked(focused_id, app_state.auto_tiling_enabled) {
match key_event.code {
KeyCode::Enter
| KeyCode::Esc
| KeyCode::F(8)
| KeyCode::Char('m')
| KeyCode::Char('`') => {
app_state.keyboard_mode.return_to_navigation();
app_state.move_state.reset();
}
_ => {}
}
return true;
}
}
let has_shift = key_event.modifiers.contains(KeyModifiers::SHIFT);
match key_event.code {
KeyCode::Enter | KeyCode::Esc | KeyCode::F(8) | KeyCode::Char('m') => {
app_state.keyboard_mode.return_to_navigation();
app_state.move_state.reset();
true
}
KeyCode::Char('`') => {
let now = Instant::now();
let is_double_press = app_state
.last_backtick_time
.map(|t| {
now.duration_since(t) < Duration::from_millis(DOUBLE_BACKTICK_THRESHOLD_MS)
})
.unwrap_or(false);
if is_double_press {
app_state.last_backtick_time = None;
app_state.keyboard_mode.exit_to_normal();
app_state.move_state.reset();
app_state.resize_state.reset();
let _ = window_manager.send_to_focused("`");
} else {
app_state.last_backtick_time = Some(now);
app_state.keyboard_mode.return_to_navigation();
app_state.move_state.reset();
}
true
}
KeyCode::Char('h') | KeyCode::Left if !has_shift => {
let step = app_state.move_state.get_step() as i16;
window_manager.move_focused_window_by(-step, 0, cols, rows, top_y);
true
}
KeyCode::Char('j') | KeyCode::Down if !has_shift => {
let step = app_state.move_state.get_step() as i16;
window_manager.move_focused_window_by(0, step, cols, rows, top_y);
true
}
KeyCode::Char('k') | KeyCode::Up if !has_shift => {
let step = app_state.move_state.get_step() as i16;
window_manager.move_focused_window_by(0, -step, cols, rows, top_y);
true
}
KeyCode::Char('l') | KeyCode::Right if !has_shift => {
let step = app_state.move_state.get_step() as i16;
window_manager.move_focused_window_by(step, 0, cols, rows, top_y);
true
}
KeyCode::Char('H') | KeyCode::Left if has_shift => {
if let Some(win) = window_manager.get_focused_window() {
let new_x = 0;
window_manager.snap_focused_window(
new_x,
win.window.y,
win.window.width,
win.window.height,
);
}
true
}
KeyCode::Char('J') | KeyCode::Down if has_shift => {
if let Some(win) = window_manager.get_focused_window() {
let new_y = rows.saturating_sub(win.window.height);
window_manager.snap_focused_window(
win.window.x,
new_y,
win.window.width,
win.window.height,
);
}
true
}
KeyCode::Char('K') | KeyCode::Up if has_shift => {
if let Some(win) = window_manager.get_focused_window() {
window_manager.snap_focused_window(
win.window.x,
top_y,
win.window.width,
win.window.height,
);
}
true
}
KeyCode::Char('L') | KeyCode::Right if has_shift => {
if let Some(win) = window_manager.get_focused_window() {
let new_x = cols.saturating_sub(win.window.width);
window_manager.snap_focused_window(
new_x,
win.window.y,
win.window.width,
win.window.height,
);
}
true
}
_ => true,
}
}
fn handle_resize_mode(
app_state: &mut AppState,
key_event: KeyEvent,
window_manager: &mut WindowManager,
_resize_direction: ResizeDirection, ) -> bool {
if let Some(focused_id) = window_manager.get_focused_window_id() {
if window_manager.is_window_tiled_locked(focused_id, app_state.auto_tiling_enabled) {
match key_event.code {
KeyCode::Enter
| KeyCode::Esc
| KeyCode::F(8)
| KeyCode::Char('r')
| KeyCode::Char('`') => {
app_state.keyboard_mode.return_to_navigation();
app_state.resize_state.reset();
}
_ => {}
}
return true;
}
}
let has_shift = key_event.modifiers.contains(KeyModifiers::SHIFT);
match key_event.code {
KeyCode::Enter | KeyCode::Esc | KeyCode::F(8) | KeyCode::Char('r') => {
app_state.keyboard_mode.return_to_navigation();
app_state.resize_state.reset();
true
}
KeyCode::Char('`') => {
let now = Instant::now();
let is_double_press = app_state
.last_backtick_time
.map(|t| {
now.duration_since(t) < Duration::from_millis(DOUBLE_BACKTICK_THRESHOLD_MS)
})
.unwrap_or(false);
if is_double_press {
app_state.last_backtick_time = None;
app_state.keyboard_mode.exit_to_normal();
app_state.move_state.reset();
app_state.resize_state.reset();
let _ = window_manager.send_to_focused("`");
} else {
app_state.last_backtick_time = Some(now);
app_state.keyboard_mode.return_to_navigation();
app_state.resize_state.reset();
}
true
}
KeyCode::Char('h') | KeyCode::Left if !has_shift => {
let step = app_state.resize_state.get_step() as i16;
window_manager.resize_focused_window_by(-step, 0);
true
}
KeyCode::Char('H') | KeyCode::Left if has_shift => {
let step = app_state.resize_state.get_step() as i16;
window_manager.resize_focused_window_from_left(step);
true
}
KeyCode::Char('l') | KeyCode::Right if !has_shift => {
let step = app_state.resize_state.get_step() as i16;
window_manager.resize_focused_window_by(step, 0);
true
}
KeyCode::Char('L') | KeyCode::Right if has_shift => {
let step = app_state.resize_state.get_step() as i16;
window_manager.resize_focused_window_from_left(-step);
true
}
KeyCode::Char('k') | KeyCode::Up if !has_shift => {
let step = app_state.resize_state.get_step() as i16;
window_manager.resize_focused_window_by(0, -step);
true
}
KeyCode::Char('K') | KeyCode::Up if has_shift => {
let step = app_state.resize_state.get_step() as i16;
window_manager.resize_focused_window_from_top(step);
true
}
KeyCode::Char('j') | KeyCode::Down if !has_shift => {
let step = app_state.resize_state.get_step() as i16;
window_manager.resize_focused_window_by(0, step);
true
}
KeyCode::Char('J') | KeyCode::Down if has_shift => {
let step = app_state.resize_state.get_step() as i16;
window_manager.resize_focused_window_from_top(-step);
true
}
_ => true,
}
}
pub fn show_winmode_help_window(app_state: &mut AppState, cols: u16, rows: u16) {
let help_message = "\
{C}WINDOW MODE HELP{W}
Press {Y}`{W} or {Y}F8{W} to toggle Window Mode
{C}NAVIGATION (default){W}
{Y}h{W}/{Y}\u{2190}{W} Focus window to left
{Y}j{W}/{Y}\u{2193}{W} Focus window below
{Y}k{W}/{Y}\u{2191}{W} Focus window above
{Y}l{W}/{Y}\u{2192}{W} Focus window to right
{Y}Tab{W} Cycle to next window
{Y}Shift+Tab{W} Cycle to previous window
{C}SNAP (Shift + h/j/k/l){W}
{Y}H{W} Snap to left half
{Y}J{W} Snap to bottom half
{Y}K{W} Snap to top half
{Y}L{W} Snap to right half
{C}NUMPAD POSITIONS (1-9){W}
{Y}7{W} {Y}8{W} {Y}9{W} Top-left, Top-center, Top-right
{Y}4{W} {Y}5{W} {Y}6{W} Middle-left, Center, Middle-right
{Y}1{W} {Y}2{W} {Y}3{W} Bottom-left, Bottom-center, Bottom-right
{C}WINDOW ACTIONS{W}
{Y}t{W} New terminal window
{Y}T{W} New maximized terminal window
{Y}m{W} Enter Move mode
{Y}r{W} Enter Resize mode
{Y}z{W}/{Y}+{W}/{Y}Space{W} Toggle maximize
{Y}-{W}/{Y}_{W} Toggle minimize
{Y}x{W}/{Y}q{W} Close focused window
{Y}a{W} Toggle auto-tiling
{C}MOVE MODE (after 'm'){W}
{Y}h/j/k/l{W} Move window (adaptive speed)
{Y}Shift+H/J/K/L{W} Snap to edge
{Y}Enter{W}/{Y}Esc{W}/{Y}m{W} Exit Move mode
{C}RESIZE MODE (after 'r'){W}
{Y}h{W}/{Y}l{W} Shrink/Grow width
{Y}k{W}/{Y}j{W} Shrink/Grow height
{Y}Shift{W} Invert direction
{Y}Enter{W}/{Y}Esc{W}/{Y}r{W} Exit Resize mode
{C}EXIT WINDOW MODE{W}
{Y}`{W}/{Y}F8{W}/{Y}Esc{W} Return to Normal mode";
app_state.active_winmode_help_window = Some(InfoWindow::new(
"Window Mode Help".to_string(),
help_message,
cols,
rows,
));
}