1use 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 let port = port!(cx, TaskProgress, TaskCommand);
51
52 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 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 match rx.recv() {
199 Ok(TaskCommand::Start) => {}
200 Ok(TaskCommand::Cancel) => continue,
201 Err(_) => return, }
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 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}