mod context_finder;
mod error;
use context_finder::{ContextFinder, InputType};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use error::Error;
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
widgets::{Block, BorderType, Borders, Paragraph},
Frame, Terminal,
};
use std::{
io::{self, stdin, BufRead},
sync::mpsc::{channel, Receiver, TryRecvError},
thread::{self, JoinHandle},
time::Duration,
};
use tracing::{error, trace, warn, Level};
const INPUT_STREAM_TIMEOUT: u64 = 1000;
const ENVIRONMENT_VARIABLE_ENABLE_TRACING: &str = "ENABLE_TRACING";
fn main() -> Result<(), Error> {
if let Ok(enable_tracing) = std::env::var(ENVIRONMENT_VARIABLE_ENABLE_TRACING) {
if enable_tracing == "1" || &enable_tracing.to_lowercase() == "true" {
let file_appender = tracing_appender::rolling::hourly("./.logs/", "runlog");
tracing_subscriber::fmt()
.with_max_level(Level::TRACE)
.with_writer(file_appender)
.init();
}
}
trace!("Enabling raw mode");
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let res = run_app(&mut terminal);
trace!("Disabling raw mode");
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
error!("{:?}", err);
eprintln!("{err}");
}
Ok(())
}
fn decrement(scroll: usize, count: usize) -> usize {
if let Some(pos) = scroll.checked_sub(count) {
pos
} else {
0
}
}
fn increment(scroll: usize, count: usize, max_val: usize, vertical_size: u16) -> usize {
if let Some(pos) = scroll.checked_add(count) {
if pos > (max_val - vertical_size as usize) {
max_val - vertical_size as usize
} else {
pos
}
} else {
usize::MAX
}
}
fn stream_input(num_lines: usize) -> (Receiver<Result<Vec<String>, Error>>, JoinHandle<()>) {
trace!("Opening channel for input reader");
let (tx, rx) = channel::<Result<Vec<String>, Error>>();
let thread_handle = thread::spawn(move || {
trace!("Reading input");
let input = stdin().lock();
trace!("Splitting input");
let mut input_lines = input.split(b'\n');
loop {
trace!("Reading lines");
let mut maybe_err = None;
let mut lines = Vec::with_capacity(num_lines);
for _ in 0..num_lines {
match input_lines.next() {
Some(Ok(buf)) => {
trace!("Got lines");
let line = String::from_utf8_lossy(&buf).to_string();
lines.push(line);
}
Some(Err(err)) => {
warn!("Error reading input lines: {err}");
maybe_err = Some(err);
break;
}
None => {
trace!("No new lines");
return;
}
}
}
if let Err(err) = tx.send(Ok(lines)) {
warn!("Error sending input streaming result: {err}");
return;
}
if let Some(read_err) = maybe_err {
warn!("Got read error streaming input: {read_err}");
if let Err(_send_err) = tx.send(Err(Error::StreamingSend)) {
return;
}
};
}
});
(rx, thread_handle)
}
fn get_lines(log_lines: &[String], position: usize, vertical_size: u16) -> &[String] {
trace!("Getting screenful of lines");
let lines = if log_lines.len() > (position + vertical_size as usize) {
log_lines.get(position..(position + vertical_size as usize))
} else {
log_lines.get(position..(log_lines.len() - 1))
};
lines.unwrap()
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Result<(), Error> {
let mut position: usize = 0;
let mut vertical_size = terminal.size()?.height;
let (rx, _thread_handle) = stream_input((vertical_size as usize) * 4);
let mut all_lines = rx.recv_timeout(Duration::from_millis(INPUT_STREAM_TIMEOUT))??;
let cf = ContextFinder::new(InputType::Git)?;
loop {
all_lines = match rx.try_recv() {
Ok(maybe_new_lines) => {
trace!("Got more lines");
all_lines.extend(maybe_new_lines?);
all_lines
}
Err(TryRecvError::Disconnected) => all_lines,
Err(e) => {
warn!("Got error receiving new lines: {e}");
all_lines
}
};
let context = cf.get_context(&all_lines[..], position);
let lines = get_lines(&all_lines[..], position, terminal.size()?.height);
terminal.draw(|frame| pager(frame, lines, context, &mut vertical_size))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => {
position = increment(position, 1, all_lines.len(), vertical_size)
}
KeyCode::Char('k') | KeyCode::Up => position = decrement(position, 1),
KeyCode::PageDown => {
position = increment(
position,
vertical_size as usize,
all_lines.len(),
vertical_size,
)
}
KeyCode::PageUp => position = decrement(position, vertical_size as usize),
_ => (),
}
}
}
}
fn pager<B: Backend>(
f: &mut Frame<B>,
git_log: &[String],
commit: Option<&[String]>,
vertical_size: &mut u16,
) {
trace!("Rendering screen");
let commit_len = commit.map(|commit| commit.iter().len() + 1).unwrap_or(0);
let commit = commit.map(|commit| commit.join("\n"));
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Max(std::cmp::min(7, commit_len as u16)),
Constraint::Min(8),
]
.as_ref(),
)
.margin(1)
.split(f.size());
let commit_paragraph = Paragraph::new(commit.unwrap_or("".to_string())).block(
Block::default()
.borders(Borders::BOTTOM)
.border_type(BorderType::Double),
);
f.render_widget(commit_paragraph, chunks[0]);
let paragraph = Paragraph::new(git_log.join("\n")); f.render_widget(paragraph, chunks[1]);
*vertical_size = chunks[1].height;
}