Skip to main content

Terminal

Struct Terminal 

Source
pub struct Terminal<B: Backend> { /* private fields */ }
Expand description

Renderer that manages live and pinned blocks over a terminal backend.

A terminal starts by hiding the cursor. Callers mutate blocks, call Terminal::render when they want output, and call Terminal::finish to leave the live transcript behind and restore terminal state.

Implementations§

Source§

impl Terminal<StdoutBackend<Stdout>>

Source

pub fn stdout() -> Result<Self>

Creates a terminal that renders to process stdout.

Examples found in repository?
examples/conversation.rs (line 15)
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11    // Run this example manually to watch Mural update a conversation in the
12    // terminal's normal buffer:
13    //
14    //     cargo run --example conversation
15    let mut terminal = Terminal::stdout()?;
16
17    // Live blocks are the transcript. Pinned blocks render after live blocks and
18    // are useful for transient status and input UI. The status block starts
19    // hidden, then appears directly above the input separator while the
20    // assistant is answering.
21    terminal.push_pinned(Text::from_plain("")?)?;
22    terminal.insert_pinned("status", pinned(AnswerStatus::hidden()))?;
23    terminal.push_pinned(pinned(
24        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
25    ))?;
26    terminal.insert_pinned(
27        "input",
28        pinned(
29            Textarea::new()
30                .placeholder("type a message…")?
31                .placeholder_style(Style::new().fg(Color::BrightBlack).dim())
32                .max_height(3),
33        ),
34    )?;
35    terminal.push_pinned(pinned(
36        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
37    ))?;
38    render_frames(&mut terminal, 8)?;
39
40    // The first user message also goes through the textarea: applications own
41    // raw mode and keyboard events; this example mutates the textarea directly
42    // to simulate typing and submitting.
43    type_into_input(&mut terminal, "explain Mural in one sentence")?;
44    render_frames(&mut terminal, 4)?;
45    submit_input(&mut terminal)?;
46
47    show_status(&mut terminal, "answering")?;
48    terminal.insert_live("thinking", live_padded(thinking_message("thinking…")?))?;
49    render_frames(&mut terminal, 10)?;
50    terminal.remove_live("thinking")?;
51    terminal.insert_live("assistant", live_padded(assistant_message("Mural keeps")?))?;
52    render_frames(&mut terminal, 3)?;
53
54    // Identified blocks can be mutated between renders. This simulates a
55    // streaming assistant response over several visible frames. The pinned
56    // status spinner advances on every render while it is visible.
57    for content in [
58        "Mural keeps",
59        "Mural keeps a live conversation",
60        "Mural keeps a live conversation region plus pinned input",
61        "Mural keeps a live conversation region plus pinned input/status UI in a normal terminal buffer.",
62    ] {
63        *terminal
64            .live_block_mut::<Padding<ListItem>>("assistant")?
65            .content_mut() = assistant_message(content)?;
66        render_frames(&mut terminal, 5)?;
67    }
68    hide_status(&mut terminal)?;
69    render_frames(&mut terminal, 6)?;
70
71    // Type a second message, move the cursor left, insert a missing word, then
72    // submit. The submitted value is appended to the live transcript.
73    type_into_input(&mut terminal, "what happens if terminal changes size?")?;
74    move_input_left(&mut terminal, "terminal changes size?".chars().count())?;
75    type_into_input(&mut terminal, "the ")?;
76    render_frames(&mut terminal, 6)?;
77
78    let submitted = submit_input(&mut terminal)?;
79    show_status(&mut terminal, "adapting to resize")?;
80    terminal.insert_live(
81        "thinking-resize",
82        live_padded(thinking_message("checking terminal size…")?),
83    )?;
84    render_frames(&mut terminal, 8)?;
85
86    terminal.resize(Size::new(48, 12))?;
87    *terminal
88        .live_block_mut::<Padding<ListItem>>("thinking-resize")?
89        .content_mut() = thinking_message("reflowing the conversation for 48 columns…")?;
90    render_frames(&mut terminal, 10)?;
91    terminal.remove_live("thinking-resize")?;
92    terminal.insert_live(
93        "assistant-resize",
94        live_padded(assistant_message(format!(
95            "For `{submitted}`, the caller notifies Mural about the new size, and the next render performs a full redraw at the new safe width."
96        ))?),
97    )?;
98    render_frames(&mut terminal, 12)?;
99    hide_status(&mut terminal)?;
100    render_frames(&mut terminal, 6)?;
101
102    // finish() removes pinned UI, leaves live transcript text behind, restores
103    // the cursor, and flushes the backend.
104    terminal.finish()?;
105    println!("\nfinished: pinned status/input were cleaned up; live transcript remains above.");
106
107    Ok(())
108}
Source§

impl<B: Backend> Terminal<B>

Source

pub fn new(backend: B) -> Result<Self>

Creates a terminal over backend and hides the cursor.

Source

pub fn backend(&self) -> &B

Returns the backend used by this terminal.

This is primarily useful for tests and backend-specific inspection. Prefer the renderer APIs for normal terminal output.

Source

pub fn backend_mut(&mut self) -> &mut B

