返回 CodeWhale
habitat.rs
1 //! A live marine specimen: `cargo run --example habitat`.
2 //! The example supplies the clock; the kit neither schedules nor simulates
3 //! Engine activity. `p` changes profile, `m` changes motion, `q` exits.
4 //! `--frames DIR` exports 80 deterministic actual-buffer SVG frames.
5 use codewhale_ratatui::{
6 Habitat, HabitatDensity, KeyHint, KeyHints, Message, MotionMode, Paint, PaneHeader, Role,
7 Theme, Whale, WhaleState, WorkspaceFrame,
8 testing::{self, Profile},
9 };
10 use crossterm::{
11 event::{self, Event, KeyCode, KeyEventKind},
12 execute,
13 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
14 };
15 use ratatui::{Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect};
16 use std::{
17 io,
18 path::Path,
19 time::{Duration, Instant},
20 };
21
22 fn specimen(area: Rect, buf: &mut Buffer, theme: &Theme, elapsed: Duration, motion: MotionMode) {
23 let area = area.intersection(buf.area);
24 let frame = WorkspaceFrame::new("Habitat").mode("Marine specimen").side_width(0).footer("Demonstration / p profile / m motion / q close / host supplies the clock and session facts");
25 frame.paint(area, buf, theme);
26 let main = frame.areas(area).main;
27 PaneHeader::new("Life in the water")
28 .meta(if motion.animates() {
29 "Full motion"
30 } else {
31 "Ambient motion off"
32 })
33 .paint(
34 Rect {
35 height: main.height.min(2),
36 ..main
37 },
38 buf,
39 theme,
40 );
41 let prose = Rect::new(
42 main.x,
43 main.y.saturating_add(3),
44 main.width.min(58),
45 main.height.saturating_sub(3).min(5),
46 );
47 Message::new("Codewhale", "A fish school, a passing jellyfish, and rising bubbles.\nThe water gives way to words and decisions.").paint(prose, buf, theme);
48 if main.height >= 20 && main.width >= 30 {
49 let width = main.width.min(30);
50 let whale = Rect::new(
51 main.x.saturating_add((main.width - width) / 2),
52 main.y.saturating_add(9),
53 width,
54 11,
55 );
56 Whale::new(WhaleState::Rest)
57 .words("Whale companion / demo")
58 .paint(whale, buf, theme);
59 }
60 let hints = KeyHints::new(vec![
61 KeyHint::new("p", "profile"),
62 KeyHint::new("m", "motion"),
63 KeyHint::new("q", "close"),
64 ]);
65 if main.height > 3 {
66 hints.paint(
67 Rect::new(main.x, main.bottom().saturating_sub(2), main.width, 2),
68 buf,
69 theme,
70 );
71 }
72 // Paint after foreground content so open-water collision protects it.
73 Habitat::new(elapsed, motion)
74 .density(HabitatDensity::Rich)
75 .hold_jellyfish_visit(true)
76 .paint(main, buf, theme);
77 }
78
79 fn frames(dir: &Path) -> io::Result<()> {
80 std::fs::create_dir_all(dir)?;
81 let theme = Profile::DarkTrue.theme();
82 let mut manifest = Vec::new();
83 for index in 0..80 {
84 let elapsed_ms = 6_000 + index * 100;
85 let buf = testing::render(112, 30, |area, buf| {
86 buf.set_style(area, theme.bg(Role::Background));
87 specimen(
88 area,
89 buf,
90 &theme,
91 Duration::from_millis(elapsed_ms),
92 MotionMode::Full,
93 );
94 });
95 let name = format!("frame-{index:03}.svg");
96 std::fs::write(dir.join(&name), testing::svg(&buf, &theme))?;
97 manifest.push(serde_json::json!({"file":name, "elapsed_ms":elapsed_ms, "profile":"dark-truecolor", "width":112, "height":30}));
98 }
99 std::fs::write(
100 dir.join("manifest.json"),
101 serde_json::to_vec_pretty(&manifest)?,
102 )?;
103 Ok(())
104 }
105
106 struct Restore;
107 impl Drop for Restore {
108 fn drop(&mut self) {
109 let _ = disable_raw_mode();
110 let _ = execute!(io::stdout(), LeaveAlternateScreen);
111 }
112 }
113 fn main() -> io::Result<()> {
114 let args: Vec<String> = std::env::args().skip(1).collect();
115 if args.first().map(String::as_str) == Some("--frames") {
116 return frames(Path::new(
117 args.get(1).map_or("target/habitat-frames", String::as_str),
118 ));
119 }
120 if !args.is_empty() {
121 return Err(io::Error::other("usage: habitat [--frames DIR]"));
122 }
123 enable_raw_mode()?;
124 let _restore = Restore;
125 execute!(io::stdout(), EnterAlternateScreen)?;
126 let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
127 let start = Instant::now();
128 let mut profile = 0;
129 let mut motion = MotionMode::Full;
130 loop {
131 let theme = Profile::ALL[profile].theme();
132 terminal.draw(|frame| {
133 let area = frame.area();
134 specimen(
135 area,
136 frame.buffer_mut(),
137 &theme,
138 start.elapsed().saturating_add(Duration::from_secs(6)),
139 motion,
140 );
141 })?;
142 // The example's event loop owns frame scheduling. A quiet mode can
143 // block on input instead of repeatedly painting identical frames.
144 if motion.animates() && !event::poll(Duration::from_millis(50))? {
145 continue;
146 }
147 if let Event::Key(key) = event::read()?
148 && key.kind == KeyEventKind::Press
149 {
150 match key.code {
151 KeyCode::Char('q') | KeyCode::Esc => break,
152 KeyCode::Char('p') => profile = (profile + 1) % Profile::ALL.len(),
153 KeyCode::Char('m') => {
154 motion = match motion {
155 MotionMode::Full => MotionMode::Reduced,
156 MotionMode::Reduced => MotionMode::Still,
157 MotionMode::Still => MotionMode::Full,
158 }
159 }
160 _ => {}
161 }
162 }
163 }
164 Ok(())
165 }
166
166 lines RUST