Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Navigation, Buffers & Panes

Same rule as focus: you describe intent with a TuiEffect, the runtime owns the state. There is no BufferState API you call into — you return effects, and the runtime mutates tui.buffer for you. You only ever read tui.buffer, and only to render it.

There are two layers:

  • Buffer history — the list of open views you cycle through (like editor tabs or buffers).
  • Panes — how the visible area is split into regions.

Driving it: effects

Every navigation action in your handler maps to one effect. From the buffers example:

#![allow(unused)]
fn main() {
let effect = match action {
    Action::Open(view)    => TuiEffect::Navigate(view),   // open / switch to a view
    Action::NextBuffer    => TuiEffect::NextBuffer,        // cycle history forward
    Action::PrevBuffer    => TuiEffect::PreviousBuffer,    // cycle history back
    Action::CloseBuffer   => TuiEffect::CloseBuffer,       // drop the active buffer
    Action::Split(split)  => TuiEffect::SplitPane(split),  // split the active pane
    Action::NextPane      => TuiEffect::NextPane,          // move pane focus
    Action::PrevPane      => TuiEffect::PreviousPane,
    Action::ClosePane     => TuiEffect::ClosePane,
    Action::Quit          => TuiEffect::Quit,
};
Ok(ActionOutcome::effect(effect))
}

That's the whole handler for a multi-buffer, multi-pane app. Bind keys to those actions and you're done:

#![allow(unused)]
fn main() {
TuiPages::builder(View::Editor)
    .focus_wrap(FocusWrap::Wrap)   // NextBuffer past the end wraps to the first
    .bind(modes::GENERAL, "tab", Action::NextBuffer)
    .bind(modes::GENERAL, "w",   Action::CloseBuffer)
    .bind(modes::GENERAL, "v",   Action::Split(PaneSplit::Vertical))
    .bind(modes::GENERAL, "s",   Action::Split(PaneSplit::Horizontal))
    .bind(modes::GENERAL, "o",   Action::NextPane)
    .bind(modes::GENERAL, "x",   Action::ClosePane)
    // ...
}

PaneSplit is just:

#![allow(unused)]
fn main() {
pub enum PaneSplit {
    Horizontal,  // stacked top/bottom
    Vertical,    // side by side
}
}

TuiEffect::Navigate(view) and buffer/pane cycling honour your FocusWrap policy. Buffer history is per-view: when you navigate back to a buffer, its focus is restored. After a navigation effect the runtime re-runs your page function, so the new view's PageSpec takes effect automatically.

Reading it: rendering

Your renderer reads tui.buffer (a BufferState<V>) to lay out the screen. These are the read accessors:

#![allow(unused)]
fn main() {
tui.buffer.get_active_view()    // Option<&V> — the focused buffer's view
tui.buffer.is_split()           // bool — is the area split into panes?
tui.buffer.split_direction()    // Option<PaneSplit>
tui.buffer.panes()              // &[PaneSession<V>] — one per pane
tui.buffer.active_pane_index()  // usize — which pane has focus
}

A render function that handles both the single and split cases (from the buffers example) looks like:

#![allow(unused)]
fn main() {
pub fn render(frame: &mut Frame, buffer: &BufferState<View>) {
    if buffer.is_split() {
        let dir = match buffer.split_direction() {
            Some(PaneSplit::Vertical) => Direction::Horizontal,
            _                         => Direction::Vertical,
        };
        let areas = Layout::default().direction(dir)
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
            .split(frame.area());

        for (i, pane) in buffer.panes().iter().enumerate() {
            let active = i == buffer.active_pane_index();
            draw_pane(frame, areas[i], pane, active);
        }
    } else {
        draw_single(frame, frame.area(), buffer.get_active_view());
    }
}
}

The shortcut tui.current_view() returns the active buffer's view directly — handy when you don't care about panes.

When do you need this?

Only if your app has tabs/buffers or split panes. A single-page app ignores all of it: there's always exactly one buffer and one pane, and you just render tui.current_view(). See the buffers example for the full thing.