Returns mutable access to the backend used by this terminal.

Direct backend writes can invalidate the renderer’s cached screen assumptions. After writing through this escape hatch, call Terminal::force_full_redraw before the next Terminal::render so the renderer recovers with a full rewrite.

Source

pub fn push_live(&mut self, block: impl Render + 'static) -> Result<()>

Appends an unidentified live block before pinned blocks.

Live blocks form the durable conversation transcript and are left behind by Terminal::finish.

Examples found in repository?
examples/conversation.rs (line 244)
234fn submit_input<B: mural::Backend>(
235    terminal: &mut Terminal<B>,
236) -> Result<String, Box<dyn std::error::Error>> {
237    let submitted = {
238        terminal
239            .pinned_block_mut::<Padding<Textarea>>("input")?
240            .content_mut()
241            .take()
242    };
243
244    terminal.push_live(live_padded(user_message(&submitted)?))?;
245    render_frame(terminal)?;
246    Ok(submitted)
247}
Source

pub fn push_pinned(&mut self, block: impl Render + 'static) -> Result<()>

Appends an unidentified pinned block after all live blocks.

Pinned blocks are useful for status UI and are removed during Terminal::finish.

Examples found in repository?
examples/conversation.rs (line 21)
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11    // Run this example manually to watch Mural update a conversation in the
12    // terminal's normal buffer:
13    //
14    //     cargo run --example conversation
15    let mut terminal = Terminal::stdout()?;
16
17    // Live blocks are the transcript. Pinned blocks render after live blocks and
18    // are useful for transient status and input UI. The status block starts
19    // hidden, then appears directly above the input separator while the
20    // assistant is answering.
21    terminal.push_pinned(Text::from_plain("")?)?;
22    terminal.insert_pinned("status", pinned(AnswerStatus::hidden()))?;
23    terminal.push_pinned(pinned(
24        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
25    ))?;
26    terminal.insert_pinned(
27        "input",
28        pinned(
29            Textarea::new()
30                .placeholder("type a message…")?
31                .placeholder_style(Style::new().fg(Color::BrightBlack).dim())
32                .max_height(3),
33        ),
34    )?;
35    terminal.push_pinned(pinned(
36        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
37    ))?;
38    render_frames(&mut terminal, 8)?;
39
40    // The first user message also goes through the textarea: applications own
41    // raw mode and keyboard events; this example mutates the textarea directly
42    // to simulate typing and submitting.
43    type_into_input(&mut terminal, "explain Mural in one sentence")?;
44    render_frames(&mut terminal, 4)?;
45    submit_input(&mut terminal)?;
46
47    show_status(&mut terminal, "answering")?;
48    terminal.insert_live("thinking", live_padded(thinking_message("thinking…")?))?;
49    render_frames(&mut terminal, 10)?;
50    terminal.remove_live("thinking")?;
51    terminal.insert_live("assistant", live_padded(assistant_message("Mural keeps")?))?;
52    render_frames(&mut terminal, 3)?;
53
54    // Identified blocks can be mutated between renders. This simulates a
55    // streaming assistant response over several visible frames. The pinned
56    // status spinner advances on every render while it is visible.
57    for content in [
58        "Mural keeps",
59        "Mural keeps a live conversation",
60        "Mural keeps a live conversation region plus pinned input",
61        "Mural keeps a live conversation region plus pinned input/status UI in a normal terminal buffer.",
62    ] {
63        *terminal
64            .live_block_mut::<Padding<ListItem>>("assistant")?
65            .content_mut() = assistant_message(content)?;
66        render_frames(&mut terminal, 5)?;
67    }
68    hide_status(&mut terminal)?;
69    render_frames(&mut terminal, 6)?;
70
71    // Type a second message, move the cursor left, insert a missing word, then
72    // submit. The submitted value is appended to the live transcript.
73    type_into_input(&mut terminal, "what happens if terminal changes size?")?;
74    move_input_left(&mut terminal, "terminal changes size?".chars().count())?;
75    type_into_input(&mut terminal, "the ")?;
76    render_frames(&mut terminal, 6)?;
77
78    let submitted = submit_input(&mut terminal)?;
79    show_status(&mut terminal, "adapting to resize")?;
80    terminal.insert_live(
81        "thinking-resize",
82        live_padded(thinking_message("checking terminal size…")?),
83    )?;
84    render_frames(&mut terminal, 8)?;
85
86    terminal.resize(Size::new(48, 12))?;
87    *terminal
88        .live_block_mut::<Padding<ListItem>>("thinking-resize")?
89        .content_mut() = thinking_message("reflowing the conversation for 48 columns…")?;
90    render_frames(&mut terminal, 10)?;
91    terminal.remove_live("thinking-resize")?;
92    terminal.insert_live(
93        "assistant-resize",
94        live_padded(assistant_message(format!(
95            "For `{submitted}`, the caller notifies Mural about the new size, and the next render performs a full redraw at the new safe width."
96        ))?),
97    )?;
98    render_frames(&mut terminal, 12)?;
99    hide_status(&mut terminal)?;
100    render_frames(&mut terminal, 6)?;
101
102    // finish() removes pinned UI, leaves live transcript text behind, restores
103    // the cursor, and flushes the backend.
104    terminal.finish()?;
105    println!("\nfinished: pinned status/input were cleaned up; live transcript remains above.");
106
107    Ok(())
108}
Source

pub fn insert_live( &mut self, id: impl Into<String>, block: impl Render + 'static, ) -> Result<(), TerminalError>

Appends an identified live block.

Identified blocks can later be mutated with Terminal::live_block_mut or removed with Terminal::remove_live. Ids are unique across live and pinned regions.

Examples found in repository?
examples/conversation.rs (line 48)
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11    // Run this example manually to watch Mural update a conversation in the
12    // terminal's normal buffer:
13    //
14    //     cargo run --example conversation
15    let mut terminal = Terminal::stdout()?;
16
17    // Live blocks are the transcript. Pinned blocks render after live blocks and
18    // are useful for transient status and input UI. The status block starts
19    // hidden, then appears directly above the input separator while the
20    // assistant is answering.
21    terminal.push_pinned(Text::from_plain("")?)?;
22    terminal.insert_pinned("status", pinned(AnswerStatus::hidden()))?;
23    terminal.push_pinned(pinned(
24        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
25    ))?;
26    terminal.insert_pinned(
27        "input",
28        pinned(
29            Textarea::new()
30                .placeholder("type a message…")?
31                .placeholder_style(Style::new().fg(Color::BrightBlack).dim())
32                .max_height(3),
33        ),
34    )?;
35    terminal.push_pinned(pinned(
36        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
37    ))?;
38    render_frames(&mut terminal, 8)?;
39
40    // The first user message also goes through the textarea: applications own
41    // raw mode and keyboard events; this example mutates the textarea directly
42    // to simulate typing and submitting.
43    type_into_input(&mut terminal, "explain Mural in one sentence")?;
44    render_frames(&mut terminal, 4)?;
45    submit_input(&mut terminal)?;
46
47    show_status(&mut terminal, "answering")?;
48    terminal.insert_live("thinking", live_padded(thinking_message("thinking…")?))?;
49    render_frames(&mut terminal, 10)?;
50    terminal.remove_live("thinking")?;
51    terminal.insert_live("assistant", live_padded(assistant_message("Mural keeps")?))?;
52    render_frames(&mut terminal, 3)?;
53
54    // Identified blocks can be mutated between renders. This simulates a
55    // streaming assistant response over several visible frames. The pinned
56    // status spinner advances on every render while it is visible.
57    for content in [
58        "Mural keeps",
59        "Mural keeps a live conversation",
60        "Mural keeps a live conversation region plus pinned input",
61        "Mural keeps a live conversation region plus pinned input/status UI in a normal terminal buffer.",
62    ] {
63        *terminal
64            .live_block_mut::<Padding<ListItem>>("assistant")?
65            .content_mut() = assistant_message(content)?;
66        render_frames(&mut terminal, 5)?;
67    }
68    hide_status(&mut terminal)?;
69    render_frames(&mut terminal, 6)?;
70
71    // Type a second message, move the cursor left, insert a missing word, then
72    // submit. The submitted value is appended to the live transcript.
73    type_into_input(&mut terminal, "what happens if terminal changes size?")?;
74    move_input_left(&mut terminal, "terminal changes size?".chars().count())?;
75    type_into_input(&mut terminal, "the ")?;
76    render_frames(&mut terminal, 6)?;
77
78    let submitted = submit_input(&mut terminal)?;
79    show_status(&mut terminal, "adapting to resize")?;
80    terminal.insert_live(
81        "thinking-resize",
82        live_padded(thinking_message("checking terminal size…")?),
83    )?;
84    render_frames(&mut terminal, 8)?;
85
86    terminal.resize(Size::new(48, 12))?;
87    *terminal
88        .live_block_mut::<Padding<ListItem>>("thinking-resize")?
89        .content_mut() = thinking_message("reflowing the conversation for 48 columns…")?;
90    render_frames(&mut terminal, 10)?;
91    terminal.remove_live("thinking-resize")?;
92    terminal.insert_live(
93        "assistant-resize",
94        live_padded(assistant_message(format!(
95            "For `{submitted}`, the caller notifies Mural about the new size, and the next render performs a full redraw at the new safe width."
96        ))?),
97    )?;
98    render_frames(&mut terminal, 12)?;
99    hide_status(&mut terminal)?;
100    render_frames(&mut terminal, 6)?;
101
102    // finish() removes pinned UI, leaves live transcript text behind, restores
103    // the cursor, and flushes the backend.
104    terminal.finish()?;
105    println!("\nfinished: pinned status/input were cleaned up; live transcript remains above.");
106
107    Ok(())
108}
Source

pub fn insert_pinned( &mut self, id: impl Into<String>, block: impl Render + 'static, ) -> Result<(), TerminalError>

Appends an identified pinned block.

Identified pinned blocks can later be mutated with Terminal::pinned_block_mut or removed with Terminal::remove_pinned.

Examples found in repository?
examples/conversation.rs (line 22)
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11    // Run this example manually to watch Mural update a conversation in the
12    // terminal's normal buffer:
13    //
14    //     cargo run --example conversation
15    let mut terminal = Terminal::stdout()?;
16
17    // Live blocks are the transcript. Pinned blocks render after live blocks and
18    // are useful for transient status and input UI. The status block starts
19    // hidden, then appears directly above the input separator while the
20    // assistant is answering.
21    terminal.push_pinned(Text::from_plain("")?)?;
22    terminal.insert_pinned("status", pinned(AnswerStatus::hidden()))?;
23    terminal.push_pinned(pinned(
24        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
25    ))?;
26    terminal.insert_pinned(
27        "input",
28        pinned(
29            Textarea::new()
30                .placeholder("type a message…")?
31                .placeholder_style(Style::new().fg(Color::BrightBlack).dim())
32                .max_height(3),
33        ),
34    )?;
35    terminal.push_pinned(pinned(
36        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
37    ))?;
38    render_frames(&mut terminal, 8)?;
39
40    // The first user message also goes through the textarea: applications own
41    // raw mode and keyboard events; this example mutates the textarea directly
42    // to simulate typing and submitting.
43    type_into_input(&mut terminal, "explain Mural in one sentence")?;
44    render_frames(&mut terminal, 4)?;
45    submit_input(&mut terminal)?;
46
47    show_status(&mut terminal, "answering")?;
48    terminal.insert_live("thinking", live_padded(thinking_message("thinking…")?))?;
49    render_frames(&mut terminal, 10)?;
50    terminal.remove_live("thinking")?;
51    terminal.insert_live("assistant", live_padded(assistant_message("Mural keeps")?))?;
52    render_frames(&mut terminal, 3)?;
53
54    // Identified blocks can be mutated between renders. This simulates a
55    // streaming assistant response over several visible frames. The pinned
56    // status spinner advances on every render while it is visible.
57    for content in [
58        "Mural keeps",
59        "Mural keeps a live conversation",
60        "Mural keeps a live conversation region plus pinned input",
61        "Mural keeps a live conversation region plus pinned input/status UI in a normal terminal buffer.",
62    ] {
63        *terminal
64            .live_block_mut::<Padding<ListItem>>("assistant")?
65            .content_mut() = assistant_message(content)?;
66        render_frames(&mut terminal, 5)?;
67    }
68    hide_status(&mut terminal)?;
69    render_frames(&mut terminal, 6)?;
70
71    // Type a second message, move the cursor left, insert a missing word, then
72    // submit. The submitted value is appended to the live transcript.
73    type_into_input(&mut terminal, "what happens if terminal changes size?")?;
74    move_input_left(&mut terminal, "terminal changes size?".chars().count())?;
75    type_into_input(&mut terminal, "the ")?;
76    render_frames(&mut terminal, 6)?;
77
78    let submitted = submit_input(&mut terminal)?;
79    show_status(&mut terminal, "adapting to resize")?;
80    terminal.insert_live(
81        "thinking-resize",
82        live_padded(thinking_message("checking terminal size…")?),
83    )?;
84    render_frames(&mut terminal, 8)?;
85
86    terminal.resize(Size::new(48, 12))?;
87    *terminal
88        .live_block_mut::<Padding<ListItem>>("thinking-resize")?
89        .content_mut() = thinking_message("reflowing the conversation for 48 columns…")?;
90    render_frames(&mut terminal, 10)?;
91    terminal.remove_live("thinking-resize")?;
92    terminal.insert_live(
93        "assistant-resize",
94        live_padded(assistant_message(format!(
95            "For `{submitted}`, the caller notifies Mural about the new size, and the next render performs a full redraw at the new safe width."
96        ))?),
97    )?;
98    render_frames(&mut terminal, 12)?;
99    hide_status(&mut terminal)?;
100    render_frames(&mut terminal, 6)?;
101
102    // finish() removes pinned UI, leaves live transcript text behind, restores
103    // the cursor, and flushes the backend.
104    terminal.finish()?;
105    println!("\nfinished: pinned status/input were cleaned up; live transcript remains above.");
106
107    Ok(())
108}
Source

pub fn live_block_mut<T: 'static>( &mut self, id: &str, ) -> Result<&mut T, TerminalError>

Returns typed mutable access to an identified live block.

Successful mutable access marks the block dirty so the next render will rerender it.

Examples found in repository?
examples/conversation.rs (line 64)
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11    // Run this example manually to watch Mural update a conversation in the
12    // terminal's normal buffer:
13    //
14    //     cargo run --example conversation
15    let mut terminal = Terminal::stdout()?;
16
17    // Live blocks are the transcript. Pinned blocks render after live blocks and
18    // are useful for transient status and input UI. The status block starts
19    // hidden, then appears directly above the input separator while the
20    // assistant is answering.
21    terminal.push_pinned(Text::from_plain("")?)?;
22    terminal.insert_pinned("status", pinned(AnswerStatus::hidden()))?;
23    terminal.push_pinned(pinned(
24        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
25    ))?;
26    terminal.insert_pinned(
27        "input",
28        pinned(
29            Textarea::new()
30                .placeholder("type a message…")?
31                .placeholder_style(Style::new().fg(Color::BrightBlack).dim())
32                .max_height(3),
33        ),
34    )?;
35    terminal.push_pinned(pinned(
36        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
37    ))?;
38    render_frames(&mut terminal, 8)?;
39
40    // The first user message also goes through the textarea: applications own
41    // raw mode and keyboard events; this example mutates the textarea directly
42    // to simulate typing and submitting.
43    type_into_input(&mut terminal, "explain Mural in one sentence")?;
44    render_frames(&mut terminal, 4)?;
45    submit_input(&mut terminal)?;
46
47    show_status(&mut terminal, "answering")?;
48    terminal.insert_live("thinking", live_padded(thinking_message("thinking…")?))?;
49    render_frames(&mut terminal, 10)?;
50    terminal.remove_live("thinking")?;
51    terminal.insert_live("assistant", live_padded(assistant_message("Mural keeps")?))?;
52    render_frames(&mut terminal, 3)?;
53
54    // Identified blocks can be mutated between renders. This simulates a
55    // streaming assistant response over several visible frames. The pinned
56    // status spinner advances on every render while it is visible.
57    for content in [
58        "Mural keeps",
59        "Mural keeps a live conversation",
60        "Mural keeps a live conversation region plus pinned input",
61        "Mural keeps a live conversation region plus pinned input/status UI in a normal terminal buffer.",
62    ] {
63        *terminal
64            .live_block_mut::<Padding<ListItem>>("assistant")?
65            .content_mut() = assistant_message(content)?;
66        render_frames(&mut terminal, 5)?;
67    }
68    hide_status(&mut terminal)?;
69    render_frames(&mut terminal, 6)?;
70
71    // Type a second message, move the cursor left, insert a missing word, then
72    // submit. The submitted value is appended to the live transcript.
73    type_into_input(&mut terminal, "what happens if terminal changes size?")?;
74    move_input_left(&mut terminal, "terminal changes size?".chars().count())?;
75    type_into_input(&mut terminal, "the ")?;
76    render_frames(&mut terminal, 6)?;
77
78    let submitted = submit_input(&mut terminal)?;
79    show_status(&mut terminal, "adapting to resize")?;
80    terminal.insert_live(
81        "thinking-resize",
82        live_padded(thinking_message("checking terminal size…")?),
83    )?;
84    render_frames(&mut terminal, 8)?;
85
86    terminal.resize(Size::new(48, 12))?;
87    *terminal
88        .live_block_mut::<Padding<ListItem>>("thinking-resize")?
89        .content_mut() = thinking_message("reflowing the conversation for 48 columns…")?;
90    render_frames(&mut terminal, 10)?;
91    terminal.remove_live("thinking-resize")?;
92    terminal.insert_live(
93        "assistant-resize",
94        live_padded(assistant_message(format!(
95            "For `{submitted}`, the caller notifies Mural about the new size, and the next render performs a full redraw at the new safe width."
96        ))?),
97    )?;
98    render_frames(&mut terminal, 12)?;
99    hide_status(&mut terminal)?;
100    render_frames(&mut terminal, 6)?;
101
102    // finish() removes pinned UI, leaves live transcript text behind, restores
103    // the cursor, and flushes the backend.
104    terminal.finish()?;
105    println!("\nfinished: pinned status/input were cleaned up; live transcript remains above.");
106
107    Ok(())
108}
Source

