1use anyhow::Result;
5use cactui::inline_prompt::{InlineInputOpts, InlinePrompt};
6use crossterm::{event::KeyCode, style::Color};
7
8use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
12struct DemoFrame {
13 content: String,
14}
15
16#[derive(Debug, Clone)]
18struct ChannelState {
19 fg_red: bool,
20 fg_green: bool,
21 fg_blue: bool,
22 bg_red: bool,
23 bg_green: bool,
24 bg_blue: bool,
25 modifiers: bool,
26}
27
28impl ChannelState {
29 fn new() -> Self {
30 Self {
31 fg_red: false,
32 fg_green: false,
33 fg_blue: false,
34 bg_red: false,
35 bg_green: false,
36 bg_blue: false,
37 modifiers: false,
38 }
39 }
40
41 fn format_channels(&self) -> String {
42 if !self.fg_red
43 && !self.fg_green
44 && !self.fg_blue
45 && !self.bg_red
46 && !self.bg_green
47 && !self.bg_blue
48 && !self.modifiers
49 {
50 return "(none)".to_string();
51 }
52
53 let mut result = String::new();
54
55 let mut fg_parts = Vec::new();
57 if self.fg_red {
58 fg_parts.push("R");
59 }
60 if self.fg_green {
61 fg_parts.push("G");
62 }
63 if self.fg_blue {
64 fg_parts.push("B");
65 }
66 result.push_str(&fg_parts.join(""));
67
68 if self.bg_red || self.bg_green || self.bg_blue {
70 result.push('/');
71 let mut bg_parts = Vec::new();
72 if self.bg_red {
73 bg_parts.push("R");
74 }
75 if self.bg_green {
76 bg_parts.push("G");
77 }
78 if self.bg_blue {
79 bg_parts.push("B");
80 }
81 result.push_str(&bg_parts.join(""));
82 }
83
84 if self.modifiers {
86 result.push('+');
87 }
88
89 if result.is_empty() {
90 "(none)".to_string()
91 } else {
92 result
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99struct PlaybackSession {
100 recording_description: Option<String>,
101 frame_descriptions: HashMap<usize, String>, frame_channels: HashMap<usize, ChannelState>, frames: Vec<DemoFrame>,
104 current_frame_num: usize,
105}
106
107impl PlaybackSession {
108 fn new() -> Self {
109 Self {
110 recording_description: None,
111 frame_descriptions: HashMap::new(),
112 frame_channels: HashMap::new(),
113 frames: create_demo_frames(),
114 current_frame_num: 0,
115 }
116 }
117
118 fn get_current_channels(&self) -> &ChannelState {
119 static DEFAULT_CHANNELS: ChannelState = ChannelState {
120 fg_red: false,
121 fg_green: false,
122 fg_blue: false,
123 bg_red: false,
124 bg_green: false,
125 bg_blue: false,
126 modifiers: false,
127 };
128 self.frame_channels
129 .get(&self.current_frame_num)
130 .unwrap_or(&DEFAULT_CHANNELS)
131 }
132
133 fn get_current_channels_mut(&mut self) -> &mut ChannelState {
134 self.frame_channels
135 .entry(self.current_frame_num)
136 .or_insert_with(ChannelState::new)
137 }
138
139 fn collect_recording_description(&mut self) -> Result<()> {
141 println!("Interactive Recording Playback");
142 println!("═══════════════════════════════════");
143 println!();
144
145 let description = InlinePrompt::input("Recording description:", None)?;
146 if !description.is_empty() {
147 self.recording_description = Some(description);
148 }
149
150 Ok(())
151 }
152
153 fn run_interactive_playback(&mut self) -> Result<()> {
155 let collect_descriptions =
156 InlinePrompt::confirm("Collect descriptions for each frame?", true)?;
157
158 if !collect_descriptions {
159 println!("⏭️ Skipping frame descriptions");
160 return Ok(());
161 }
162
163 println!();
164 println!("🎥 Starting frame-by-frame playback...");
165 println!("Use keys to navigate, Enter to continue");
166 println!();
167 println!();
168
169 let inline_menu = create_inline_frame_menu();
170
171 for frame_num in 0..self.frames.len() {
172 self.current_frame_num = frame_num;
174
175 inline_menu.run_menu(self)?;
176
177 println!();
178 println!();
179 }
180
181 println!("✅ Playback complete!");
182 Ok(())
183 }
184}
185
186fn calculate_description_lines(description: &str) -> Result<usize> {
188 let full_text = format!("Description: {}", description);
189 cactui::text_wrapping::calculate_text_lines(&full_text)
190}
191
192fn get_frame_header_lines(session: &PlaybackSession) -> Vec<String> {
194 let mut lines = Vec::new();
195
196 if let Some(current_frame) = session.frames.get(session.current_frame_num) {
198 for line in current_frame.content.lines() {
199 lines.push(line.to_string());
200 }
201
202 let channels = session.get_current_channels();
203
204 if channels.fg_red
206 || channels.fg_green
207 || channels.fg_blue
208 || channels.bg_red
209 || channels.bg_green
210 || channels.bg_blue
211 || channels.modifiers
212 {
213 lines.push("=== FRAME CHANNELS ===".to_string());
214
215 if channels.fg_red {
216 lines.push("--- Foreground Red ---".to_string());
217 lines.push("1│255..............".to_string());
218 lines.push("2│255,128............,255".to_string());
219 lines.push("3│255,128............,255".to_string());
220 lines.push("4│255,128............,255".to_string());
221 lines.push("5│255,128............,255".to_string());
222 lines.push("6│255,128............,255".to_string());
223 lines.push("7│255..............".to_string());
224 lines.push("8│128..............".to_string());
225 }
226
227 if channels.fg_green {
228 lines.push("--- Foreground Green ---".to_string());
229 lines.push("1│0,255,0..............".to_string());
230 lines.push("2│0,255,0..............".to_string());
231 lines.push("3│0,255,0..............".to_string());
232 lines.push("4│0,255,0..............".to_string());
233 lines.push("5│0,255,0..............".to_string());
234 lines.push("6│0,255,0..............".to_string());
235 lines.push("7│0,255,0..............".to_string());
236 lines.push("8│0,255,0..............".to_string());
237 }
238
239 if channels.fg_blue {
240 lines.push("--- Foreground Blue ---".to_string());
241 lines.push("1│0,0,255..............".to_string());
242 lines.push("2│0,0,255..............".to_string());
243 lines.push("3│0,0,255..............".to_string());
244 lines.push("4│0,0,255..............".to_string());
245 lines.push("5│0,0,255..............".to_string());
246 lines.push("6│0,0,255..............".to_string());
247 lines.push("7│0,0,255..............".to_string());
248 lines.push("8│0,0,255..............".to_string());
249 }
250
251 if channels.bg_red {
252 lines.push("--- Background Red ---".to_string());
253 lines.push("1│255,0,0..............".to_string());
254 lines.push("2│255,0,0..............".to_string());
255 lines.push("3│255,0,0..............".to_string());
256 lines.push("4│255,0,0..............".to_string());
257 lines.push("5│255,0,0..............".to_string());
258 lines.push("6│255,0,0..............".to_string());
259 lines.push("7│255,0,0..............".to_string());
260 lines.push("8│255,0,0..............".to_string());
261 }
262
263 if channels.bg_green {
264 lines.push("--- Background Green ---".to_string());
265 lines.push("1│0,255,0..............".to_string());
266 lines.push("2│0,255,0..............".to_string());
267 lines.push("3│0,255,0..............".to_string());
268 lines.push("4│0,255,0..............".to_string());
269 lines.push("5│0,255,0..............".to_string());
270 lines.push("6│0,255,0..............".to_string());
271 lines.push("7│0,255,0..............".to_string());
272 lines.push("8│0,255,0..............".to_string());
273 }
274
275 if channels.bg_blue {
276 lines.push("--- Background Blue ---".to_string());
277 lines.push("1│0..............".to_string());
278 lines.push("2│0..............".to_string());
279 lines.push("3│0..............".to_string());
280 lines.push("4│0..............".to_string());
281 lines.push("5│0..............".to_string());
282 lines.push("6│0..............".to_string());
283 lines.push("7│0..............".to_string());
284 lines.push("8│0..............".to_string());
285 }
286
287 lines.push("=== END FRAME 0 ===".to_string());
288 }
289 }
290
291 lines.push(format!(
293 "Channels: {}",
294 session.get_current_channels().format_channels()
295 ));
296
297 let desc = session
298 .frame_descriptions
299 .get(&session.current_frame_num)
300 .map(|s| s.clone())
301 .unwrap_or_else(|| "(none)".to_string());
302
303 lines.push(format!("Description: {}", desc));
304
305 lines
306}
307
308fn create_inline_frame_menu() -> cactui::KeyMenuConfig<PlaybackSession> {
310 cactui::KeyMenuConfig {
311 header_lines: Some(Box::new(|session: &PlaybackSession| {
312 get_frame_header_lines(session)
313 })),
314 items: vec![
315 cactui::KeyMenuItem {
316 key: KeyCode::Char('e'),
317 description: "edit description".to_string(),
318 color: Some(Color::Cyan),
319 action: cactui::MenuAction::Callback {
320 callback: Box::new(|session, line_counts| {
321 let current_desc = session
323 .frame_descriptions
324 .get(&session.current_frame_num)
325 .map(|s| s.as_str());
326
327 let current_description = session
329 .frame_descriptions
330 .get(&session.current_frame_num)
331 .map(|s| s.as_str())
332 .unwrap_or("(none)");
333 let current_lines_to_clear =
334 calculate_description_lines(current_description)?;
335
336 cactui::inline_keymenu::clear_lines(current_lines_to_clear)?;
338 let mut opts = InlineInputOpts::new().escape_to_exit();
340 if current_desc.is_some() {
341 opts = opts.placeholder(current_desc.unwrap());
342 };
343 let new_desc = InlinePrompt::input("Description:", Some(opts))?;
344 let new_lines_to_clear = calculate_description_lines(&new_desc)?;
346 if new_lines_to_clear > current_lines_to_clear {
347 line_counts.header += new_lines_to_clear - current_lines_to_clear
348 } else if new_lines_to_clear < current_lines_to_clear {
349 line_counts.header = line_counts
350 .header
351 .saturating_sub(current_lines_to_clear - new_lines_to_clear)
352 }
353
354 if !new_desc.is_empty() {
356 session
357 .frame_descriptions
358 .insert(session.current_frame_num, new_desc);
359 } else {
360 session
361 .frame_descriptions
362 .remove(&session.current_frame_num);
363 }
364
365 Ok(cactui::MenuResult::Stay)
366 }),
367 clear_menu: true,
368 },
369 },
370 cactui::KeyMenuItem {
371 key: KeyCode::Char('c'),
372 description: "channels".to_string(),
373 color: Some(Color::Cyan),
374 action: cactui::MenuAction::Submenu(cactui::KeyMenuConfig {
375 header_lines: Some(Box::new(|session: &PlaybackSession| {
376 get_frame_header_lines(session)
377 })),
378 items: vec![
379 cactui::KeyMenuItem {
380 key: KeyCode::Char('1'),
381 description: "fg red".to_string(),
382 color: Some(Color::Red),
383 action: cactui::MenuAction::Callback {
384 callback: Box::new(|session, _| {
385 session.get_current_channels_mut().fg_red =
386 !session.get_current_channels().fg_red;
387 Ok(cactui::MenuResult::Stay)
388 }),
389 clear_menu: false,
390 },
391 },
392 cactui::KeyMenuItem {
393 key: KeyCode::Char('2'),
394 description: "fg green".to_string(),
395 color: Some(Color::Green),
396 action: cactui::MenuAction::Callback {
397 callback: Box::new(|session, _| {
398 session.get_current_channels_mut().fg_green =
399 !session.get_current_channels().fg_green;
400 Ok(cactui::MenuResult::Stay)
401 }),
402 clear_menu: false,
403 },
404 },
405 cactui::KeyMenuItem {
406 key: KeyCode::Char('3'),
407 description: "fg blue".to_string(),
408 color: Some(Color::Blue),
409 action: cactui::MenuAction::Callback {
410 callback: Box::new(|session, _| {
411 session.get_current_channels_mut().fg_blue =
412 !session.get_current_channels().fg_blue;
413 Ok(cactui::MenuResult::Stay)
414 }),
415 clear_menu: false,
416 },
417 },
418 cactui::KeyMenuItem {
419 key: KeyCode::Char('4'),
420 description: "bg red".to_string(),
421 color: Some(Color::Red),
422 action: cactui::MenuAction::Callback {
423 callback: Box::new(|session, _| {
424 session.get_current_channels_mut().bg_red =
425 !session.get_current_channels().bg_red;
426 Ok(cactui::MenuResult::Stay)
427 }),
428 clear_menu: false,
429 },
430 },
431 cactui::KeyMenuItem {
432 key: KeyCode::Char('5'),
433 description: "bg green".to_string(),
434 color: Some(Color::Green),
435 action: cactui::MenuAction::Callback {
436 callback: Box::new(|session, _| {
437 session.get_current_channels_mut().bg_green =
438 !session.get_current_channels().bg_green;
439 Ok(cactui::MenuResult::Stay)
440 }),
441 clear_menu: false,
442 },
443 },
444 cactui::KeyMenuItem {
445 key: KeyCode::Char('6'),
446 description: "bg blue".to_string(),
447 color: Some(Color::Blue),
448 action: cactui::MenuAction::Callback {
449 callback: Box::new(|session, _| {
450 session.get_current_channels_mut().bg_blue =
451 !session.get_current_channels().bg_blue;
452 Ok(cactui::MenuResult::Stay)
453 }),
454 clear_menu: false,
455 },
456 },
457 cactui::KeyMenuItem {
458 key: KeyCode::Char('7'),
459 description: "mods".to_string(),
460 color: Some(Color::White),
461 action: cactui::MenuAction::Callback {
462 callback: Box::new(|session, _| {
463 session.get_current_channels_mut().modifiers =
464 !session.get_current_channels().modifiers;
465 Ok(cactui::MenuResult::Stay)
466 }),
467 clear_menu: false,
468 },
469 },
470 cactui::KeyMenuItem {
471 key: KeyCode::Esc,
472 description: "return".to_string(),
473 color: None,
474 action: cactui::MenuAction::Exit,
475 },
476 ],
477 should_loop: true,
478 }),
479 },
480 cactui::KeyMenuItem {
481 key: KeyCode::Enter,
482 description: "next frame".to_string(),
483 color: Some(Color::Cyan),
484 action: cactui::MenuAction::ExitKeepHeader,
485 },
486 ],
487 should_loop: true,
488 }
489}
490
491fn create_demo_frames() -> Vec<DemoFrame> {
493 vec![
494 DemoFrame {
495 content: format!(
496 "{}\n{}",
497 "=== DEBUGTERM FRAME 0 (22ms) ===",
498 " 1│┌ 2 (FOCUSED)────────────────┐\n \
499 2││>> WIDGET 1 << │\n \
500 3││Welcome to i3-style TUI! │\n \
501 4││ │\n \
502 5││Keys: │\n \
503 6││- Space: New widget │\n \
504 7││- Alt+h/Alt+v: Split horizon│\n \
505 8││- Alt+e: Toggle layout │\n \
506 9││- Alt-Arrows: Navigate │\n\
507 10││- Alt+Shift+Arrows: Move │\n\
508 11││- Tab: Cycle focus │\n\
509 12││- s: Seal/unseal container │\n\
510 13││- u: Update content │\n\
511 14││- r: Reset content │\n\
512 15││- x: Remove widget │\n\
513 16││- q: Quit │\n\
514 17││ │\n\
515 18││ │\n\
516 19│└────────────────────────────┘\n\
517 20│-- INPUT --\n\
518 === END FRAME 0 ==="
519 ),
520 },
521 DemoFrame {
522 content: format!(
523 "{}\n{}",
524 "=== DEBUGTERM FRAME 5 (122ms) ===",
525 " 1│┌ 2──────────────────────────┐\n \
526 2││>> WIDGET 1 << │\n \
527 3││Welcome to i3-style TUI! │\n \
528 4││ │\n \
529 5││Keys: │\n \
530 6││- Space: New widget │\n \
531 7││- Alt+h/Alt+v: Split horizon│\n \
532 8││- Alt+e: Toggle layout │\n \
533 9││- Alt-Arrows: Navigate │\n\
534 10│└────────────────────────────┘\n\
535 11│┌ 3 (FOCUSED)────────────────┐\n\
536 12││Widget 3 │\n\
537 13││ │\n\
538 14││This is widget number 3 │\n\
539 15││ │\n\
540 16││ │\n\
541 17││ │\n\
542 18││ │\n\
543 19│└────────────────────────────┘\n\
544 20│-- INPUT --\n\
545 === END FRAME 5 ==="
546 ),
547 },
548 DemoFrame {
549 content: format!(
550 "{}\n{}",
551 "=== DEBUGTERM FRAME 10 (324ms) ===",
552 " 1│┌ 2─────────────┐┌ 4 (FOCUSED)──┐\n \
553 2││>> WIDGET 1 << ││Widget 4 │\n \
554 3││Welcome to i3- ││ │\n \
555 4││ ││This is widget│\n \
556 5││Keys: ││number 4 │\n \
557 6││- Space: New w ││ │\n \
558 7││- Alt+h/Alt+v: ││ │\n \
559 8││- Alt+e: Toggl ││ │\n \
560 9││- Alt-Arrows: ││ │\n\
561 10│└───────────────┘└──────────────┘\n\
562 11│┌ 3──────────────────────────────┐\n\
563 12││Widget 3 │\n\
564 13││ │\n\
565 14││This is widget number 3 │\n\
566 15││ │\n\
567 16││ │\n\
568 17││ │\n\
569 18││ │\n\
570 19│└────────────────────────────────┘\n\
571 20│-- INPUT --\n\
572 === END FRAME 10 ==="
573 ),
574 },
575 ]
576}
577
578fn main() -> Result<()> {
579 let mut session = PlaybackSession::new();
580
581 session.collect_recording_description()?;
583
584 session.run_interactive_playback()?;
586
587 Ok(())
588}