mod app;
mod cli;
mod clipboard;
mod config;
mod editor;
mod images;
mod index;
mod keys;
mod md;
mod notes;
mod render;
mod search;
mod ui;
use anyhow::Result;
use crossterm::event::{
self, Event, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
PushKeyboardEnhancementFlags,
};
use std::time::Duration;
fn keyboard_flags() -> KeyboardEnhancementFlags {
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
}
static ENHANCED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
fn push_keyboard() -> bool {
if !matches!(
crossterm::terminal::supports_keyboard_enhancement(),
Ok(true)
) {
return false;
}
let ok = crossterm::execute!(
std::io::stdout(),
PushKeyboardEnhancementFlags(keyboard_flags())
)
.is_ok();
ENHANCED.store(ok, std::sync::atomic::Ordering::Relaxed);
ok
}
fn pop_keyboard() {
if ENHANCED.swap(false, std::sync::atomic::Ordering::Relaxed) {
let _ = crossterm::execute!(std::io::stdout(), PopKeyboardEnhancementFlags);
}
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let launch = match cli::parse(&args, probe) {
cli::Cli::Tui(l) => l,
cli::Cli::Help => {
print!("{}", cli::USAGE);
return Ok(());
}
cli::Cli::PrintPath => {
println!("{}", config::Config::load()?.notes_dir.display());
return Ok(());
}
cli::Cli::Add(text) => return add(text),
cli::Cli::Keys => return keys(),
cli::Cli::Error(msg) => {
eprintln!("tinynote: {msg}\n\n{}", cli::USAGE);
std::process::exit(2);
}
};
tui(launch)
}
fn probe(s: &str) -> cli::PathKind {
let path = std::path::Path::new(s);
if path.is_dir() {
cli::PathKind::Dir
} else if path.is_file() {
cli::PathKind::File
} else {
cli::PathKind::Missing
}
}
fn add(text: Option<String>) -> Result<()> {
let text = match text {
Some(t) => t,
None => {
let mut buf = String::new();
std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)?;
buf
}
};
let text = text.trim();
if text.is_empty() {
anyhow::bail!("nothing to add");
}
let config = config::Config::load()?;
config.ensure_dirs()?;
let note = notes::create_with(&config.notes_dir, format!("{text}\n"))?;
println!("{}", note.path.display());
Ok(())
}
fn keys() -> Result<()> {
crossterm::terminal::enable_raw_mode()?;
let supported = crossterm::terminal::supports_keyboard_enhancement();
let pushed = push_keyboard();
println!("keyboard enhancement: supported={supported:?} pushed={pushed}\r");
println!("press keys — esc to quit\r");
loop {
match event::read()? {
Event::Key(k) => {
println!(
"code={:?} modifiers={:?} kind={:?} state={:?}\r",
k.code, k.modifiers, k.kind, k.state
);
if k.code == event::KeyCode::Esc && k.kind != event::KeyEventKind::Release {
break;
}
}
other => println!("{other:?}\r"),
}
}
pop_keyboard();
crossterm::terminal::disable_raw_mode()?;
Ok(())
}
fn tui(launch: cli::Launch) -> Result<()> {
let mut app = app::App::launch(launch)?;
let mut terminal = ratatui::init();
crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture)?;
push_keyboard();
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
pop_keyboard();
let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);
hook(info);
}));
app.images.probe();
let result = run(&mut terminal, &mut app);
pop_keyboard();
let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);
ratatui::restore();
result
}
fn run(terminal: &mut ratatui::DefaultTerminal, app: &mut app::App) -> Result<()> {
while !app.quit {
terminal.draw(|f| ui::draw(f, app))?;
if event::poll(Duration::from_millis(100))? {
match event::read()? {
Event::Key(k) if k.kind != event::KeyEventKind::Release => app.on_key(k),
Event::Mouse(m) => app.on_mouse(m),
_ => {}
}
}
app.tick();
}
app.save_now();
Ok(())
}