pub fn pinned_block_mut<T: 'static>( &mut self, id: &str, ) -> Result<&mut T, TerminalError>

Returns typed mutable access to an identified pinned block.

Successful mutable access marks the block dirty so the next render will rerender it.

Examples found in repository?
examples/conversation.rs (line 212)
206fn type_into_input<B: mural::Backend>(
207    terminal: &mut Terminal<B>,
208    content: &str,
209) -> Result<(), Box<dyn std::error::Error>> {
210    for ch in content.chars() {
211        terminal
212            .pinned_block_mut::<Padding<Textarea>>("input")?
213            .content_mut()
214            .insert_char(ch);
215        render_frame(terminal)?;
216    }
217    Ok(())
218}
219
220fn move_input_left<B: mural::Backend>(
221    terminal: &mut Terminal<B>,
222    steps: usize,
223) -> Result<(), Box<dyn std::error::Error>> {
224    for _ in 0..steps {
225        terminal
226            .pinned_block_mut::<Padding<Textarea>>("input")?
227            .content_mut()
228            .move_left();
229        render_frame(terminal)?;
230    }
231    Ok(())
232}
233
234fn submit_input<B: mural::Backend>(
235    terminal: &mut Terminal<B>,
236) -> Result<String, Box<dyn std::error::Error>> {
237    let submitted = {
238        terminal
239            .pinned_block_mut::<Padding<Textarea>>("input")?
240            .content_mut()
241            .take()
242    };
243
244    terminal.push_live(live_padded(user_message(&submitted)?))?;
245    render_frame(terminal)?;
246    Ok(submitted)
247}
248
249fn show_status<B: mural::Backend>(
250    terminal: &mut Terminal<B>,
251    content: &str,
252) -> Result<(), Box<dyn std::error::Error>> {
253    terminal
254        .pinned_block_mut::<Padding<AnswerStatus>>("status")?
255        .content_mut()
256        .show(content)?;
257    Ok(())
258}
259
260fn hide_status<B: mural::Backend>(
261    terminal: &mut Terminal<B>,
262) -> Result<(), Box<dyn std::error::Error>> {
263    terminal
264        .pinned_block_mut::<Padding<AnswerStatus>>("status")?
265        .content_mut()
266        .hide();
267    Ok(())
268}
Source

