Skip to main content

tui/view/
console.rs

1//! Inherent draw + intent handling for `ConsoleState`.
2//!
3//! The draw logic (scrollback + prompt + hint) lives here; the pure state
4//! stays in `tabs::nms`. The console consumes no tab-layer intents (input is
5//! routed via `handle_console` while focused), so `handle` always ignores.
6
7use crate::event::Intent;
8use crate::pane::PaneId;
9use crate::tabs::nms::ConsoleState;
10use crate::view::{DrawCtx, Handled};
11use ratatui::Frame;
12use ratatui::layout::{Constraint, Direction, Layout, Rect};
13use ratatui::style::{Color, Modifier, Style};
14use ratatui::text::{Line, Span};
15use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
16
17fn draw_scrollback(state: &ConsoleState, frame: &mut Frame, area: Rect) {
18    let visible = usize::from(area.height);
19    let lines: Vec<Line> = state
20        .visible_scrollback(visible)
21        .iter()
22        .map(|l| Line::from(l.as_str()))
23        .collect();
24    frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), area);
25}
26
27fn draw_prompt(state: &ConsoleState, frame: &mut Frame, area: Rect, focused: bool) {
28    let marker = if state.pending.is_empty() {
29        "nms> "
30    } else {
31        "...> "
32    };
33    let content = format!("{marker}{}", state.input.buffer());
34    let style = if focused {
35        Style::default()
36            .fg(Color::White)
37            .add_modifier(Modifier::BOLD)
38    } else {
39        Style::default().fg(Color::Gray)
40    };
41    frame.render_widget(Paragraph::new(Span::styled(content, style)), area);
42}
43
44fn draw_hint(frame: &mut Frame, area: Rect, active: bool, pane: PaneId) {
45    let hint = if active {
46        match pane {
47            PaneId::Scrollback => "Tab prompt  Up/Down scroll  PgUp/PgDn page  Esc blur",
48            PaneId::Prompt => "Enter run  Esc blur  C-c interrupt  Up/Down history",
49        }
50    } else {
51        "i/Enter focus  1-6 tabs  Tab next  q quit"
52    };
53    let line = Span::styled(hint, Style::default().fg(Color::DarkGray));
54    frame.render_widget(Paragraph::new(line), area);
55}
56
57impl ConsoleState {
58    pub fn draw(&self, frame: &mut Frame, area: Rect, ctx: &DrawCtx) {
59        let title = if ctx.is_active && self.panes.is_focused(PaneId::Scrollback) {
60            "Console — Scrollback ↕"
61        } else {
62            "Console"
63        };
64        let block = Block::default().borders(Borders::ALL).title(title);
65        let inner = block.inner(area);
66        frame.render_widget(block, area);
67
68        let chunks = Layout::default()
69            .direction(Direction::Vertical)
70            .constraints([
71                Constraint::Min(1),
72                Constraint::Length(1),
73                Constraint::Length(1),
74            ])
75            .split(inner);
76
77        draw_scrollback(self, frame, chunks[0]);
78        draw_prompt(
79            self,
80            frame,
81            chunks[1],
82            ctx.is_active && self.panes.is_focused(PaneId::Prompt),
83        );
84        draw_hint(frame, chunks[2], ctx.is_active, self.panes.focused());
85    }
86
87    pub fn handle(&mut self, _intent: Intent, _ctx: &DrawCtx) -> Handled {
88        Handled::Ignored
89    }
90}