tmux_tui/
app.rs

1//! Application state and behavior: what the manager knows and what each
2//! gesture does. Rendering lives in [`crate::ui`].
3
4use std::cell::{Cell, RefCell};
5use std::io;
6
7use ratatui::crossterm::event::{
8    KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
9};
10use ratatui::layout::Rect;
11
12use crate::geometry::{contains, hit, mpane_at, rects_for, tile_at};
13use crate::menu::{build_menu_items, Action, Menu};
14use crate::tmux::{self, Pane, Win};
15
16/// What is being dragged in window mode.
17pub enum WDrag {
18    /// A whole window (drop on another to reorder).
19    Win(String),
20    /// A pane being moved (drop on another window to relocate it).
21    Pane { win: String, pane: String },
22}
23
24/// A modal text-input prompt (currently used only for renaming a window).
25pub struct Prompt {
26    pub win: String,
27    pub buf: String,
28}
29
30pub struct App {
31    // pane mode (current window)
32    pub win_w: u16,
33    pub win_h: u16,
34    pub panes: Vec<Pane>,
35    pub drag_src: Option<usize>,
36    pub selected: Option<String>, // pane id
37    // window mode
38    pub windows: Vec<Win>,
39    pub window_mode: bool,
40    pub wdrag: Option<WDrag>,
41    pub sel_win: Option<String>,                      // window id
42    pub tiles: RefCell<Vec<(String, Rect)>>,          // window id -> tile rect (last draw)
43    pub mpanes: RefCell<Vec<(String, String, Rect)>>, // (win, pane) -> mini rect
44    // shared / modal
45    pub pending_kill: Option<(String, bool)>, // (id, is_window) awaiting confirm
46    pub prompt: Option<Prompt>,
47    pub menu: Option<Menu>,
48    pub show_help: bool,
49    pub should_quit: bool,
50    pub cursor: (u16, u16),
51    pub status: String,
52    pub last_canvas: Cell<Rect>,
53    pub last_full: Cell<Rect>,
54}
55
56impl App {
57    pub fn new() -> io::Result<Self> {
58        let (win_w, win_h, panes) = tmux::query()?;
59        Ok(Self {
60            win_w,
61            win_h,
62            panes,
63            drag_src: None,
64            selected: None,
65            windows: tmux::query_windows().unwrap_or_default(),
66            window_mode: false,
67            wdrag: None,
68            sel_win: None,
69            tiles: RefCell::new(Vec::new()),
70            mpanes: RefCell::new(Vec::new()),
71            pending_kill: None,
72            prompt: None,
73            menu: None,
74            show_help: false,
75            should_quit: false,
76            cursor: (0, 0),
77            status: "ready".into(),
78            last_canvas: Cell::new(Rect::default()),
79            last_full: Cell::new(Rect::default()),
80        })
81    }
82
83    pub fn refresh(&mut self) -> io::Result<()> {
84        let (w, h, p) = tmux::query()?;
85        self.win_w = w;
86        self.win_h = h;
87        self.panes = p;
88        self.windows = tmux::query_windows().unwrap_or_default();
89        self.drag_src = None;
90        self.wdrag = None;
91        if let Some(sel) = &self.selected {
92            if !self.panes.iter().any(|p| &p.id == sel) {
93                self.selected = None;
94            }
95        }
96        if let Some(sw) = &self.sel_win {
97            if !self.windows.iter().any(|w| &w.id == sw) {
98                self.sel_win = None;
99            }
100        }
101        Ok(())
102    }
103
104    pub fn selected_index(&self) -> Option<usize> {
105        self.selected.as_ref().and_then(|id| self.panes.iter().position(|p| &p.id == id))
106    }
107
108    /// Target pane for create/kill: the selected pane, else the active one.
109    fn target(&self) -> Option<String> {
110        self.selected.clone().or_else(|| self.panes.iter().find(|p| p.active).map(|p| p.id.clone()))
111    }
112
113    /// Id of the currently active (focused) window.
114    pub fn active_window_id(&self) -> Option<String> {
115        self.windows.iter().find(|w| w.active).map(|w| w.id.clone())
116    }
117
118    /// ` index:name ` of the active window, shown in the top bar.
119    pub fn active_window_label(&self) -> Option<String> {
120        self.windows.iter().find(|w| w.active).map(|w| format!(" {}:{} ", w.index, w.name))
121    }
122
123    // --- pane operations ---
124
125    pub fn split(&mut self, horizontal: bool) -> io::Result<()> {
126        let dir = if horizontal { "-h" } else { "-v" };
127        self.new_pane(dir, false)
128    }
129
130    /// Create a pane next to the target. `flag` is "-h"/"-v"; `before` puts the
131    /// new pane left/above instead of right/below.
132    pub fn new_pane(&mut self, flag: &str, before: bool) -> io::Result<()> {
133        let Some(t) = self.target() else {
134            self.status = "no pane to split from".into();
135            return Ok(());
136        };
137        match tmux::split(&t, flag, before)? {
138            Ok(new_id) => {
139                self.refresh()?;
140                if !new_id.is_empty() {
141                    self.selected = Some(new_id);
142                }
143                self.status = "new pane".into();
144            }
145            Err(e) => self.status = format!("split failed: {e}"),
146        }
147        Ok(())
148    }
149
150    pub fn request_kill(&mut self) {
151        if self.panes.len() <= 1 {
152            self.status = "only one pane — refusing to kill the last one".into();
153            return;
154        }
155        match self.target() {
156            Some(id) => {
157                self.status = format!("kill {id}?  Yes / No");
158                self.pending_kill = Some((id, false));
159            }
160            None => self.status = "no pane selected".into(),
161        }
162    }
163
164    pub fn set_layout(&mut self, name: &str, label: &str) -> io::Result<()> {
165        match tmux::select_layout(name)? {
166            Ok(_) => {
167                self.refresh()?;
168                self.status = format!("layout: {label}");
169            }
170            Err(e) => self.status = format!("layout failed: {e}"),
171        }
172        Ok(())
173    }
174
175    pub fn cycle_layout(&mut self) -> io::Result<()> {
176        tmux::next_layout()?;
177        self.refresh()?;
178        self.status = "cycled layout".into();
179        Ok(())
180    }
181
182    // --- window operations ---
183
184    pub fn select_window(&mut self, id: &str) -> io::Result<()> {
185        tmux::select_window(id)?;
186        self.refresh()?;
187        self.sel_win = Some(id.to_string());
188        self.status = format!("window {id}");
189        Ok(())
190    }
191
192    pub fn swap_windows(&mut self, a: &str, b: &str) -> io::Result<()> {
193        tmux::swap_window(a, b)?;
194        self.refresh()?;
195        self.status = format!("reordered {a} ⇄ {b}");
196        Ok(())
197    }
198
199    pub fn move_pane_to_window(&mut self, pane: &str, win: &str) -> io::Result<()> {
200        match tmux::join_pane(pane, win)? {
201            Ok(_) => {
202                self.refresh()?;
203                self.status = format!("moved {pane} → window {win}");
204            }
205            Err(e) => self.status = format!("move failed: {e}"),
206        }
207        Ok(())
208    }
209
210    pub fn new_window(&mut self) -> io::Result<()> {
211        tmux::new_window()?;
212        self.refresh()?;
213        self.status = "new window".into();
214        Ok(())
215    }
216
217    pub fn request_kill_window(&mut self, id: String) {
218        if self.windows.len() <= 1 {
219            self.status = "only one window — refusing to close the last one".into();
220            return;
221        }
222        self.status = format!("close window {id}?  Yes / No");
223        self.pending_kill = Some((id, true));
224    }
225
226    pub fn open_rename(&mut self, id: String) {
227        let name =
228            self.windows.iter().find(|w| w.id == id).map(|w| w.name.clone()).unwrap_or_default();
229        self.status = "rename: type a name, Enter to confirm".into();
230        self.prompt = Some(Prompt { win: id, buf: name });
231    }
232
233    pub fn confirm_rename(&mut self) -> io::Result<()> {
234        if let Some(p) = self.prompt.take() {
235            if !p.buf.is_empty() {
236                tmux::rename_window(&p.win, &p.buf)?;
237                self.status = format!("renamed → {}", p.buf);
238            } else {
239                self.status = "rename cancelled (empty)".into();
240            }
241            self.refresh()?;
242        }
243        Ok(())
244    }
245
246    // --- kill confirmation (shared by panes and windows) ---
247
248    pub fn confirm_kill(&mut self) -> io::Result<()> {
249        if let Some((id, is_win)) = self.pending_kill.take() {
250            if is_win {
251                tmux::kill_window(&id)?;
252                if self.sel_win.as_deref() == Some(id.as_str()) {
253                    self.sel_win = None;
254                }
255                self.status = format!("closed window {id}");
256            } else {
257                tmux::kill_pane(&id)?;
258                if self.selected.as_deref() == Some(id.as_str()) {
259                    self.selected = None;
260                }
261                self.status = format!("killed {id}");
262            }
263            self.refresh()?;
264        }
265        Ok(())
266    }
267
268    // --- menus ---
269
270    pub fn open_menu(&mut self, name: &str, x: u16, y: u16, target: Option<String>) {
271        let (title, items) = build_menu_items(name, &target, self.panes.len(), self.windows.len());
272        self.menu = Some(Menu { x, y, target, title, items, highlight: 0 });
273    }
274
275    pub fn menu_rect(&self, menu: &Menu) -> Rect {
276        let full = self.last_full.get();
277        let label_w = menu.items.iter().map(|(l, _)| l.chars().count()).max().unwrap_or(10) as u16;
278        let w = (label_w + 4).min(full.width.max(1));
279        let h = (menu.items.len() as u16 + 2).min(full.height.max(1));
280        let x = menu.x.min(full.right().saturating_sub(w));
281        let y = menu.y.min(full.bottom().saturating_sub(h));
282        Rect { x, y, width: w, height: h }
283    }
284
285    pub fn menu_item_at(&self, col: u16, row: u16) -> Option<usize> {
286        let menu = self.menu.as_ref()?;
287        let area = self.menu_rect(menu);
288        if !contains(area, col, row) {
289            return None;
290        }
291        let i = row.checked_sub(area.y + 1)? as usize;
292        (i < menu.items.len()).then_some(i)
293    }
294
295    pub fn activate_menu(&mut self, idx: usize) -> io::Result<()> {
296        let Some(menu) = self.menu.as_ref() else {
297            return Ok(());
298        };
299        let Some((_, action)) = menu.items.get(idx).cloned() else {
300            return Ok(());
301        };
302        let target = menu.target.clone();
303
304        // Submenu navigation keeps the menu open and swaps its contents.
305        if let Action::Submenu(name) = action {
306            let (title, items) =
307                build_menu_items(name, &target, self.panes.len(), self.windows.len());
308            if let Some(menu) = &mut self.menu {
309                menu.title = title;
310                menu.items = items;
311                menu.highlight = 0;
312            }
313            return Ok(());
314        }
315
316        self.menu = None;
317        match action {
318            Action::Kill => {
319                self.selected = target;
320                self.request_kill();
321            }
322            Action::Layout(name, label) => self.set_layout(name, label)?,
323            Action::NewPane(flag, before) => {
324                self.selected = target;
325                self.new_pane(flag, before)?;
326            }
327            Action::RenameWindow => {
328                // In window mode the menu targets a tile; in pane mode the
329                // menu targets a pane, so rename the active window instead.
330                let id = if self.window_mode { target } else { self.active_window_id() };
331                if let Some(id) = id {
332                    self.open_rename(id);
333                }
334            }
335            Action::NewWindow => self.new_window()?,
336            Action::KillWindow => {
337                if let Some(t) = target {
338                    self.request_kill_window(t);
339                }
340            }
341            Action::Submenu(_) => unreachable!(),
342        }
343        Ok(())
344    }
345
346    // --- chrome rectangles (shared by mouse hit-testing and drawing) ---
347
348    pub fn quit_button_rect(&self) -> Rect {
349        let full = self.last_full.get();
350        let w = 8u16.min(full.width.max(1));
351        Rect { x: full.x, y: full.y, width: w, height: 1 }
352    }
353
354    pub fn mode_button_rect(&self) -> Rect {
355        let full = self.last_full.get();
356        let w = 11u16.min(full.width.max(1));
357        let x = (full.x + 9).min(full.right().saturating_sub(w));
358        Rect { x, y: full.y, width: w, height: 1 }
359    }
360
361    pub fn help_button_rect(&self) -> Rect {
362        let full = self.last_full.get();
363        let w = 10u16.min(full.width.max(1));
364        Rect { x: full.right().saturating_sub(w), y: full.y, width: w, height: 1 }
365    }
366
367    /// (dialog area, Yes button, No button) for the kill confirmation.
368    pub fn confirm_rects(&self) -> (Rect, Rect, Rect) {
369        let full = self.last_full.get();
370        let w = 34u16.min(full.width.max(1));
371        let h = 5u16.min(full.height.max(1));
372        let area = Rect {
373            x: full.x + full.width.saturating_sub(w) / 2,
374            y: full.y + full.height.saturating_sub(h) / 2,
375            width: w,
376            height: h,
377        };
378        let by = area.y + area.height.saturating_sub(2);
379        let yes = Rect { x: area.x + 4, y: by, width: 7, height: 1 };
380        let no = Rect { x: area.x + area.width.saturating_sub(10), y: by, width: 6, height: 1 };
381        (area, yes, no)
382    }
383
384    /// (dialog area, text field, OK button, Cancel button) for the rename prompt.
385    pub fn prompt_rects(&self) -> (Rect, Rect, Rect, Rect) {
386        let full = self.last_full.get();
387        let w = 44u16.min(full.width.max(1));
388        let h = 6u16.min(full.height.max(1));
389        let area = Rect {
390            x: full.x + full.width.saturating_sub(w) / 2,
391            y: full.y + full.height.saturating_sub(h) / 2,
392            width: w,
393            height: h,
394        };
395        let field =
396            Rect { x: area.x + 2, y: area.y + 2, width: area.width.saturating_sub(4), height: 1 };
397        let by = area.y + area.height.saturating_sub(2);
398        let ok = Rect { x: area.x + 4, y: by, width: 6, height: 1 };
399        let cancel =
400            Rect { x: area.x + area.width.saturating_sub(12), y: by, width: 10, height: 1 };
401        (area, field, ok, cancel)
402    }
403
404    // --- mouse ---
405
406    pub fn on_mouse(&mut self, m: MouseEvent) -> io::Result<()> {
407        self.cursor = (m.column, m.row);
408        let left_down = matches!(m.kind, MouseEventKind::Down(MouseButton::Left));
409
410        // Rename prompt is modal — only its OK/Cancel buttons respond to the mouse.
411        if self.prompt.is_some() {
412            if left_down {
413                let (_, _, ok, cancel) = self.prompt_rects();
414                if contains(ok, m.column, m.row) {
415                    self.confirm_rename()?;
416                } else if contains(cancel, m.column, m.row) {
417                    self.prompt = None;
418                    self.status = "rename cancelled".into();
419                }
420            }
421            return Ok(());
422        }
423
424        // Kill confirmation is modal.
425        if self.pending_kill.is_some() {
426            if left_down {
427                let (_, yes, no) = self.confirm_rects();
428                if contains(yes, m.column, m.row) {
429                    self.confirm_kill()?;
430                } else if contains(no, m.column, m.row) {
431                    self.pending_kill = None;
432                    self.status = "cancelled".into();
433                }
434            }
435            return Ok(());
436        }
437
438        // Help overlay: any left click dismisses it.
439        if self.show_help {
440            if left_down {
441                self.show_help = false;
442            }
443            return Ok(());
444        }
445
446        // Top-bar buttons.
447        if left_down && contains(self.help_button_rect(), m.column, m.row) {
448            self.show_help = true;
449            self.menu = None;
450            return Ok(());
451        }
452        if left_down && contains(self.quit_button_rect(), m.column, m.row) {
453            self.should_quit = true;
454            return Ok(());
455        }
456        if left_down && contains(self.mode_button_rect(), m.column, m.row) {
457            self.set_window_mode(!self.window_mode)?;
458            return Ok(());
459        }
460
461        // An open menu captures interaction first.
462        if self.menu.is_some() {
463            match m.kind {
464                MouseEventKind::Down(MouseButton::Left) => {
465                    match self.menu_item_at(m.column, m.row) {
466                        Some(idx) => self.activate_menu(idx)?,
467                        None => self.menu = None,
468                    }
469                    return Ok(());
470                }
471                MouseEventKind::Down(MouseButton::Right) => self.menu = None,
472                _ => return Ok(()),
473            }
474        }
475
476        if self.window_mode {
477            self.on_mouse_windows(m)
478        } else {
479            self.on_mouse_panes(m)
480        }
481    }
482
483    fn on_mouse_panes(&mut self, m: MouseEvent) -> io::Result<()> {
484        let rects = rects_for(&self.panes, self.last_canvas.get(), self.win_w, self.win_h);
485        match m.kind {
486            MouseEventKind::Down(MouseButton::Right) => {
487                let target = hit(&rects, m.column, m.row).map(|i| self.panes[i].id.clone());
488                self.open_menu("root", m.column, m.row, target);
489            }
490            MouseEventKind::Down(MouseButton::Left) => {
491                self.drag_src = hit(&rects, m.column, m.row);
492            }
493            MouseEventKind::Up(MouseButton::Left) => {
494                if let Some(src) = self.drag_src {
495                    match hit(&rects, m.column, m.row) {
496                        Some(dst) if dst != src => {
497                            let s = self.panes[src].id.clone();
498                            let t = self.panes[dst].id.clone();
499                            tmux::swap_pane(&s, &t)?;
500                            self.refresh()?;
501                            self.selected = Some(s.clone());
502                            self.status = format!("swapped {s} ⇄ {t}");
503                        }
504                        Some(same) => {
505                            let id = self.panes[same].id.clone();
506                            self.status = format!("selected {id}");
507                            self.selected = Some(id);
508                        }
509                        None => self.status = "dropped outside — cancelled".into(),
510                    }
511                }
512                self.drag_src = None;
513            }
514            _ => {}
515        }
516        Ok(())
517    }
518
519    fn on_mouse_windows(&mut self, m: MouseEvent) -> io::Result<()> {
520        let tiles = self.tiles.borrow().clone();
521        let mpanes = self.mpanes.borrow().clone();
522        match m.kind {
523            MouseEventKind::Down(MouseButton::Right) => {
524                let target = tile_at(&tiles, m.column, m.row);
525                self.open_menu("win", m.column, m.row, target);
526            }
527            MouseEventKind::Down(MouseButton::Left) => {
528                // grabbing a mini-pane moves the pane; grabbing the tile frame
529                // moves the whole window.
530                if let Some((win, pane)) = mpane_at(&mpanes, m.column, m.row) {
531                    self.wdrag = Some(WDrag::Pane { win, pane });
532                } else if let Some(win) = tile_at(&tiles, m.column, m.row) {
533                    self.wdrag = Some(WDrag::Win(win));
534                }
535            }
536            MouseEventKind::Up(MouseButton::Left) => {
537                let drop = tile_at(&tiles, m.column, m.row);
538                match self.wdrag.take() {
539                    Some(WDrag::Win(src)) => match drop {
540                        Some(dst) if dst != src => self.swap_windows(&src, &dst)?,
541                        Some(_) => self.select_window(&src)?,
542                        None => {}
543                    },
544                    Some(WDrag::Pane { win, pane }) => match drop {
545                        Some(dst) if dst != win => self.move_pane_to_window(&pane, &dst)?,
546                        Some(same) => self.select_window(&same)?,
547                        None => {}
548                    },
549                    None => {}
550                }
551            }
552            _ => {}
553        }
554        Ok(())
555    }
556
557    // --- keyboard navigation ---
558
559    /// Switch between pane and window mode, seeding the window-mode cursor.
560    pub fn set_window_mode(&mut self, on: bool) -> io::Result<()> {
561        self.window_mode = on;
562        self.menu = None;
563        self.refresh()?;
564        if on && self.sel_win.is_none() {
565            self.sel_win = self.active_window_id();
566        }
567        self.status = if on { "window mode" } else { "pane mode" }.into();
568        Ok(())
569    }
570
571    /// Columns the mission-control grid uses (matches the renderer).
572    fn win_cols(&self) -> i32 {
573        ((self.windows.len() as f64).sqrt().ceil() as i32).max(1)
574    }
575
576    /// Index of the focused window (selected, else active, else first).
577    fn focused_window_index(&self) -> i32 {
578        self.sel_win
579            .as_ref()
580            .and_then(|id| self.windows.iter().position(|w| &w.id == id))
581            .or_else(|| self.windows.iter().position(|w| w.active))
582            .unwrap_or(0) as i32
583    }
584
585    /// Pane mode: move selection directionally, via tmux's spatial logic.
586    pub fn select_dir(&mut self, dir: &str) -> io::Result<()> {
587        tmux::select_pane_dir(dir)?;
588        self.refresh()?;
589        self.selected = self.panes.iter().find(|p| p.active).map(|p| p.id.clone());
590        Ok(())
591    }
592
593    /// Pane mode: swap the selected pane with its neighbour in `dir`.
594    pub fn swap_dir(&mut self, dir: &str) -> io::Result<()> {
595        let Some(src) = self.target() else {
596            return Ok(());
597        };
598        tmux::select_pane_dir(dir)?;
599        let (_, _, panes) = tmux::query()?;
600        if let Some(nbr) = panes.iter().find(|p| p.active).map(|p| p.id.clone()) {
601            if nbr != src {
602                tmux::swap_pane(&src, &nbr)?;
603                tmux::select_pane(&src)?;
604                self.status = format!("moved {src}");
605            }
606        }
607        self.refresh()?;
608        self.selected = Some(src);
609        Ok(())
610    }
611
612    /// Window mode: move the focus across the grid AND switch to that window
613    /// live, so arrowing around takes you through the session.
614    pub fn win_nav(&mut self, dx: i32, dy: i32) -> io::Result<()> {
615        if self.windows.is_empty() {
616            return Ok(());
617        }
618        let n = self.windows.len() as i32;
619        let idx = (self.focused_window_index() + dx + dy * self.win_cols()).clamp(0, n - 1);
620        let id = self.windows[idx as usize].id.clone();
621        self.select_window(&id)
622    }
623
624    /// Window mode: reorder — swap the focused window with the grid neighbour.
625    pub fn win_swap(&mut self, dx: i32, dy: i32) -> io::Result<()> {
626        if self.windows.is_empty() {
627            return Ok(());
628        }
629        let n = self.windows.len() as i32;
630        let cur = self.focused_window_index();
631        let tgt = cur + dx + dy * self.win_cols();
632        if tgt < 0 || tgt >= n || tgt == cur {
633            return Ok(());
634        }
635        let a = self.windows[cur as usize].id.clone();
636        let b = self.windows[tgt as usize].id.clone();
637        self.swap_windows(&a, &b)?;
638        self.sel_win = Some(a);
639        Ok(())
640    }
641
642    /// Dispatch a key in pane mode (arrows navigate, Shift+arrows move).
643    pub fn pane_key(&mut self, k: KeyEvent) -> io::Result<()> {
644        let shift = k.modifiers.contains(KeyModifiers::SHIFT);
645        match k.code {
646            KeyCode::Left => self.move_or_select("-L", shift)?,
647            KeyCode::Right => self.move_or_select("-R", shift)?,
648            KeyCode::Up => self.move_or_select("-U", shift)?,
649            KeyCode::Down => self.move_or_select("-D", shift)?,
650            KeyCode::Char('|') => self.split(true)?,
651            KeyCode::Char('-') => self.split(false)?,
652            KeyCode::Char('x') | KeyCode::Char('d') => self.request_kill(),
653            KeyCode::Char('1') => self.set_layout("even-horizontal", "side-by-side")?,
654            KeyCode::Char('2') => self.set_layout("even-vertical", "stacked")?,
655            KeyCode::Char('3') => self.set_layout("main-vertical", "main-left")?,
656            KeyCode::Char('4') => self.set_layout("main-horizontal", "main-top")?,
657            KeyCode::Char('5') => self.set_layout("tiled", "tiled")?,
658            KeyCode::Char(' ') => self.cycle_layout()?,
659            _ => {}
660        }
661        Ok(())
662    }
663
664    fn move_or_select(&mut self, dir: &str, shift: bool) -> io::Result<()> {
665        if shift {
666            self.swap_dir(dir)
667        } else {
668            self.select_dir(dir)
669        }
670    }
671
672    /// Dispatch a key in window mode (arrows navigate, Shift+arrows reorder).
673    pub fn window_key(&mut self, k: KeyEvent) -> io::Result<()> {
674        let shift = k.modifiers.contains(KeyModifiers::SHIFT);
675        let (dx, dy) = match k.code {
676            KeyCode::Left => (-1, 0),
677            KeyCode::Right => (1, 0),
678            KeyCode::Up => (0, -1),
679            KeyCode::Down => (0, 1),
680            KeyCode::Enter => {
681                // Land in the focused window and close the manager.
682                if let Some(id) = self.sel_win.clone() {
683                    self.select_window(&id)?;
684                }
685                self.should_quit = true;
686                return Ok(());
687            }
688            KeyCode::Char('n') => return self.new_window(),
689            KeyCode::Char('x') | KeyCode::Char('d') => {
690                if let Some(id) = self.sel_win.clone().or_else(|| self.active_window_id()) {
691                    self.request_kill_window(id);
692                }
693                return Ok(());
694            }
695            _ => return Ok(()),
696        };
697        if shift {
698            self.win_swap(dx, dy)?;
699        } else {
700            self.win_nav(dx, dy)?;
701        }
702        Ok(())
703    }
704}