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>>
impl Terminal<StdoutBackend<Stdout>>
Sourcepub fn stdout() -> Result<Self>
pub fn stdout() -> Result<Self>
Creates a terminal that renders to process stdout.
Examples found in repository?
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>
impl<B: Backend> Terminal<B>
Sourcepub fn backend(&self) -> &B
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.
Sourcepub fn backend_mut(&mut self) -> &mut B
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.
Sourcepub fn push_live(&mut self, block: impl Render + 'static) -> Result<()>
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?
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}Sourcepub fn push_pinned(&mut self, block: impl Render + 'static) -> Result<()>
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?
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}Sourcepub fn insert_live(
&mut self,
id: impl Into<String>,
block: impl Render + 'static,
) -> Result<(), TerminalError>
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?
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}Sourcepub fn insert_pinned(
&mut self,
id: impl Into<String>,
block: impl Render + 'static,
) -> Result<(), TerminalError>
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?
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}Sourcepub fn live_block_mut<T: 'static>(
&mut self,
id: &str,
) -> Result<&mut T, TerminalError>
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?
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}Sourcepub fn pinned_block_mut<T: 'static>(
&mut self,
id: &str,
) -> Result<&mut T, TerminalError>
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?
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}Sourcepub fn remove_live(&mut self, id: &str) -> Result<(), TerminalError>
pub fn remove_live(&mut self, id: &str) -> Result<(), TerminalError>
Removes an identified live block.
Examples found in repository?
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}Sourcepub fn remove_pinned(&mut self, id: &str) -> Result<(), TerminalError>
pub fn remove_pinned(&mut self, id: &str) -> Result<(), TerminalError>
Removes an identified pinned block.
Sourcepub fn is_block_dirty(&self, id: &str) -> Result<bool, TerminalError>
pub fn is_block_dirty(&self, id: &str) -> Result<bool, TerminalError>
Reports whether an identified block is dirty and will be rerendered.
Sourcepub fn force_full_redraw(&mut self) -> Result<()>
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.
Sourcepub fn resize(&mut self, size: Size) -> Result<()>
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?
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}Sourcepub fn render(&mut self) -> Result<()>
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.
Sourcepub fn finish(&mut self) -> Result<()>
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?
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}