#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::pedantic, clippy::nursery)]
pub mod app;
pub mod graph;
pub mod keymap;
pub mod markdown;
pub mod search;
pub mod snapshot;
pub mod theme;
pub mod ui;
pub mod watch;
pub mod worker;
use app::{App, Command, Msg};
use crossterm::event::{self, Event};
use okf_core::Date;
use std::io::Write as _;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Tab {
#[default]
Explorer,
Graph,
Trust,
Computations,
}
impl std::str::FromStr for Tab {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"explorer" => Ok(Self::Explorer),
"graph" => Ok(Self::Graph),
"trust" => Ok(Self::Trust),
"computations" | "compute" => Ok(Self::Computations),
other => Err(format!(
"unknown tab {other:?} (expected explorer|graph|trust|computations)"
)),
}
}
}
#[derive(Clone, Debug)]
pub struct StudioOptions {
pub root: PathBuf,
pub today: Option<Date>,
pub no_watch: bool,
pub initial_tab: Option<Tab>,
pub author: Option<String>,
}
impl Default for StudioOptions {
fn default() -> Self {
Self {
root: PathBuf::from("."),
today: None,
no_watch: false,
initial_tab: None,
author: None,
}
}
}
pub fn run(options: StudioOptions) -> std::io::Result<ExitCode> {
let (msg_tx, msg_rx) = channel::<Msg>();
let mut app = App::new(&options);
let _watcher = if options.no_watch {
None
} else {
Some(watch::spawn(
options.root.clone(),
Duration::from_millis(500),
msg_tx.clone(),
))
};
let worker_tx = worker::spawn(
worker::WorkerConfig {
root: options.root,
today: options.today,
author: app.author.clone(),
},
msg_tx,
);
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
ratatui::restore();
default_hook(info);
}));
let mut terminal = ratatui::try_init()?;
let tick = Duration::from_millis(250);
let mut last_tick = Instant::now();
let mut dirty = true;
let result = loop {
while let Ok(msg) = msg_rx.try_recv() {
app.update(msg);
dirty = true;
}
if last_tick.elapsed() >= tick {
last_tick = Instant::now();
app.update(Msg::Tick);
dirty = true;
}
for command in app.pending_commands.drain(..) {
let _ = worker_tx.send(command);
}
if let Some(path) = app.editor_request.take() {
open_in_editor(&mut terminal, &path)?;
app.update(Msg::FilesChanged);
dirty = true;
}
if let Some(text) = app.copy_request.take() {
osc52_copy(&text);
}
if app.should_quit {
break Ok(ExitCode::SUCCESS);
}
if dirty {
terminal.draw(|frame| ui::shell::draw(frame, &app))?;
dirty = false;
}
if event::poll(Duration::from_millis(100))? {
match event::read()? {
Event::Key(key) if key.is_press() => app.update(Msg::Key(key)),
Event::Mouse(mouse) => app.update(Msg::Mouse(mouse)),
Event::Resize(_, _) => app.update(Msg::Resize),
_ => {}
}
dirty = true;
}
};
let _ = worker_tx.send(Command::Shutdown);
ratatui::restore();
result
}
fn open_in_editor(
terminal: &mut ratatui::DefaultTerminal,
path: &std::path::Path,
) -> std::io::Result<()> {
let editor = std::env::var("VISUAL")
.or_else(|_| std::env::var("EDITOR"))
.unwrap_or_else(|_| "vi".to_string());
ratatui::restore();
let status = std::process::Command::new(&editor).arg(path).status();
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(std::io::stdout(), crossterm::terminal::EnterAlternateScreen)?;
terminal.clear()?;
if let Err(e) = status {
return Err(std::io::Error::other(format!(
"could not launch editor {editor:?}: {e}"
)));
}
Ok(())
}
fn osc52_copy(text: &str) {
let encoded = base64(text.as_bytes());
let mut stdout = std::io::stdout();
let _ = write!(stdout, "\x1b]52;c;{encoded}\x07");
let _ = stdout.flush();
}
fn base64(input: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b = [
chunk[0],
chunk.get(1).copied().unwrap_or(0),
chunk.get(2).copied().unwrap_or(0),
];
let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
out.push(ALPHABET[(n >> 18) as usize & 63] as char);
out.push(ALPHABET[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 {
ALPHABET[(n >> 6) as usize & 63] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
ALPHABET[n as usize & 63] as char
} else {
'='
});
}
out
}