1mod anim;
15mod eclipse;
16mod overlay;
17mod render;
18
19#[allow(
20 dead_code,
21 reason = "the shared picker module also carries the chat example's overlay driver"
22)]
23#[path = "../chat/picker.rs"]
24mod picker;
25
26use std::io;
27
28use omp_tui::{AppEvent, AppOptions, Key, OverlayId, Size, Ui, UiContext, dom};
29
30const PICKER_ROWS: u16 = 8;
32
33fn build_ui(viewport: Size, context: UiContext) -> Ui {
34 let pane_height = render::pane_height(viewport);
37 let charset = context.charset;
38
39 Ui::from_root(
40 dom! {
41 <col gap=1>
42 <tabs id=view>
43 <tab title="Markdown">
44 <scroll id="pane-md" h={pane_height}>
45 <md>{render::MARKDOWN_TAB}</md>
46 </scroll>
47 </tab>
48 <tab title="Math">
49 <scroll id="pane-math" h={pane_height}>
50 <md>{render::MATH_TAB}</md>
51 </scroll>
52 </tab>
53 <tab title="Mermaid">
54 <scroll id="pane-mermaid" h={pane_height}>
55 <md>{render::MERMAID_TAB}</md>
56 </scroll>
57 </tab>
58 <tab title="Graphviz">
59 <scroll id="pane-graphviz" h={pane_height}>
60 <md>{render::GRAPHVIZ_TAB}</md>
61 </scroll>
62 </tab>
63 <tab title="Macro">
64 <box border=round title="Built with dom!">
65 <col gap=1>
66 <row gap=1>
67 <i:info/>
68 <text bold>{"Macro-built pane"}</text>
69 </row>
70 <gallery-note>
71 <text dim>{format!("Interpolated at runtime: {} nested layout levels", 3)}</text>
72 </gallery-note>
73 </col>
74 </box>
75 </tab>
76 <tab title="Live">
77 <col gap=1>
78 <editor id=src value={render::LIVE_PREFILL}/>
79 <box border=round title="Preview">
80 <md id=preview>{"..."}</md>
81 </box>
82 </col>
83 </tab>
84 <tab title="Anim">{anim::pane()}</tab>
85 <tab title="Overlay">{overlay::pane()}</tab>
86 <tab title="Eclipse">{eclipse::pane(viewport, pane_height)}</tab>
87 <tab title="Picker">
88 {picker::models_pane(0, PICKER_ROWS, viewport.width, charset)}
89 </tab>
90 </tabs>
91 <text dim>{"Tab focus · ←/→ switch tabs · ↑/↓ PgUp/PgDn scroll · Ctrl-C quit"}</text>
92 </col>
93 },
94 viewport.width,
95 context,
96 )
97}
98
99fn active_tab(ui: &Ui) -> String {
101 ui.values()["view"].as_str().unwrap_or_default().to_owned()
102}
103
104#[derive(Default)]
106struct Layers {
107 picker: Option<OverlayId>,
108 help: Option<OverlayId>,
109}
110
111#[tokio::main]
112async fn main() -> io::Result<()> {
113 let mut app = AppOptions::new()
114 .mouse()
115 .quit([Key::Ctrl('c'), Key::Ctrl('q')])
116 .start(|env| build_ui(env.viewport, env.ctx))
117 .await?;
118 picker::show_detail_on(app.ui_mut(), Some(0));
121
122 let mut synced = String::new();
123 let mut lab = anim::Lab::new();
124 let mut layers = Layers::default();
125 let mut next_step = tokio::time::Instant::now() + anim::AUTOPLAY_STEP;
126
127 loop {
128 let event = tokio::select! {
129 event = app.next() => match event? {
130 Some(event) => event,
131 None => break,
132 },
133 () = tokio::time::sleep_until(next_step) => {
134 if lab.autoplay && active_tab(app.ui()) == "Anim" {
135 lab.advance(app.ui_mut());
136 }
137 next_step += anim::AUTOPLAY_STEP;
138 continue;
139 },
140 };
141 match event {
142 AppEvent::Resized(viewport) => {
143 for pane in render::PANE_IDS {
144 app.ui_mut().set_height(pane, render::pane_height(viewport));
145 }
146 },
147 AppEvent::Key(key) => match active_tab(app.ui()).as_str() {
148 "Anim" => lab.handle_key(key, app.ui_mut()),
149 "Overlay" => match key {
150 Key::Ctrl('k') if layers.picker.is_none() => {
151 layers.picker = Some(overlay::show_picker(app.ui_mut()));
152 },
153 Key::Ctrl('g') => match layers.help.take() {
154 Some(id) => {
155 app.ui_mut().close_overlay(id);
156 },
157 None => layers.help = Some(overlay::show_help(app.ui_mut())),
158 },
159 _ => {},
160 },
161 _ => {},
162 },
163 AppEvent::Changed { id, value } if id == "model" => {
165 if let Some(overlay) = layers.picker.take() {
166 let label = overlay::MODELS
167 .iter()
168 .find(|(short, ..)| *short == value)
169 .map_or(value.as_str(), |(_, label, _)| label);
170 app.ui_mut().set_text("status", format!("model: {label}"));
171 app.ui_mut().close_overlay(overlay);
172 }
173 },
174 AppEvent::Highlighted { id, value } if id == "models" => {
177 picker::show_detail_on(app.ui_mut(), value.as_str().parse().ok());
178 },
179 AppEvent::Filtered { id, value, .. } if id == "models" => {
180 let model = value.and_then(|value| value.as_str().parse().ok());
181 picker::show_detail_on(app.ui_mut(), model);
182 },
183 AppEvent::OverlayClosed(id) => {
184 if layers.picker == Some(id) {
185 layers.picker = None;
186 }
187 if layers.help == Some(id) {
188 layers.help = None;
189 }
190 },
191 _ => {},
192 }
193 render::sync_preview(app.ui_mut(), &mut synced);
194 let chords: &[Key] = if active_tab(app.ui()) == "Overlay" {
198 &[Key::Ctrl('k'), Key::Ctrl('g')]
199 } else {
200 &[]
201 };
202 app.set_hotkeys(chords.iter().copied());
203 }
204 Ok(())
205}