pub fn remove_live(&mut self, id: &str) -> Result<(), TerminalError>

Removes an identified live block.

Examples found in repository?
examples/conversation.rs (line 50)
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11    // Run this example manually to watch Mural update a conversation in the
12    // terminal's normal buffer:
13    //
14    //     cargo run --example conversation
15    let mut terminal = Terminal::stdout()?;
16
17    // Live blocks are the transcript. Pinned blocks render after live blocks and
18    // are useful for transient status and input UI. The status block starts
19    // hidden, then appears directly above the input separator while the
20    // assistant is answering.
21    terminal.push_pinned(Text::from_plain("")?)?;
22    terminal.insert_pinned("status", pinned(AnswerStatus::hidden()))?;
23    terminal.push_pinned(pinned(
24        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
25    ))?;
26    terminal.insert_pinned(
27        "input",
28        pinned(
29            Textarea::new()
30                .placeholder("type a message…")?
31                .placeholder_style(Style::new().fg(Color::BrightBlack).dim())
32                .max_height(3),
33        ),
34    )?;
35    terminal.push_pinned(pinned(
36        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
37    ))?;
38    render_frames(&mut terminal, 8)?;
39
40    // The first user message also goes through the textarea: applications own
41    // raw mode and keyboard events; this example mutates the textarea directly
42    // to simulate typing and submitting.
43    type_into_input(&mut terminal, "explain Mural in one sentence")?;
44    render_frames(&mut terminal, 4)?;
45    submit_input(&mut terminal)?;
46
47    show_status(&mut terminal, "answering")?;
48    terminal.insert_live("thinking", live_padded(thinking_message("thinking…")?))?;
49    render_frames(&mut terminal, 10)?;
50    terminal.remove_live("thinking")?;
51    terminal.insert_live("assistant", live_padded(assistant_message("Mural keeps")?))?;
52    render_frames(&mut terminal, 3)?;
53
54    // Identified blocks can be mutated between renders. This simulates a
55    // streaming assistant response over several visible frames. The pinned
56    // status spinner advances on every render while it is visible.
57    for content in [
58        "Mural keeps",
59        "Mural keeps a live conversation",
60        "Mural keeps a live conversation region plus pinned input",
61        "Mural keeps a live conversation region plus pinned input/status UI in a normal terminal buffer.",
62    ] {
63        *terminal
64            .live_block_mut::<Padding<ListItem>>("assistant")?
65            .content_mut() = assistant_message(content)?;
66        render_frames(&mut terminal, 5)?;
67    }
68    hide_status(&mut terminal)?;
69    render_frames(&mut terminal, 6)?;
70
71    // Type a second message, move the cursor left, insert a missing word, then
72    // submit. The submitted value is appended to the live transcript.
73    type_into_input(&mut terminal, "what happens if terminal changes size?")?;
74    move_input_left(&mut terminal, "terminal changes size?".chars().count())?;
75    type_into_input(&mut terminal, "the ")?;
76    render_frames(&mut terminal, 6)?;
77
78    let submitted = submit_input(&mut terminal)?;
79    show_status(&mut terminal, "adapting to resize")?;
80    terminal.insert_live(
81        "thinking-resize",
82        live_padded(thinking_message("checking terminal size…")?),
83    )?;
84    render_frames(&mut terminal, 8)?;
85
86    terminal.resize(Size::new(48, 12))?;
87    *terminal
88        .live_block_mut::<Padding<ListItem>>("thinking-resize")?
89        .content_mut() = thinking_message("reflowing the conversation for 48 columns…")?;
90    render_frames(&mut terminal, 10)?;
91    terminal.remove_live("thinking-resize")?;
92    terminal.insert_live(
93        "assistant-resize",
94        live_padded(assistant_message(format!(
95            "For `{submitted}`, the caller notifies Mural about the new size, and the next render performs a full redraw at the new safe width."
96        ))?),
97    )?;
98    render_frames(&mut terminal, 12)?;
99    hide_status(&mut terminal)?;
100    render_frames(&mut terminal, 6)?;
101
102    // finish() removes pinned UI, leaves live transcript text behind, restores
103    // the cursor, and flushes the backend.
104    terminal.finish()?;
105    println!("\nfinished: pinned status/input were cleaned up; live transcript remains above.");
106
107    Ok(())
108}
Source

