Skip to main content

ansiq_runtime/
run.rs

1use std::io;
2use std::time::Duration;
3
4use ansiq_core::Rect;
5use ansiq_render::{
6    FrameBuffer, diff_buffers, diff_buffers_in_regions, frame_patches, render_cursor_at_origin,
7    render_patches_at_origin,
8};
9use ansiq_surface::{InputEvent, InputEventStream, TerminalSession, Viewport, ViewportPolicy};
10
11use crate::{App, Engine, cursor_position, draw_tree, draw_tree_in_regions};
12
13pub fn viewport_bounds(viewport: Viewport) -> Rect {
14    Rect::new(0, 0, viewport.width, viewport.height)
15}
16
17pub fn exit_row_for_content(viewport: Viewport, required_height: u16) -> u16 {
18    let used_height = required_height.clamp(1, viewport.height);
19    viewport
20        .origin_y
21        .saturating_add(used_height.saturating_sub(1))
22}
23
24pub async fn run_app<A: App>(app: A) -> io::Result<()> {
25    run_app_with_policy(app, ViewportPolicy::PreserveVisible).await
26}
27
28pub async fn run_app_with_policy<A: App>(app: A, policy: ViewportPolicy) -> io::Result<()> {
29    let mut terminal = TerminalSession::enter(policy)?;
30    let mut viewport = terminal.viewport();
31    let mut input_stream = InputEventStream::default();
32
33    let mut engine = Engine::new(app);
34    engine.mount();
35    engine.set_bounds(viewport_bounds(viewport));
36    engine.render_tree();
37    if flush_committed_history(&mut terminal, &mut viewport, policy, &mut engine)? {
38        engine.render_tree();
39    }
40    sync_viewport_to_content(&mut terminal, &mut viewport, policy, &mut engine)?;
41
42    let mut previous = FrameBuffer::new(viewport.width, viewport.height);
43    render_current_frame(&mut terminal, &engine, viewport.origin_y, &mut previous)?;
44
45    loop {
46        if engine.drain_requests() {
47            break;
48        }
49
50        if let Some(event) = input_stream.next_event(Duration::from_millis(16)).await? {
51            match event {
52                InputEvent::Resize(width, height) => {
53                    viewport = terminal.resize(policy, (width, height));
54                    engine.set_bounds(viewport_bounds(viewport));
55                    previous = FrameBuffer::new(viewport.width, viewport.height);
56                }
57                InputEvent::Key(key) => {
58                    if engine.handle_input(key) {
59                        break;
60                    }
61                }
62            }
63        }
64
65        if engine.drain_requests() {
66            break;
67        }
68
69        if engine.is_dirty() {
70            engine.render_tree();
71            if flush_committed_history(&mut terminal, &mut viewport, policy, &mut engine)? {
72                previous = FrameBuffer::new(viewport.width, viewport.height);
73                engine.render_tree();
74            }
75            if sync_viewport_to_content(&mut terminal, &mut viewport, policy, &mut engine)? {
76                previous = FrameBuffer::new(viewport.width, viewport.height);
77            }
78            render_current_frame(&mut terminal, &engine, viewport.origin_y, &mut previous)?;
79        }
80
81        tokio::task::yield_now().await;
82    }
83
84    Ok(())
85}
86
87fn flush_committed_history<A: App>(
88    terminal: &mut TerminalSession,
89    viewport: &mut Viewport,
90    policy: ViewportPolicy,
91    engine: &mut Engine<A>,
92) -> io::Result<bool> {
93    let history = engine.take_pending_history();
94    if history.is_empty() {
95        return Ok(false);
96    }
97
98    // Completed turns become terminal scrollback instead of inflating the live viewport.
99    *viewport = terminal.commit_history_blocks(history, policy)?;
100    engine.set_bounds(viewport_bounds(*viewport));
101    Ok(true)
102}
103
104fn sync_viewport_to_content<A: App>(
105    terminal: &mut TerminalSession,
106    viewport: &mut Viewport,
107    policy: ViewportPolicy,
108    engine: &mut Engine<A>,
109) -> io::Result<bool> {
110    let Some(requested_height) = policy.requested_height(viewport.height, engine.required_height())
111    else {
112        return Ok(false);
113    };
114
115    // ReservePreferred is the app shell mode: when content naturally grows,
116    // expand the inline viewport so the stacked transcript is not clipped early.
117    terminal.reserve_inline_space(requested_height)?;
118    *viewport = terminal.viewport();
119    engine.set_bounds(viewport_bounds(*viewport));
120    engine.render_tree();
121    Ok(true)
122}
123
124fn render_current_frame<A: App>(
125    terminal: &mut TerminalSession,
126    engine: &Engine<A>,
127    origin_y: u16,
128    previous: &mut FrameBuffer,
129) -> io::Result<()> {
130    let Some(tree) = engine.tree() else {
131        return Ok(());
132    };
133
134    let next = if let Some(regions) = engine.redraw_regions() {
135        let mut next = previous.clone();
136        draw_tree_in_regions(tree, engine.focused(), &mut next, regions);
137        next
138    } else {
139        let mut next = FrameBuffer::new(previous.width(), previous.height());
140        draw_tree(tree, engine.focused(), &mut next);
141        next
142    };
143    let cursor = cursor_position(tree, engine.focused());
144
145    let patches = if let Some(regions) = engine.redraw_regions() {
146        diff_buffers_in_regions(previous, &next, regions)
147    } else if previous.is_blank() {
148        frame_patches(&next)
149    } else {
150        diff_buffers(previous, &next)
151    };
152    let mut bytes = Vec::new();
153    if !patches.is_empty() {
154        render_patches_at_origin(&mut bytes, &patches, origin_y)?;
155    }
156    render_cursor_at_origin(&mut bytes, cursor, origin_y)?;
157    let output = String::from_utf8(bytes).expect("rendered patches should be valid utf-8");
158    terminal.write_ansi(&output)?;
159    terminal.set_exit_row(exit_row_for_content(
160        terminal.viewport(),
161        engine.required_height(),
162    ));
163
164    *previous = next;
165    Ok(())
166}