use std::time::Duration;
use crossterm::event::{
Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, MouseButton, MouseEvent,
MouseEventKind,
};
use ratatui::buffer::Buffer;
use ratatui::style::{Color, Modifier, Style};
const POINTER: &str = "➤";
#[derive(Clone, Debug)]
pub enum Step {
MoveTo { col: u16, row: u16, secs: f64 },
Press,
Release,
Dwell(f64),
Scroll { up: bool, clicks: u8 },
Key(KeyCode),
}
pub struct Autopilot {
steps: Vec<Step>,
index: usize,
t: f64,
from: (f64, f64),
pos: (f64, f64),
pressed: bool,
}
impl Autopilot {
pub fn new(start: (u16, u16), steps: Vec<Step>) -> Self {
let pos = (start.0 as f64, start.1 as f64);
Autopilot {
steps,
index: 0,
t: 0.0,
from: pos,
pos,
pressed: false,
}
}
pub fn finished(&self) -> bool {
self.index >= self.steps.len()
}
pub fn cell(&self) -> (u16, u16) {
(self.pos.0.round() as u16, self.pos.1.round() as u16)
}
pub fn tick(&mut self, dt: Duration) -> Vec<Event> {
let mut out = Vec::new();
if self.finished() {
return out;
}
self.t += dt.as_secs_f64();
while let Some(step) = self.steps.get(self.index).cloned() {
match step {
Step::MoveTo { col, row, secs } => {
let target = (col as f64, row as f64);
let secs = secs.max(0.001);
let k = (self.t / secs).min(1.0);
let e = if k < 0.5 {
2.0 * k * k
} else {
1.0 - (-2.0 * k + 2.0).powi(2) / 2.0
};
self.pos = (
self.from.0 + (target.0 - self.from.0) * e,
self.from.1 + (target.1 - self.from.1) * e,
);
let (c, r) = self.cell();
out.push(mouse(
if self.pressed {
MouseEventKind::Drag(MouseButton::Left)
} else {
MouseEventKind::Moved
},
c,
r,
));
if k < 1.0 {
break;
}
self.advance(target);
}
Step::Dwell(secs) => {
if self.t < secs {
break;
}
let pos = self.pos;
self.advance(pos);
}
Step::Press => {
let (c, r) = self.cell();
self.pressed = true;
out.push(mouse(MouseEventKind::Down(MouseButton::Left), c, r));
let pos = self.pos;
self.advance(pos);
}
Step::Release => {
let (c, r) = self.cell();
self.pressed = false;
out.push(mouse(MouseEventKind::Up(MouseButton::Left), c, r));
let pos = self.pos;
self.advance(pos);
}
Step::Key(code) => {
out.push(Event::Key(KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}));
let pos = self.pos;
self.advance(pos);
}
Step::Scroll { up, clicks } => {
let (c, r) = self.cell();
let kind = if up {
MouseEventKind::ScrollUp
} else {
MouseEventKind::ScrollDown
};
for _ in 0..clicks {
out.push(mouse(kind, c, r));
}
let pos = self.pos;
self.advance(pos);
}
}
}
out
}
fn advance(&mut self, from: (f64, f64)) {
self.index += 1;
self.t = 0.0;
self.from = from;
}
pub fn draw(&self, buf: &mut Buffer) {
let (c, r) = self.cell();
let area = buf.area;
if c >= area.width || r >= area.height {
return;
}
let style = Style::default()
.fg(if self.pressed {
Color::White
} else {
Color::Indexed(220)
})
.add_modifier(Modifier::BOLD);
if let Some(cell) = buf.cell_mut((c, r)) {
cell.set_symbol(POINTER).set_style(style);
}
}
}
fn mouse(kind: MouseEventKind, column: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind,
column,
row,
modifiers: crossterm::event::KeyModifiers::NONE,
})
}
const THROW_X: i32 = 22;
const THROW_Y: i32 = -4;
#[derive(Clone, Copy, Default)]
pub struct TourMarks {
pub click_col: u16,
pub click_row: u16,
pub pan_col: u16,
pub pan_row: u16,
pub bar_row: u16,
pub bar_l: u16,
pub bar_r: u16,
}
pub fn tour_steps(m: TourMarks) -> Vec<Step> {
use Step::*;
let TourMarks {
click_col,
click_row,
pan_col,
pan_row,
bar_row,
bar_l,
bar_r,
} = m;
vec![
MoveTo {
col: click_col,
row: click_row,
secs: 0.5,
},
Dwell(0.15),
Press,
Dwell(0.1),
Release,
Dwell(1.5),
Key(KeyCode::Esc),
Dwell(0.25),
MoveTo {
col: pan_col,
row: pan_row,
secs: 0.5,
},
Dwell(0.15),
Press,
Dwell(0.1),
MoveTo {
col: (pan_col as i32 + THROW_X) as u16,
row: (pan_row as i32 + THROW_Y).max(1) as u16,
secs: 0.6,
},
Dwell(0.2),
Release,
Dwell(0.25),
MoveTo {
col: click_col,
row: click_row,
secs: 0.45,
},
Dwell(0.15),
Scroll {
up: true,
clicks: 3,
},
Dwell(0.9),
Scroll {
up: false,
clicks: 3,
},
Dwell(0.3),
MoveTo {
col: bar_r,
row: bar_row,
secs: 0.5,
},
Dwell(0.15),
Press,
Dwell(0.1),
MoveTo {
col: bar_l,
row: bar_row,
secs: 1.1,
},
Dwell(0.35),
MoveTo {
col: bar_r,
row: bar_row,
secs: 0.9,
},
Release,
Dwell(0.3),
MoveTo {
col: pan_col,
row: pan_row,
secs: 0.4,
},
Key(KeyCode::Char('o')),
Dwell(0.9),
]
}
pub fn tour_secs() -> f64 {
duration(&tour_steps(TourMarks::default()))
}
pub fn duration(steps: &[Step]) -> f64 {
steps
.iter()
.map(|s| match s {
Step::MoveTo { secs, .. } => *secs,
Step::Dwell(secs) => *secs,
Step::Press | Step::Release | Step::Scroll { .. } | Step::Key(_) => 0.0,
})
.sum()
}
pub fn tour(app: &crate::state::App) -> Vec<Step> {
let size = app.flow.canvas_size();
let (cw, ch) = (size.width as i32, size.height as i32);
let rects: Vec<(i32, i32, i32, i32)> = app
.flow
.nodes()
.filter_map(|n| app.flow.node_terminal_rect(&n.id))
.collect();
let richest = app
.session
.agents
.iter()
.filter(|(_, a)| a.kind == crate::state::session::AgentKind::Subagent)
.max_by_key(|(_, a)| a.tool_calls.len())
.map(|(id, _)| id.clone());
let target = richest
.and_then(|id| app.flow.node_terminal_rect(&id))
.or_else(|| rects.iter().filter(|(_, t, _, _)| *t > 2).copied().next())
.unwrap_or((10, 10, 30, 14));
let (click_col, click_row) = (
((target.0 + target.2) / 2).clamp(1, cw - 2) as u16,
((target.1 + target.3) / 2).clamp(1, ch - 2) as u16,
);
const INSET: i32 = 4;
let free = |x: i32, y: i32| {
!rects
.iter()
.any(|(l, t, r, b)| x >= l - 2 && x <= r + 2 && y >= t - 1 && y <= b + 1)
};
let mut spot = None;
'search: for y in (INSET..ch - INSET).rev() {
for x in INSET..(cw - INSET - THROW_X).max(INSET + 1) {
if free(x, y) && free(x + THROW_X, y + THROW_Y) {
spot = Some((x as u16, y as u16));
break 'search;
}
}
}
let (pan_col, pan_row) = spot.unwrap_or((INSET as u16, (ch - INSET) as u16));
let bar = app.scrubber_area;
let (bar_row, bar_l, bar_r) = bar.map_or((ch as u16 - 3, 8, cw as u16 - 8), |b| {
(b.y + b.height / 2, b.x + 3, b.x + b.width.saturating_sub(4))
});
tour_steps(TourMarks {
click_col,
click_row,
pan_col,
pan_row,
bar_row,
bar_l,
bar_r,
})
}
pub fn is_trigger(event: &Event) -> bool {
matches!(event, Event::Key(k)
if k.kind == KeyEventKind::Press && k.code == KeyCode::Char('t'))
}
pub fn requested() -> bool {
std::env::var("ZOETROPE_DEMO").is_ok_and(|v| v == "1")
}
pub fn is_key_press(event: &Event) -> bool {
matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press)
}