use std::fmt::Write as FmtWrite;
use std::io::{self, IsTerminal};
use std::process::ExitCode;
use std::path::PathBuf;
use tess::app::{self, RebuildSpec};
use tess::batch::{self, BatchDestination, BatchSpec};
use tess::cli::Args;
use tess::error::{Error, Result};
use tess::filter::{CompiledFilter, FilterSpec};
use tess::grep::GrepPredicate;
use tess::format;
use tess::line_index::LineIndex;
use tess::prettify::{self, PrettifyMode, ResolvedType};
use tess::source::{find_tail_offset, MockSource, Source, StdinSource, TransformingSource};
use tess::terminal::{install_panic_hook, install_signal_flag, TerminalGuard};
use tess::viewport::Viewport;
use clap::Parser;
const MANUAL_TEXT: &str = include_str!("../MANUAL.md");
use colored::Colorize;
fn examples_section(buf: &mut String, title: &str) {
let _ = writeln!(buf, " {}", title.yellow().bold());
let _ = writeln!(buf);
}
fn examples_example(buf: &mut String, desc: &str, commands: &[&str]) {
let _ = writeln!(buf, " {}", desc.bold());
for cmd in commands {
let _ = writeln!(buf, " {}", cmd.cyan());
}
let _ = writeln!(buf);
}
fn examples_note(buf: &mut String, text: &str) {
let _ = writeln!(buf, " {} {}", "note:".dimmed().bold(), text.dimmed());
let _ = writeln!(buf);
}
fn build_examples_text() -> String {
let mut buf = String::new();
let _ = writeln!(buf);
let _ = writeln!(buf, "{}", "tess — usage examples".bold());
let _ = writeln!(buf);
examples_section(&mut buf, "Plain viewing");
examples_example(&mut buf, "Open a file", &[
"tess Cargo.toml",
"tess -N -S src/main.rs",
"tess --tab-width 4 Makefile",
]);
examples_section(&mut buf, "Piped input");
examples_example(&mut buf, "Pipe output through tess", &[
"git log | tess",
"cargo build 2>&1 | tess",
"ls --color=always | tess",
]);
examples_section(&mut buf, "Big files: --head / --tail");
examples_example(&mut buf, "Cheap views of large files", &[
"tess --head 50 schema.sql",
"tess --tail 1000 huge.log",
"tess -f --tail 1000 huge.log",
]);
examples_section(&mut buf, "Pretty-printing structured files");
examples_example(&mut buf, "Auto-detect from extension or force a content type", &[
"tess --prettify config.json",
"tess --prettify schema.yaml",
"tess --prettify Cargo.toml",
"tess --prettify page.html",
"tess --prettify rows.csv",
"tess --content-type=json data.bin",
]);
examples_note(&mut buf, "Inside the pager: Shift-P toggles, -Pj/y/t/x/h/c/a/r switches type.");
examples_section(&mut buf, "Following live output");
examples_example(&mut buf, "Watch a log file or a file rewritten in place", &[
"tess -f /var/log/syslog",
"tail -F /var/log/access.log | tess -f",
"tess --live src/main.rs",
"tess --live notes.md",
]);
examples_section(&mut buf, "Plain-text grep (no format needed)");
examples_example(&mut buf, "Filter lines by regex — no format required", &[
r"tess --grep error access.log",
r"tess --grep error --grep '^\[' access.log",
]);
examples_section(&mut buf, "Apache log analysis (built-in formats)");
examples_example(&mut buf, "Filter by status code, URL, or combine grep and filter", &[
"tess --format apache-combined --filter status~^5 access.log",
"tess --format apache-combined --filter status~^5 --filter url~^/api/ access.log",
"tess --format apache-combined --filter 'status!=200' access.log",
"tess --format apache-combined --filter 'status>=500' access.log",
"tess --format apache-combined --filter status~^5 --dim access.log",
"tess -f --tail 100 --format apache-combined --filter status~^5 access.log",
"tess --format apache-combined --filter status=500 --grep timeout access.log",
]);
examples_note(&mut buf, "Single-quote filters that use `!` or `<`/`>` — bash's history expansion eats `!`, and `<`/`>` are I/O redirection without quotes.");
examples_section(&mut buf, "Batch (non-interactive) output");
examples_example(&mut buf, "Write filtered output to a file or stdout", &[
"tess --filter status~^5 --format apache-combined -o errors.log access.log",
"tess --head 1000 --stdout huge.log | grep -c something",
"tess --prettify --stdout config.json > pretty.json",
"tess -f --format app --filter level=ERROR -o /tmp/live-errors.log app.log",
]);
examples_section(&mut buf, "Display templates (reformat each line)");
examples_example(&mut buf, "Reformat matched lines with a custom template", &[
"tess --format apache-combined --display '[<status>] <method> <url>' access.log",
"tess --format app --display '[<ts>] <level> <msg>' --filter 'level>=WARN' app.log",
r"tess --format apache-combined --display '<status>: <url>' \",
r" --filter 'status>=500' -o errors.log access.log",
]);
examples_note(&mut buf, "Or set the default per format: add `display = '[<ts>] <level> <msg>'` under `[format.app]` in ~/.config/tess/formats.toml.");
examples_section(&mut buf, "Custom format (declare in ~/.config/tess/formats.toml)");
examples_example(&mut buf, "Define a named format with a regex and optional display template", &[
r"# [format.app]",
r"# regex = '^(?P<ts>\S+ \S+) (?P<level>\w+) \[(?P<reqid>[0-9a-f]+)\] (?P<msg>.*)$'",
"",
"tess --list-formats",
"tess --format app --filter level=ERROR app.log",
"tess --format app --filter 'level~^(ERROR|WARN)$' app.log",
"tess -f --tail 200 --format app --filter level=ERROR app.log",
]);
examples_section(&mut buf, "Groups (shortcut bundles, also in formats.toml)");
examples_example(&mut buf, "Expand --<groupname> into a fixed flag bundle", &[
"# [group.errorlog]",
r#"# format = "app""#,
r#"# file = "/var/log/myapp/app.log""#,
"# follow = true",
"# tail = 1000",
r#"# filter = ["level=ERROR"]"#,
"",
"tess --errorlog",
"tess --errorlog 'msg~timeout'",
"tess --errorlog --tail 50",
]);
examples_section(&mut buf, "Interactive keys (inside tess)");
examples_example(&mut buf, "Search, scroll, and toggle display options", &[
"/ pat <Enter> forward regex search n / N repeat search",
"? pat <Enter> backward regex search g / G top / bottom",
"Space / b page down / up Shift-F toggle follow",
"-N / -S / -F toggle line numbers / chop / follow",
"R force-reload from disk (with --live)",
"q quit",
]);
let _ = writeln!(buf, " {}", "See `tess --manual` for the full reference, or `tess --help` for a flag list.".dimmed());
let _ = writeln!(buf);
buf
}
fn main() -> ExitCode {
install_panic_hook();
match real_main() {
Ok(()) => ExitCode::from(0),
Err(e) => {
eprintln!("{}", e);
ExitCode::from(e.exit_code() as u8)
}
}
}
#[cfg(unix)]
fn redirect_stdin_to_tty() -> std::io::Result<()> {
use std::os::unix::io::AsRawFd;
let tty = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")?;
unsafe {
if libc::dup2(tty.as_raw_fd(), libc::STDIN_FILENO) < 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
}
fn resolve_ansi_mode(args: &Args) -> tess::render::AnsiMode {
use tess::render::AnsiMode;
if args.raw_control_chars {
return AnsiMode::Raw;
}
if args.no_color {
return AnsiMode::Strict;
}
if let Ok(v) = std::env::var("NO_COLOR") {
if !v.is_empty() {
return AnsiMode::Strict;
}
}
if std::env::var("CLICOLOR").as_deref() == Ok("0") {
return AnsiMode::Strict;
}
AnsiMode::Interpret
}
fn resolve_truecolor(args: &Args) -> std::result::Result<bool, String> {
use tess::render::TrueColor;
match args.truecolor.as_str() {
"always" => Ok(true),
"never" => Ok(false),
"auto" => Ok(TrueColor::Auto.resolve()),
other => Err(format!(
"--truecolor: unknown mode `{other}` (expected auto, always, never)"
)),
}
}
fn page_bytes(label: &str, content: &[u8], ansi_mode: tess::render::AnsiMode) -> Result<()> {
let src = MockSource::new();
src.append(content);
src.finish();
#[cfg(unix)]
if !io::stdin().is_terminal() {
let _ = redirect_stdin_to_tty();
}
let sigterm = install_signal_flag();
let _guard = TerminalGuard::enter(false, true)
.map_err(|e| Error::Runtime(format!("terminal init: {}", e)))?;
let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));
let mut viewport = Viewport::new(cols, rows, label.to_string());
viewport.set_ansi_mode(ansi_mode);
let idx = LineIndex::new();
let keymap = tess::keys::KeyMap::load_layered()
.unwrap_or_else(|_| tess::keys::KeyMap::empty());
let file_set = tess::file_set::FileSet::new(vec![std::path::PathBuf::from(label)]);
let stub_args = Args::parse_from(["tess"]);
app::run(Box::new(src), viewport, idx, sigterm, RebuildSpec::default(), keymap, file_set, None, stub_args, None, None)?;
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum PlusCmd {
GotoBottom,
Goto(usize),
SearchForward(String),
SearchBackward(String),
}
fn parse_header_spec(s: &str) -> std::result::Result<(usize, usize), String> {
let parts: Vec<&str> = s.split(',').collect();
match parts.as_slice() {
[l] => l.parse::<usize>()
.map(|n| (n, 0))
.map_err(|_| format!("--header: not a number `{l}`")),
[l, c] => {
let l = l.parse::<usize>()
.map_err(|_| format!("--header: bad L `{l}`"))?;
let c = c.parse::<usize>()
.map_err(|_| format!("--header: bad C `{c}`"))?;
Ok((l, c))
}
_ => Err("--header takes L or L,C".to_string()),
}
}
fn parse_plus_cmd(s: &str) -> std::result::Result<PlusCmd, String> {
debug_assert!(s.starts_with('+') && s.len() > 1);
let rest = &s[1..];
if rest == "G" {
return Ok(PlusCmd::GotoBottom);
}
if let Some(p) = rest.strip_prefix('/') {
return Ok(PlusCmd::SearchForward(p.to_string()));
}
if let Some(p) = rest.strip_prefix('?') {
return Ok(PlusCmd::SearchBackward(p.to_string()));
}
if let Ok(n) = rest.parse::<usize>() {
return Ok(PlusCmd::Goto(n));
}
Err(format!(
"unrecognized startup command `{s}` (expected +G, +N, +/pat, or +?pat)"
))
}
fn real_main() -> Result<()> {
let raw_argv: Vec<String> = std::env::args().collect();
let plus_cmds: Vec<String> = raw_argv
.iter()
.skip(1)
.filter(|a| a.starts_with('+') && a.len() > 1)
.cloned()
.collect();
let cleaned_argv: Vec<String> = raw_argv
.into_iter()
.enumerate()
.filter(|(i, a)| *i == 0 || !(a.starts_with('+') && a.len() > 1))
.map(|(_, a)| a)
.collect();
let groups = format::load_groups().map_err(Error::Runtime)?;
let argv = format::expand_argv(cleaned_argv, &groups);
let args = Args::parse_from(argv);
let parsed_plus_cmds: Vec<PlusCmd> = plus_cmds
.iter()
.map(|s| parse_plus_cmd(s).map_err(Error::Runtime))
.collect::<Result<Vec<_>>>()?;
let ansi_mode = resolve_ansi_mode(&args);
resolve_truecolor(&args).map_err(Error::Runtime)?;
if args.follow_name && !args.follow {
eprintln!("tess: --follow-name has no effect without -f / --follow");
}
if args.manual {
if io::stdout().is_terminal() {
return page_bytes("(manual)", MANUAL_TEXT.as_bytes(), ansi_mode);
}
print!("{}", MANUAL_TEXT);
return Ok(());
}
if args.examples {
let is_tty = io::stdout().is_terminal();
colored::control::set_override(is_tty);
let text = build_examples_text();
if is_tty {
return page_bytes("(examples)", text.as_bytes(), ansi_mode);
}
print!("{}", text);
return Ok(());
}
if args.list_formats {
let formats = format::load_all().map_err(Error::Runtime)?;
format::print_format_list(&formats);
return Ok(());
}
if !args.filter.is_empty() && args.format.is_none() {
return Err(Error::Runtime(
"--filter requires --format".to_string(),
));
}
if args.display.is_some() && args.format.is_none() {
return Err(Error::Runtime(
"--display requires --format".to_string(),
));
}
if args.dim && args.filter.is_empty() && args.grep.is_empty() {
return Err(Error::Runtime(
"--dim has no effect without --filter or --grep".to_string(),
));
}
if args.live && args.files.is_empty() {
return Err(Error::Runtime(
"--live requires a file path (stdin can't be re-stat'd)".to_string(),
));
}
let batch_destination: Option<BatchDestination> = if args.stdout {
Some(BatchDestination::Stdout)
} else if let Some(path) = args.output.as_deref() {
if path == "-" { Some(BatchDestination::Stdout) }
else { Some(BatchDestination::File(PathBuf::from(path))) }
} else {
None
};
if batch_destination.is_some() && args.live {
return Err(Error::Runtime(
"--output / --stdout is not compatible with --live".to_string(),
));
}
let explicit_content_type: Option<PrettifyMode> = match args.content_type.as_deref() {
Some(name) => prettify::parse_content_type(name).map_err(Error::Runtime)?,
None => None,
};
let want_prettify = match explicit_content_type {
Some(PrettifyMode::Off) => false,
Some(_) => true,
None => args.prettify,
};
if want_prettify {
if args.follow {
return Err(Error::Runtime(
"--prettify is not supported with --follow (streaming partial \
documents can't be parsed)".to_string(),
));
}
if args.live {
return Err(Error::Runtime(
"--prettify is not supported with --live".to_string(),
));
}
if !args.filter.is_empty() {
return Err(Error::Runtime(
"--prettify is not supported with --filter".to_string(),
));
}
if !args.grep.is_empty() {
return Err(Error::Runtime(
"--prettify is not supported with --grep".to_string(),
));
}
if args.display.is_some() {
return Err(Error::Runtime(
"--prettify is not supported with --display".to_string(),
));
}
}
let preprocessor: Option<tess::preprocess::Preprocessor> = if args.no_preprocess {
None
} else {
let raw = args.preprocess.clone()
.or_else(|| std::env::var("LESSOPEN").ok());
match raw {
Some(r) => Some(tess::preprocess::Preprocessor::parse(&r)
.map_err(Error::Runtime)?),
None => None,
}
};
let file_set = tess::file_set::FileSet::new(args.files.clone());
let mut consumed_stdin = false;
let mut source_supports_tail = true;
let mut preprocess_failure: Option<String> = None;
let (src, label): (Box<dyn Source>, String) = match args.files.first() {
Some(path) => {
let (s, l, pf) = tess::open::open_source_for_path(path, &args, preprocessor.as_ref())?;
preprocess_failure = pf;
(s, l)
}
None if !io::stdin().is_terminal() => {
let ss = if args.follow {
source_supports_tail = false;
StdinSource::spawn_streaming()
.map_err(|e| Error::Runtime(format!("stdin: {}", e)))?
} else {
StdinSource::read_all()
.map_err(|e| Error::Runtime(format!("stdin: {}", e)))?
};
consumed_stdin = true;
(Box::new(ss), "(stdin)".to_string())
}
None => {
return Err(Error::NoInput);
}
};
let (src, prettify_label): (Box<dyn Source>, Option<String>) = if want_prettify {
let head = src.bytes(0..src.len().min(512)).to_vec();
let path_for_detect = args.files.first().map(|p| p.as_path());
let resolved = prettify::resolve(explicit_content_type, path_for_detect, &head);
match resolved {
ResolvedType::Mode(PrettifyMode::Off) => (src, None),
ResolvedType::Mode(mode) => {
let label = mode.label().to_string();
let wrapped = TransformingSource::wrap(src, mode);
if let Some(err) = wrapped.last_error() {
eprintln!("tess: prettify ({label}) failed: {err} \u{2014} showing raw");
(Box::new(wrapped), Some(format!("{label}:err")))
} else {
(Box::new(wrapped), Some(label))
}
}
ResolvedType::Undetected => {
eprintln!(
"tess: --prettify requested but content type could not be detected; \
showing raw (use --content-type=NAME to override)"
);
(src, None)
}
}
} else {
(src, None)
};
let mut idx = match args.tail {
Some(n) if source_supports_tail => {
let off = find_tail_offset(src.as_ref(), n);
LineIndex::new_starting_at(off)
}
Some(_) => {
eprintln!("tess: --tail is not supported on streaming stdin (-f); ignoring");
LineIndex::new()
}
None => LineIndex::new(),
};
if let Some(n) = args.head {
idx.set_head_cap(n);
}
let record_start_regex: Option<regex::bytes::Regex> = {
let fmt_record_start: Option<String> = if let Some(name) = args.format.as_deref() {
let formats = format::load_all().map_err(Error::Runtime)?;
formats.get(name).and_then(|f| {
f.record_start.as_ref().map(|r| r.as_str().to_string())
})
} else {
None
};
let record_start_pattern: Option<String> = args.record_start
.clone()
.or(fmt_record_start);
if let Some(pat) = record_start_pattern {
let re = regex::bytes::Regex::new(&pat)
.map_err(|e| Error::Runtime(format!("--record-start: {e}")))?;
idx.set_record_start(re.clone());
Some(re)
} else {
None
}
};
#[cfg(unix)]
if consumed_stdin {
let _ = redirect_stdin_to_tty();
}
let case_mode = if args.IGNORE_CASE {
tess::viewport::CaseMode::Insensitive
} else if args.ignore_case {
tess::viewport::CaseMode::Smart
} else {
tess::viewport::CaseMode::Sensitive
};
let compiled_grep = if !args.grep.is_empty() {
Some(
GrepPredicate::compile(&args.grep, case_mode)
.map_err(Error::Runtime)?,
)
} else {
None
};
let (compiled_filter, display_renderer) = if let Some(name) = args.format.as_deref() {
let formats = format::load_all().map_err(Error::Runtime)?;
let fmt = formats.get(name).ok_or_else(|| {
Error::Runtime(format!(
"unknown format `{name}` (run --list-formats to see available)"
))
})?;
let filter = if !args.filter.is_empty() {
let specs: Vec<FilterSpec> = args.filter.iter()
.map(|s| FilterSpec::parse(s).map_err(Error::Runtime))
.collect::<Result<_>>()?;
Some(CompiledFilter::compile(fmt, specs, case_mode).map_err(Error::Runtime)?)
} else {
None
};
let template = if let Some(cli_tmpl) = args.display.as_deref() {
Some(
format::DisplayTemplate::compile(cli_tmpl, &fmt.field_names)
.map_err(|e| Error::Runtime(format!("--display: {e}")))?,
)
} else {
fmt.display.clone()
};
let renderer = template.map(|t| format::DisplayRenderer::new(t, fmt.regex.clone()));
(filter, renderer)
} else {
(None, None)
};
let sigterm = install_signal_flag();
if let Some(destination) = batch_destination {
let _ = prettify_label;
#[cfg(feature = "image")]
if !args.hex && !args.no_image {
let head_len = src.len().min(64);
let head = src.bytes(0..head_len);
if tess::image_render::sniff_image_format(&head).is_some() {
let all = src.bytes(0..src.len());
let rgba = tess::image_render::decode_image(&all)
.map_err(|e| Error::Runtime(format!("image decode failed: {e}")))?;
let style = if args.blocks {
tess::image_render::AsciiStyle::Blocks
} else {
tess::image_render::AsciiStyle::Ramp
};
let width = args.image_width.map(|w| w.clamp(1, u16::MAX as usize) as u16).unwrap_or(80);
let grid = tess::image_render::render_image(&rgba, width, style, !args.no_color);
let mut w: Box<dyn std::io::Write> = match &destination {
BatchDestination::Stdout => Box::new(std::io::stdout().lock()),
BatchDestination::File(p) => Box::new(std::fs::File::create(p)
.map_err(|e| Error::Runtime(format!("{}: {e}", p.display())))?),
};
tess::image_export::write_grid(&mut w, &grid, !args.no_color)
.map_err(|e| Error::Runtime(e.to_string()))?;
return Ok(());
}
}
let spec = BatchSpec {
destination,
follow: args.follow,
poll_interval: std::time::Duration::from_millis(250),
};
return batch::run(src, idx, compiled_filter, compiled_grep, display_renderer, spec, sigterm);
}
if args.quit_if_one_screen && !args.follow && src.is_complete() {
let (_cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));
let body_rows = rows.saturating_sub(1) as usize;
let total_len = src.len();
let bytes = src.bytes(0..total_len);
let trailing_nl = bytes.last().map_or(true, |&b| b == b'\n');
let line_count = bytes.iter().filter(|&&b| b == b'\n').count()
+ if trailing_nl { 0 } else { 1 };
if line_count <= body_rows {
use std::io::Write;
let mut stdout = std::io::stdout().lock();
stdout.write_all(&bytes).map_err(|e| Error::Runtime(format!("stdout: {e}")))?;
if !trailing_nl {
let _ = stdout.write_all(b"\n");
}
return Ok(());
}
}
let _guard = TerminalGuard::enter(args.mouse, !args.no_init)
.map_err(|e| Error::Runtime(format!("terminal init: {}", e)))?;
let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));
let mut viewport = Viewport::new(cols, rows, label);
if args.line_numbers { viewport.toggle_line_numbers(); }
if args.chop { viewport.toggle_chop(); }
viewport.opts.tab_width = args.tab_width;
viewport.set_follow_mode(args.follow);
viewport.set_live_mode(args.live);
viewport.set_prettify_label(prettify_label);
if let Some(f) = compiled_filter {
viewport.set_filter(Some(f));
}
if let Some(g) = compiled_grep {
viewport.set_grep(Some(g));
}
if args.dim {
viewport.set_dim_mode(true);
}
if let Some(d) = display_renderer {
viewport.set_display(Some(d));
}
if args.hex {
viewport.set_hex_mode(true);
let bpg = tess::hex::hex_chars_to_bytes_per_group(args.hex_group)
.ok_or_else(|| Error::Runtime(format!(
"--hex-group must be one of 2, 4, 8, 16, 32 (got {})",
args.hex_group
)))?;
viewport.set_hex_group_size(bpg);
}
viewport.set_ansi_mode(ansi_mode);
viewport.set_case_mode(case_mode);
viewport.set_hilite_search(!args.no_hilite_search);
let qae = if args.QUIT_AT_EOF {
tess::viewport::QuitAtEof::First
} else if args.quit_at_eof {
tess::viewport::QuitAtEof::Second
} else {
tess::viewport::QuitAtEof::Off
};
viewport.set_quit_at_eof(qae);
viewport.set_squeeze_blanks(args.squeeze_blanks);
if let Some(spec) = args.header.as_deref() {
let (lines, cols) = parse_header_spec(spec).map_err(Error::Runtime)?;
viewport.set_header(lines, cols);
}
viewport.opts.rscroll_char = args.rscroll.chars().next();
viewport.opts.word_wrap = args.word_wrap;
viewport.set_page_size(args.window);
viewport.set_preprocess_failure(preprocess_failure);
{
let (fmt_prompt, fmt_prompt_style): (Option<tess::prompt::ParsedPrompt>, Option<tess::ansi::Style>) =
if let Some(name) = args.format.as_deref() {
let formats = format::load_all().map_err(Error::Runtime)?;
let entry = formats.get(name);
(
entry.and_then(|f| f.prompt.clone()),
entry.and_then(|f| f.prompt_style),
)
} else {
(None, None)
};
let prompt_template: Option<tess::prompt::ParsedPrompt> = match args.prompt.as_deref() {
Some(s) => Some(tess::prompt::ParsedPrompt::parse(s)
.map_err(|e| Error::Runtime(format!("--prompt: {e}")))?),
None => fmt_prompt,
};
let prompt_active = prompt_template.is_some();
viewport.set_prompt(prompt_template);
let status_style_base = tess::style_spec::parse(&args.status_style)
.map_err(|e| Error::Runtime(format!("--status-style: {e}")))?;
let cli_prompt_style = if args.prompt_style.trim().is_empty() {
None
} else {
Some(tess::style_spec::parse(&args.prompt_style)
.map_err(|e| Error::Runtime(format!("--prompt-style: {e}")))?)
};
let resolved_status_style = if prompt_active {
cli_prompt_style
.or(fmt_prompt_style)
.unwrap_or(status_style_base)
} else {
status_style_base
};
viewport.set_status_style(resolved_status_style);
}
viewport.set_format_label(args.format.clone());
viewport.set_file_index(0, file_set.len());
let rebuild_spec = RebuildSpec {
head: args.head,
tail: if source_supports_tail { args.tail } else { None },
};
let keymap = tess::keys::KeyMap::load_layered()
.map_err(Error::Runtime)?;
let tag_file: Option<tess::tags::TagFile> = if let Some(path) = &args.tag_file {
match tess::tags::TagFile::load(path) {
Ok(tf) => Some(tf),
Err(e) => {
eprintln!("tess: {e}");
std::process::exit(1);
}
}
} else {
let start = args
.files
.first()
.map(|p| p.as_path())
.unwrap_or_else(|| std::path::Path::new("."));
if let Some(found) = tess::tags::TagFile::find_walking_up(start) {
tess::tags::TagFile::load(&found).ok()
} else {
None
}
};
if args.tag.is_some() && tag_file.is_none() {
eprintln!("tess: tags file not found (required by -t)");
std::process::exit(1);
}
for cmd in &parsed_plus_cmds {
match cmd {
PlusCmd::Goto(n) if *n > 0 => viewport.goto_line(n - 1, src.as_ref(), &mut idx),
PlusCmd::Goto(_) => viewport.goto_top(),
PlusCmd::GotoBottom => viewport.goto_bottom(src.as_ref(), &mut idx),
PlusCmd::SearchForward(p) => {
viewport
.set_search(p.clone(), tess::viewport::SearchDirection::Forward)
.map_err(|e| Error::Runtime(format!("+/{p}: {e}")))?;
viewport.search_repeat(src.as_ref(), &mut idx, false);
}
PlusCmd::SearchBackward(p) => {
viewport
.set_search(p.clone(), tess::viewport::SearchDirection::Backward)
.map_err(|e| Error::Runtime(format!("+?{p}: {e}")))?;
viewport.search_repeat(src.as_ref(), &mut idx, false);
}
}
}
#[cfg(feature = "image")]
if !args.hex && !args.no_image {
let head_len = src.len().min(64);
let head = src.bytes(0..head_len);
if let Some(fmt) = tess::image_render::sniff_image_format(&head) {
let all = src.bytes(0..src.len());
match tess::image_render::decode_image(&all) {
Ok(rgba) => {
let style = if args.blocks {
tess::image_render::AsciiStyle::Blocks
} else {
tess::image_render::AsciiStyle::Ramp
};
viewport.set_image(rgba, fmt, style, args.image_width);
viewport.set_image_no_color(args.no_color);
}
Err(e) => {
eprintln!("tess: image decode failed ({e}); showing raw");
}
}
}
}
app::run(src, viewport, idx, sigterm, rebuild_spec, keymap, file_set, record_start_regex, args, preprocessor, tag_file)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_plus_g_is_goto_bottom() {
assert_eq!(parse_plus_cmd("+G"), Ok(PlusCmd::GotoBottom));
}
#[test]
fn parse_plus_num_is_goto() {
assert_eq!(parse_plus_cmd("+42"), Ok(PlusCmd::Goto(42)));
assert_eq!(parse_plus_cmd("+1"), Ok(PlusCmd::Goto(1)));
}
#[test]
fn parse_plus_slash_is_search_forward() {
assert_eq!(
parse_plus_cmd("+/error"),
Ok(PlusCmd::SearchForward("error".into()))
);
}
#[test]
fn parse_plus_question_is_search_backward() {
assert_eq!(
parse_plus_cmd("+?warning"),
Ok(PlusCmd::SearchBackward("warning".into()))
);
}
#[test]
fn parse_plus_unknown_errors() {
assert!(parse_plus_cmd("+xyzzy").is_err());
assert!(parse_plus_cmd("+abc").is_err());
}
#[test]
fn parse_plus_empty_pattern_still_parses() {
assert_eq!(
parse_plus_cmd("+/"),
Ok(PlusCmd::SearchForward("".into()))
);
}
}