pub fn remove_pinned(&mut self, id: &str) -> Result<(), TerminalError>

Removes an identified pinned block.

Source

pub fn is_block_dirty(&self, id: &str) -> Result<bool, TerminalError>

Reports whether an identified block is dirty and will be rerendered.

Source

pub fn force_full_redraw(&mut self) -> Result<()>

Forces the next render to recover by rewriting the full managed screen buffer.

Use this after direct writes through Terminal::backend_mut or after any external terminal interaction that may have invalidated the renderer’s cached screen snapshot.

Source

pub fn resize(&mut self, size: Size) -> Result<()>

Notifies the terminal about a caller-observed resize.

The notification performs no I/O immediately. The next render uses the new safe printable width and performs a full redraw.

Examples found in repository?
examples/conversation.rs (line 86)
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11    // Run this example manually to watch Mural update a conversation in the
12    // terminal's normal buffer:
13    //
14    //     cargo run --example conversation
15    let mut terminal = Terminal::stdout()?;
16
17    // Live blocks are the transcript. Pinned blocks render after live blocks and
18    // are useful for transient status and input UI. The status block starts
19    // hidden, then appears directly above the input separator while the
20    // assistant is answering.
21    terminal.push_pinned(Text::from_plain("")?)?;
22    terminal.insert_pinned("status", pinned(AnswerStatus::hidden()))?;
23    terminal.push_pinned(pinned(
24        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
25    ))?;
26    terminal.insert_pinned(
27        "input",
28        pinned(
29            Textarea::new()
30                .placeholder("type a message…")?
31                .placeholder_style(Style::new().fg(Color::BrightBlack).dim())
32                .max_height(3),
33        ),
34    )?;
35    terminal.push_pinned(pinned(
36        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
37    ))?;
38    render_frames(&mut terminal, 8)?;
39
40    // The first user message also goes through the textarea: applications own
41    // raw mode and keyboard events; this example mutates the textarea directly
42    // to simulate typing and submitting.
43    type_into_input(&mut terminal, "explain Mural in one sentence")?;
44    render_frames(&mut terminal, 4)?;
45    submit_input(&mut terminal)?;
46
47    show_status(&mut terminal, "answering")?;
48    terminal.insert_live("thinking", live_padded(thinking_message("thinking…")?))?;
49    render_frames(&mut terminal, 10)?;
50    terminal.remove_live("thinking")?;
51    terminal.insert_live("assistant", live_padded(assistant_message("Mural keeps")?))?;
52    render_frames(&mut terminal, 3)?;
53
54    // Identified blocks can be mutated between renders. This simulates a
55    // streaming assistant response over several visible frames. The pinned
56    // status spinner advances on every render while it is visible.
57    for content in [
58        "Mural keeps",
59        "Mural keeps a live conversation",
60        "Mural keeps a live conversation region plus pinned input",
61        "Mural keeps a live conversation region plus pinned input/status UI in a normal terminal buffer.",
62    ] {
63        *terminal
64            .live_block_mut::<Padding<ListItem>>("assistant")?
65            .content_mut() = assistant_message(content)?;
66        render_frames(&mut terminal, 5)?;
67    }
68    hide_status(&mut terminal)?;
69    render_frames(&mut terminal, 6)?;
70
71    // Type a second message, move the cursor left, insert a missing word, then
72    // submit. The submitted value is appended to the live transcript.
73    type_into_input(&mut terminal, "what happens if terminal changes size?")?;
74    move_input_left(&mut terminal, "terminal changes size?".chars().count())?;
75    type_into_input(&mut terminal, "the ")?;
76    render_frames(&mut terminal, 6)?;
77
78    let submitted = submit_input(&mut terminal)?;
79    show_status(&mut terminal, "adapting to resize")?;
80    terminal.insert_live(
81        "thinking-resize",
82        live_padded(thinking_message("checking terminal size…")?),
83    )?;
84    render_frames(&mut terminal, 8)?;
85
86    terminal.resize(Size::new(48, 12))?;
87    *terminal
88        .live_block_mut::<Padding<ListItem>>("thinking-resize")?
89        .content_mut() = thinking_message("reflowing the conversation for 48 columns…")?;
90    render_frames(&mut terminal, 10)?;
91    terminal.remove_live("thinking-resize")?;
92    terminal.insert_live(
93        "assistant-resize",
94        live_padded(assistant_message(format!(
95            "For `{submitted}`, the caller notifies Mural about the new size, and the next render performs a full redraw at the new safe width."
96        ))?),
97    )?;
98    render_frames(&mut terminal, 12)?;
99    hide_status(&mut terminal)?;
100    render_frames(&mut terminal, 6)?;
101
102    // finish() removes pinned UI, leaves live transcript text behind, restores
103    // the cursor, and flushes the backend.
104    terminal.finish()?;
105    println!("\nfinished: pinned status/input were cleaned up; live transcript remains above.");
106
107    Ok(())
108}
Source

