use std::cell::RefCell;
use std::rc::Rc;
use gtcaca::{
color, key, Application, Barchart, Entry, Gtcaca, Piechart, Statusbar, Table, TableModel, Widget,
Window,
};
struct Sheet {
headers: Vec<String>,
rows: Vec<Vec<String>>,
}
impl Sheet {
fn from_csv(text: &str) -> Sheet {
let mut lines = text.lines().filter(|l| !l.trim().is_empty());
let headers = lines
.next()
.unwrap_or("label,value")
.split(',')
.map(|s| s.trim().to_string())
.collect();
let rows = lines
.map(|l| l.split(',').map(|s| s.trim().to_string()).collect())
.collect();
Sheet { headers, rows }
}
fn cols(&self) -> usize {
self.headers.len()
}
fn column_values(&self, col: usize) -> Vec<f32> {
self.rows
.iter()
.map(|r| r.get(col).and_then(|c| c.parse::<f32>().ok()).unwrap_or(0.0))
.collect()
}
fn labels(&self) -> Vec<String> {
self.rows.iter().map(|r| r.first().cloned().unwrap_or_default()).collect()
}
fn get_cell(&self, row: usize, col: usize) -> String {
self.rows.get(row).and_then(|r| r.get(col)).cloned().unwrap_or_default()
}
fn set_cell(&mut self, row: usize, col: usize, value: &str) {
if let Some(r) = self.rows.get_mut(row) {
if let Some(c) = r.get_mut(col) {
*c = value.to_string();
}
}
}
}
fn refresh_charts(sheet: &RefCell<Sheet>, bars: &Barchart, pie: &Piechart, col: usize) {
let s = sheet.borrow();
let values = s.column_values(col);
let labels = s.labels();
let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
bars.set_data(&values, &label_refs); pie.set_data(&values, &label_refs); pie.set_title(&format!("{} share", s.headers.get(col).map(String::as_str).unwrap_or("")));
}
const SAMPLE: &str = "\
Region,Q1,Q2,Q3,Q4
North,42,55,61,70
South,30,28,35,40
East,58,63,59,66
West,25,33,44,52
Central,48,50,47,53";
struct SheetModel(Rc<RefCell<Sheet>>);
impl TableModel for SheetModel {
fn row_count(&self) -> i64 {
self.0.borrow().rows.len() as i64
}
fn headers(&self) -> Vec<String> {
self.0.borrow().headers.clone()
}
fn cell(&self, row: i64, col: i32) -> String {
self.0
.borrow()
.rows
.get(row as usize)
.and_then(|r| r.get(col as usize))
.cloned()
.unwrap_or_default()
}
}
fn main() -> Result<(), gtcaca::Error> {
let text = std::env::args()
.nth(1)
.and_then(|p| std::fs::read_to_string(p).ok())
.unwrap_or_else(|| SAMPLE.to_string());
let sheet = Rc::new(RefCell::new(Sheet::from_csv(&text)));
let mut chart_col = 1usize.min(sheet.borrow().cols().saturating_sub(1));
let ctx = Gtcaca::init()?;
let app = Application::new(&ctx, "gtcaca spreadsheet");
let win = Window::fullscreen(&app, Some("Data analytics"));
let area = win.content(1);
let table_h = (area.height / 2).max(6);
let charts_y = area.y + table_h + 1;
let charts_h = (area.y + area.height - charts_y).max(6);
let half_w = area.width / 2;
let model = Box::new(SheetModel(Rc::clone(&sheet)));
let table = Table::new(&win, area.x, area.y, area.width, table_h, model);
table.set_title("data");
table.set_current(0, 0);
let bars = Barchart::new(&win, area.x, charts_y, half_w, charts_h);
bars.set_color(color::CYAN);
let pie = Piechart::new(&win, area.x + half_w + 1, charts_y, area.width - half_w - 1, charts_h);
let status = Statusbar::new("");
let prompt = Entry::new(&win, area.x, area.y + area.height - 1, area.width);
refresh_charts(&sheet, &bars, &pie, chart_col);
table.set_focus(true);
loop {
ctx.clear();
win.paint();
table.paint();
bars.paint();
pie.paint();
status.set_text(&format!(
" row {} · charting '{}' — arrows move · Enter: edit cell · c: next column · q: quit",
table.current_row() + 1,
sheet.borrow().headers.get(chart_col).map(String::as_str).unwrap_or("")
));
status.draw();
ctx.flush();
let Some(k) = gtcaca::poll_key(-1) else { continue };
const Q: i32 = b'q' as i32;
const QU: i32 = b'Q' as i32;
const C: i32 = b'c' as i32;
match k {
Q | QU => break,
C => {
let cols = sheet.borrow().cols();
if cols > 1 {
chart_col = chart_col % (cols - 1) + 1;
refresh_charts(&sheet, &bars, &pie, chart_col);
}
}
key::RETURN => {
let (row, col) = (table.current_row() as usize, table.current_col() as usize);
prompt.set_text(&sheet.borrow().get_cell(row, col));
prompt.set_focus(true);
loop {
ctx.clear();
win.paint();
table.paint();
bars.paint();
pie.paint();
prompt.paint();
ctx.flush();
let Some(k) = gtcaca::poll_key(-1) else { continue };
match k {
key::RETURN => {
sheet.borrow_mut().set_cell(row, col, &prompt.text());
break;
}
key::ESCAPE => break,
_ => {
prompt.send_key(k); }
}
}
prompt.set_focus(false);
refresh_charts(&sheet, &bars, &pie, chart_col); }
key::UP | key::DOWN | key::LEFT | key::RIGHT => {
table.send_key(k);
}
_ => {}
}
}
Ok(())
}