use std::fs::File;
use std::io::BufWriter;
use std::path::PathBuf;
use cellix::App;
use cellix_input::{Command, Event, Key, KeyEvent, Mods, MouseEvent, MouseKind};
use cellix_render::{FontMeasure, Grid, Rasterizer, SoftwareRasterizer};
const DEFAULT_SIZE: (u16, u16) = (120, 36);
const CELL: (usize, usize) = (17, 37);
fn type_text(app: &mut App, text: &str) {
for c in text.chars() {
app.handle(Event::Key(KeyEvent::plain(Key::Char(c))));
}
}
fn find(app: &App, word: &str) -> Option<(u16, u16)> {
let (columns, rows) = app.size();
let mut grid = Grid::new(columns, rows, app.background());
app.draw(&mut grid);
(0..rows).find_map(|y| {
let row = grid.row_text(y);
let at = row.find(word)?;
Some((row[..at].chars().count() as u16, y))
})
}
fn click(app: &mut App, (x, y): (u16, u16)) {
for kind in [MouseKind::Left, MouseKind::Up] {
app.handle(Event::Mouse(MouseEvent {
kind,
x,
y,
fine_x: x as f32 + 0.5,
mods: Mods::default(),
}));
}
}
fn select(app: &mut App, root: &std::path::Path, spec: &str) {
let (file, word) = spec.split_once(':').expect("<file>:<word>");
app.open(&root.join(file)).unwrap();
let (x, y) = find(app, word).expect("the word on screen");
click(app, (x + 1, y));
click(app, (x + 1, y));
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let [root, out, rest @ ..] = args.as_slice() else {
panic!("usage: snapshot <workspace> <out.png> [review|file]");
};
let root = PathBuf::from(root).canonicalize().unwrap();
let number = |i: usize, default| rest.get(i).and_then(|v| v.parse().ok()).unwrap_or(default);
let size = (number(1, DEFAULT_SIZE.0), number(2, DEFAULT_SIZE.1));
let mut app = App::new(root.clone(), size);
if std::env::var("CELLIX_TIER").as_deref() != Ok("cells") {
app.set_measure(std::rc::Rc::new(FontMeasure::new(CELL.0, CELL.1)));
}
if std::env::var_os("CELLIX_PANE").is_some() {
app.in_pane = true;
app.compact_width = 110;
}
if let Ok(theme) = std::env::var("CELLIX_THEME") {
app.follow_terminal_theme(Some(theme == "light"));
}
if let Ok(hex) = std::env::var("CELLIX_BACKGROUND") {
let channel = |at: usize| u8::from_str_radix(&hex[at..at + 2], 16).unwrap();
app.match_terminal_background(Some((channel(0), channel(2), channel(4))));
}
match rest.first().map(String::as_str) {
Some("review") => app.run(Command::OpenDiff),
Some("welcome") => {}
Some("palette") => app.run(Command::CommandPalette),
Some("plan") => {
let path = root.join("plan.md");
let (reply, _decision) = std::sync::mpsc::channel();
app.serve(cellix_agent::Request::Plan { path, reply });
}
Some("close") => {
app.open(&root.join("README.md")).unwrap();
type_text(&mut app, "x");
app.run(Command::CloseTab);
}
Some("comment") => {
app.open(&root.join("README.md")).unwrap();
app.run(Command::AddComment);
type_text(&mut app, "Tighten this");
}
Some(view) if view.starts_with("find:") => {
app.open(&root.join("README.md")).unwrap();
app.run(Command::FindInFile);
type_text(&mut app, &view["find:".len()..]);
}
Some(view) if view.starts_with("select:") => select(&mut app, &root, &view[7..]),
Some(view) if view.starts_with("ask:") => {
select(&mut app, &root, &view[4..]);
let ask = find(&app, "Ask Agent").expect("the Ask Agent button");
click(&mut app, (ask.0 + 1, ask.1));
}
Some(view) if view.starts_with("search:") => {
app.run(Command::FindInProject);
type_text(&mut app, &view["search:".len()..]);
std::thread::sleep(std::time::Duration::from_millis(800));
app.poll();
}
Some(file) => app.open(&root.join(file)).unwrap(),
None => {}
}
let mut grid = Grid::new(size.0, size.1, app.background());
app.draw(&mut grid);
let mut rasterizer = SoftwareRasterizer::new(CELL.0, CELL.1).expect("no fonts on this host");
let mut rgb = Vec::new();
for y in 0..grid.height() {
rgb.extend(rasterizer.row(&grid, y).rgb);
}
let width = size.0 as u32 * CELL.0 as u32;
let height = size.1 as u32 * CELL.1 as u32;
let mut encoder = png::Encoder::new(BufWriter::new(File::create(out).unwrap()), width, height);
encoder.set_color(png::ColorType::Rgb);
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&rgb).unwrap();
}