pub fn render(&mut self) -> Result<()>

Renders dirty or changed live and pinned blocks to the backend.

Mural writes the smallest safe suffix when possible and falls back to a full rewrite for recovery, resize, or changes above the visible viewport.

Examples found in repository?
examples/conversation.rs (line 191)
190fn render_frame<B: mural::Backend>(terminal: &mut Terminal<B>) -> std::io::Result<()> {
191    terminal.render()?;
192    thread::sleep(FRAME_DELAY);
193    Ok(())
194}
Source

pub fn finish(&mut self) -> Result<()>

Finishes live rendering and restores terminal state.

Finish renders a final live-only frame, removes pinned UI with the same diff/recovery algorithm used by Terminal::render, moves to a fresh prompt line, shows the cursor, and flushes. After finish, rendering and mutation APIs return lifecycle errors.

Examples found in repository?
examples/conversation.rs (line 104)
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11    // Run this example manually to watch Mural update a conversation in the
12    // terminal's normal buffer:
13    //
14    //     cargo run --example conversation
15    let mut terminal = Terminal::stdout()?;
16
17    // Live blocks are the transcript. Pinned blocks render after live blocks and
18    // are useful for transient status and input UI. The status block starts
19    // hidden, then appears directly above the input separator while the
20    // assistant is answering.
21    terminal.push_pinned(Text::from_plain("")?)?;
22    terminal.insert_pinned("status", pinned(AnswerStatus::hidden()))?;
23    terminal.push_pinned(pinned(
24        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
25    ))?;
26    terminal.insert_pinned(
27        "input",
28        pinned(
29            Textarea::new()
30                .placeholder("type a message…")?
31                .placeholder_style(Style::new().fg(Color::BrightBlack).dim())
32                .max_height(3),
33        ),
34    )?;
35    terminal.push_pinned(pinned(
36        Hr::new().style(Style::new().fg(Color::BrightBlack).dim()),
37    ))?;
38    render_frames(&mut terminal, 8)?;
39
40    // The first user message also goes through the textarea: applications own
41    // raw mode and keyboard events; this example mutates the textarea directly
42    // to simulate typing and submitting.
43    type_into_input(&mut terminal, "explain Mural in one sentence")?;
44    render_frames(&mut terminal, 4)?;
45    submit_input(&mut terminal)?;
46
47    show_status(&mut terminal, "answering")?;
48    terminal.insert_live("thinking", live_padded(thinking_message("thinking…")?))?;
49    render_frames(&mut terminal, 10)?;
50    terminal.remove_live("thinking")?;
51    terminal.insert_live("assistant", live_padded(assistant_message("Mural keeps")?))?;
52    render_frames(&mut terminal, 3)?;
53
54    // Identified blocks can be mutated between renders. This simulates a
55    // streaming assistant response over several visible frames. The pinned
56    // status spinner advances on every render while it is visible.
57    for content in [
58        "Mural keeps",
59        "Mural keeps a live conversation",
60        "Mural keeps a live conversation region plus pinned input",
61        "Mural keeps a live conversation region plus pinned input/status UI in a normal terminal buffer.",
62    ] {
63        *terminal
64            .live_block_mut::<Padding<ListItem>>("assistant")?
65            .content_mut() = assistant_message(content)?;
66        render_frames(&mut terminal, 5)?;
67    }
68    hide_status(&mut terminal)?;
69    render_frames(&mut terminal, 6)?;
70
71    // Type a second message, move the cursor left, insert a missing word, then
72    // submit. The submitted value is appended to the live transcript.
73    type_into_input(&mut terminal, "what happens if terminal changes size?")?;
74    move_input_left(&mut terminal, "terminal changes size?".chars().count())?;
75    type_into_input(&mut terminal, "the ")?;
76    render_frames(&mut terminal, 6)?;
77
78    let submitted = submit_input(&mut terminal)?;
79    show_status(&mut terminal, "adapting to resize")?;
80    terminal.insert_live(
81        "thinking-resize",
82        live_padded(thinking_message("checking terminal size…")?),
83    )?;
84    render_frames(&mut terminal, 8)?;
85
86    terminal.resize(Size::new(48, 12))?;
87    *terminal
88        .live_block_mut::<Padding<ListItem>>("thinking-resize")?
89        .content_mut() = thinking_message("reflowing the conversation for 48 columns…")?;
90    render_frames(&mut terminal, 10)?;
91    terminal.remove_live("thinking-resize")?;
92    terminal.insert_live(
93        "assistant-resize",
94        live_padded(assistant_message(format!(
95            "For `{submitted}`, the caller notifies Mural about the new size, and the next render performs a full redraw at the new safe width."
96        ))?),
97    )?;
98    render_frames(&mut terminal, 12)?;
99    hide_status(&mut terminal)?;
100    render_frames(&mut terminal, 6)?;
101
102    // finish() removes pinned UI, leaves live transcript text behind, restores
103    // the cursor, and flushes the backend.
104    terminal.finish()?;
105    println!("\nfinished: pinned status/input were cleaned up; live transcript remains above.");
106
107    Ok(())
108}

Trait Implementations§

Source§

impl<B: Backend> Drop for Terminal<B>

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

§

impl<B> Freeze for Terminal<B>
where B: Freeze,

§

impl<B> !RefUnwindSafe for Terminal<B>

§

impl<B> !Send for Terminal<B>

§

impl<B> !Sync for Terminal<B>

§

impl<B> Unpin for Terminal<B>
where B: Unpin,

§

impl<B> UnsafeUnpin for Terminal<B>
where B: UnsafeUnpin,

§

impl<B> !UnwindSafe for Terminal<B>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.