Fix nested-terminal behavior: track size from frame/resize events

- Add App::term_size, updated from the drawn frame area and
  Event::Resize instead of polling crossterm::terminal::size() in
  hit-testing and dialog positioning (correct inside TUI-WM/tmux)
- Handle Event::Resize explicitly
- Fix can_paste tautology (|| true)
- Drop unused import

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:42:19 +02:00
parent ec61b057ac
commit eba6e96743
3 changed files with 27 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
use std::path::PathBuf;
use crate::config::{AppliesTo, Config, CustomMenuItem};
use crate::config::{AppliesTo, Config};
use crate::file_ops::{self, FileEntry};
#[derive(Debug, Clone, PartialEq)]
@@ -107,6 +107,11 @@ pub struct App {
pub settings_hover_button: Option<u8>,
// Terminal capabilities
pub unicode_support: bool,
/// Senast kända terminalstorlek (kolumner, rader). Uppdateras från
/// frame-arean vid varje draw och från Event::Resize — pollar aldrig
/// terminalen, vilket gör hit-testing korrekt även i nästlade
/// terminaler (t.ex. inne i TUI-WM).
pub term_size: (u16, u16),
}
// Which field inside the settings editor is being edited
@@ -181,6 +186,7 @@ impl App {
settings_custom_selected: 0,
settings_hover_button: None,
unicode_support: detect_unicode_support(),
term_size: crossterm::terminal::size().unwrap_or((80, 24)),
}
}
@@ -423,7 +429,9 @@ impl App {
pub fn build_context_menu(&self, x: u16, y: u16, is_sidebar_fav: bool, fav_path: Option<String>) -> ContextMenu {
let has_selection = !self.selected_indices.is_empty();
let has_clipboard = self.clipboard.is_some();
let can_paste = has_clipboard && (self.selected_target_dir().is_some() || true);
// Inklistring går till markerad katalog om en sådan finns, annars
// aktuell katalog — så clipboard-innehåll räcker.
let can_paste = has_clipboard;
// Determine if selected items are files/dirs for custom item filtering
let selected_is_dir = self.selected_indices.iter().all(|&i| {

View File

@@ -32,6 +32,10 @@ fn handle_event(app: &mut App, ev: Event) -> bool {
handle_mouse(app, mouse);
false
}
Event::Resize(w, h) => {
app.term_size = (w, h);
false
}
_ => false,
}
}
@@ -744,7 +748,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) {
// Check if click lands in the submenu (only visible when NewSubmenu is highlighted)
let ns_idx = m.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu));
let in_sub = if let Some(ns_pos) = ns_idx {
let (term_w, _) = crossterm::terminal::size().unwrap_or((120, 40));
let (term_w, _) = app.term_size;
let sub_x = (mx + MENU_W).min(term_w.saturating_sub(SUB_W));
let sub_y = my + 1 + ns_pos as u16;
col >= sub_x && col < sub_x + SUB_W
@@ -800,7 +804,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) {
if row == gear_row && col >= gear_col && col < gear_col + 3 {
// If settings is already open and we click inside the dialog handled by settings_click
if app.dialog == DialogMode::Settings {
let dlg = ui::settings_dialog_rect(sb.x + sb.width);
let dlg = ui::settings_dialog_rect(sb.x + sb.width, app.term_size.1);
settings_click(app, col, row, dlg);
} else {
app.dialog = DialogMode::Settings;
@@ -810,7 +814,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) {
// If settings dialog is open, route all clicks into it
if app.dialog == DialogMode::Settings {
let dlg = ui::settings_dialog_rect(sb.x + sb.width);
let dlg = ui::settings_dialog_rect(sb.x + sb.width, app.term_size.1);
settings_click(app, col, row, dlg);
return;
}
@@ -920,7 +924,7 @@ fn handle_mouse_move(app: &mut App, col: u16, row: u16) {
// Settings hover: highlight rows when mouse moves over the dialog
if app.dialog == DialogMode::Settings {
let layout = ui::compute_layout(app);
let dlg = ui::settings_dialog_rect(layout.status_bar.x + layout.status_bar.width);
let dlg = ui::settings_dialog_rect(layout.status_bar.x + layout.status_bar.width, app.term_size.1);
if col > dlg.x && col < dlg.x + dlg.width.saturating_sub(1)
&& row > dlg.y && row < dlg.y + dlg.height.saturating_sub(1)
{
@@ -983,7 +987,7 @@ fn handle_mouse_move(app: &mut App, col: u16, row: u16) {
// When NewSubmenu is highlighted, check if the mouse moves into the flyout
if let Some(ns_pos) = menu.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu)) {
if menu.selected == ns_pos {
let (term_w, _) = crossterm::terminal::size().unwrap_or((120, 40));
let (term_w, _) = app.term_size;
let sub_x = (mx + MENU_W).min(term_w.saturating_sub(SUB_W));
let sub_y = my + 1 + ns_pos as u16;

View File

@@ -56,9 +56,11 @@ pub struct LayoutAreas {
pub path_input: Rect,
}
/// Compute the layout areas for hit testing (without a frame)
pub fn compute_layout(_app: &App) -> LayoutAreas {
let (width, height) = crossterm::terminal::size().unwrap_or((120, 40));
/// Compute the layout areas for hit testing (without a frame).
/// Uses the last size seen by draw() instead of querying the terminal,
/// so it stays correct inside nested terminals.
pub fn compute_layout(app: &App) -> LayoutAreas {
let (width, height) = app.term_size;
let full = Rect::new(0, 0, width, height);
split_layout(full)
}
@@ -97,6 +99,7 @@ fn split_layout(area: Rect) -> LayoutAreas {
pub fn draw(f: &mut Frame, app: &mut App) {
let area = f.area();
app.term_size = (area.width, area.height);
let layout = split_layout(area);
draw_sidebar(f, app, layout.sidebar);
@@ -644,17 +647,16 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) {
}
/// Compute the settings dialog rect anchored to the bottom-right corner.
pub fn settings_dialog_rect(right_edge: u16) -> ratatui::layout::Rect {
pub fn settings_dialog_rect(right_edge: u16, term_h: u16) -> ratatui::layout::Rect {
let dlg_width: u16 = 72;
let dlg_height: u16 = 26;
let x = right_edge.saturating_sub(dlg_width + 1);
let (_, term_h) = crossterm::terminal::size().unwrap_or((120, 40));
let y = term_h.saturating_sub(dlg_height + 3);
Rect::new(x, y, dlg_width, dlg_height)
}
fn draw_settings(f: &mut Frame, app: &App, right_edge: u16) {
let area = settings_dialog_rect(right_edge);
let area = settings_dialog_rect(right_edge, f.area().height);
let term_area = f.area();
let area = Rect::new(
area.x.min(term_area.width.saturating_sub(area.width)),