Skip to main content

39_port/
39_port.rs

1//! Example 39: Port — Bidirectional Background Task Runner
2//!
3//! Demonstrates port! for bidirectional communication between a
4//! background worker thread and the UI. Send commands, receive progress.
5//!
6//! Run with: `cargo run -p telex-tui --example 39_port`
7
8use crossterm::event::KeyCode;
9use crossterm::style::Color;
10use std::sync::mpsc;
11use telex::prelude::*;
12
13telex::require_api!(0, 2);
14
15fn main() {
16    telex::run(App).unwrap();
17}
18
19#[derive(Clone, Debug)]
20enum TaskProgress {
21    Started,
22    Progress(u8),
23    Done(String),
24    Cancelled,
25}
26
27#[derive(Clone, Debug)]
28enum TaskCommand {
29    Start,
30    Cancel,
31}
32
33struct App;
34
35impl Component for App {
36    fn render(&self, cx: Scope) -> View {
37        let show_help = state!(cx, || false);
38
39        cx.use_command(
40            KeyBinding::key(KeyCode::F(1)),
41            with!(show_help => move || show_help.update(|v| *v = !*v)),
42        );
43
44        let status = state!(cx, || "Idle".to_string());
45        let progress = state!(cx, || 0u8);
46        let result: State<Option<String>> = state!(cx, || None);
47        let running = state!(cx, || false);
48
49        // Bidirectional port: inbound TaskProgress, outbound TaskCommand
50        let port = port!(cx, TaskProgress, TaskCommand);
51
52        // Process incoming progress messages
53        for msg in port.rx.get() {
54            match msg {
55                TaskProgress::Started => {
56                    status.set("Working...".to_string());
57                    progress.set(0);
58                    result.set(None);
59                    running.set(true);
60                }
61                TaskProgress::Progress(pct) => {
62                    status.set(format!("Progress: {}%", pct));
63                    progress.set(pct);
64                }
65                TaskProgress::Done(data) => {
66                    status.set("Done!".to_string());
67                    progress.set(100);
68                    result.set(Some(data));
69                    running.set(false);
70                }
71                TaskProgress::Cancelled => {
72                    status.set("Cancelled".to_string());
73                    running.set(false);
74                }
75            }
76        }
77
78        // Spawn the worker thread on first render
79        let worker_started = state!(cx, || false);
80        if !worker_started.get() {
81            worker_started.set(true);
82            let tx_progress = port.rx.tx();
83            if let Some(rx_commands) = port.take_outbound_rx() {
84                std::thread::spawn(move || {
85                    worker_loop(tx_progress, rx_commands);
86                });
87            }
88        }
89
90        let start_task = {
91            let tx = port.tx();
92            with!(running => move || {
93                if !running.get() {
94                    let _ = tx.send(TaskCommand::Start);
95                }
96            })
97        };
98
99        let cancel_task = {
100            let tx = port.tx();
101            with!(running => move || {
102                if running.get() {
103                    let _ = tx.send(TaskCommand::Cancel);
104                }
105            })
106        };
107
108        let pct = progress.get();
109        let bar_width = 30usize;
110        let filled = (pct as usize * bar_width) / 100;
111        let bar = format!(
112            "[{}{}] {}%",
113            "█".repeat(filled),
114            "░".repeat(bar_width - filled),
115            pct
116        );
117
118        View::vstack()
119            .spacing(1)
120            .child(View::styled_text("Port: Background Task Runner").bold().build())
121            .child(
122                View::hstack()
123                    .spacing(1)
124                    .child(View::styled_text("Status:").dim().build())
125                    .child(View::styled_text(status.get())
126                        .color(if running.get() { Color::Yellow } else if pct == 100 { Color::Green } else { Color::Reset })
127                        .bold()
128                        .build())
129                    .build(),
130            )
131            .child(View::styled_text(&bar).color(
132                if pct == 100 { Color::Green }
133                else if pct > 50 { Color::Yellow }
134                else { Color::Cyan }
135            ).build())
136            .child(if let Some(data) = result.get() {
137                View::vstack()
138                    .child(View::styled_text("Result:").dim().build())
139                    .child(View::styled_text(format!("  {}", data)).color(Color::Green).build())
140                    .build()
141            } else {
142                View::empty()
143            })
144            .child(
145                View::hstack()
146                    .spacing(1)
147                    .child(
148                        View::button()
149                            .label(if running.get() { "[ Running... ]" } else { "[ Start Task ]" })
150                            .on_press(start_task)
151                            .build(),
152                    )
153                    .child(
154                        View::button()
155                            .label("[ Cancel ]")
156                            .on_press(cancel_task)
157                            .build(),
158                    )
159                    .build(),
160            )
161            .child(View::styled_text("F1 help • Ctrl+Q quit").dim().build())
162            .child(
163                View::modal()
164                    .visible(show_help.get())
165                    .title("Example 39: Port")
166                    .on_dismiss(with!(show_help => move || show_help.set(false)))
167                    .child(
168                        View::vstack()
169                            .child(View::styled_text("What you're seeing").bold().build())
170                            .child(View::text("• Background task with progress"))
171                            .child(View::text("• Bidirectional communication"))
172                            .child(View::text("• Start and cancel controls"))
173                            .child(View::gap(1))
174                            .child(View::styled_text("Key concepts").bold().build())
175                            .child(View::text("• port!(cx, InType, OutType)"))
176                            .child(View::text("• port.rx.tx() sends to UI"))
177                            .child(View::text("• port.tx() sends to worker"))
178                            .child(View::text("• port.take_outbound_rx() for worker"))
179                            .child(View::text("• port.rx.get() reads this frame"))
180                            .child(View::gap(1))
181                            .child(View::styled_text("Try this").bold().build())
182                            .child(View::text("• Start a task, watch progress"))
183                            .child(View::text("• Cancel mid-way"))
184                            .child(View::text("• Start another after completion"))
185                            .child(View::gap(1))
186                            .child(View::styled_text("Press Escape to close").dim().build())
187                            .build(),
188                    )
189                    .build(),
190            )
191            .build()
192    }
193}
194
195fn worker_loop(tx: telex::WakingSender<TaskProgress>, rx: mpsc::Receiver<TaskCommand>) {
196    loop {
197        // Wait for a Start command
198        match rx.recv() {
199            Ok(TaskCommand::Start) => {}
200            Ok(TaskCommand::Cancel) => continue,
201            Err(_) => return, // UI dropped
202        }
203
204        tx.send(TaskProgress::Started).ok();
205
206        let mut cancelled = false;
207        for i in 1..=20 {
208            std::thread::sleep(std::time::Duration::from_millis(150));
209
210            // Check for cancel
211            match rx.try_recv() {
212                Ok(TaskCommand::Cancel) => {
213                    tx.send(TaskProgress::Cancelled).ok();
214                    cancelled = true;
215                    break;
216                }
217                _ => {}
218            }
219
220            tx.send(TaskProgress::Progress((i * 5) as u8)).ok();
221        }
222
223        if !cancelled {
224            tx.send(TaskProgress::Done("Computed 42 widgets successfully!".to_string())).ok();
225        }
226    }
227}