use crate::config::{db_path, find_vyctor_root, load_config};
use crate::daemon::is_daemon_running;
use crate::storage::Storage;
use anyhow::{Context, Result};
use colored::Colorize;
fn open_storage_for_browse(root: &std::path::Path, dimensions: usize) -> Result<Storage> {
let db_file = db_path()?;
if !db_file.exists() {
anyhow::bail!("No index found. Run 'vyctor sync' to create one.");
}
match Storage::open_readonly(&db_file, dimensions) {
Ok(storage) => Ok(storage),
Err(e) => {
if let Some(pid) = is_daemon_running(root) {
println!(
"{} The watcher daemon (PID {}) is holding the database lock.",
"!".yellow(),
pid
);
println!();
println!("To browse the index, stop the daemon first:");
println!(" {} vyctor watch --stop", "$".dimmed());
println!();
println!("You can restart it later with:");
println!(" {} vyctor watch --daemon", "$".dimmed());
println!();
anyhow::bail!("Database locked by daemon");
}
Err(e).context("Failed to open database")
}
}
}
pub async fn list_files(filter: Option<&str>, show_hash: bool) -> Result<()> {
let root = find_vyctor_root()?;
let config = load_config()?;
let storage = open_storage_for_browse(&root, config.embedding.dimensions)?;
let files = storage.list_files_with_stats()?;
if files.is_empty() {
println!("{} No files indexed yet.", "!".yellow());
return Ok(());
}
let files: Vec<_> = if let Some(pattern) = filter {
files
.into_iter()
.filter(|f| f.path.contains(pattern))
.collect()
} else {
files
};
println!("{}", "Indexed Files".bold());
println!();
let max_path_len = files
.iter()
.map(|f| f.path.len())
.max()
.unwrap_or(20)
.min(60);
if show_hash {
println!(
"{:width$} {:>8} {:>10} {:>12} {}",
"Path".bold(),
"Chunks".bold(),
"Size".bold(),
"Hash".bold(),
"Last Indexed".bold(),
width = max_path_len
);
println!("{}", "-".repeat(max_path_len + 60));
} else {
println!(
"{:width$} {:>8} {:>10} {}",
"Path".bold(),
"Chunks".bold(),
"Size".bold(),
"Last Indexed".bold(),
width = max_path_len
);
println!("{}", "-".repeat(max_path_len + 40));
}
for file in &files {
let path_display = if file.path.len() > max_path_len {
format!("...{}", &file.path[file.path.len() - max_path_len + 3..])
} else {
file.path.clone()
};
if show_hash {
println!(
"{:width$} {:>8} {:>10} {:>12} {}",
path_display.cyan(),
file.chunk_count.to_string().green(),
format_bytes(file.total_size),
&file.content_hash[..12],
file.last_indexed.dimmed(),
width = max_path_len
);
} else {
println!(
"{:width$} {:>8} {:>10} {}",
path_display.cyan(),
file.chunk_count.to_string().green(),
format_bytes(file.total_size),
file.last_indexed.dimmed(),
width = max_path_len
);
}
}
println!();
println!(
"Total: {} files, {} chunks",
files.len().to_string().green(),
files
.iter()
.map(|f| f.chunk_count)
.sum::<usize>()
.to_string()
.cyan()
);
Ok(())
}
pub async fn show_chunks(
file: Option<&str>,
chunk_id: Option<i64>,
page: usize,
page_size: usize,
full: bool,
) -> Result<()> {
let root = find_vyctor_root()?;
let config = load_config()?;
let storage = open_storage_for_browse(&root, config.embedding.dimensions)?;
if let Some(id) = chunk_id {
return show_single_chunk(&storage, id);
}
let chunks = if let Some(file_path) = file {
storage.get_chunks_for_file(file_path)?
} else {
let offset = (page - 1) * page_size;
storage.get_chunks_paginated(offset, page_size, None)?
};
if chunks.is_empty() {
if file.is_some() {
println!("{} No chunks found for this file.", "!".yellow());
} else {
println!("{} No chunks found.", "!".yellow());
}
return Ok(());
}
println!("{}", "Chunks".bold());
if let Some(f) = file {
println!("File: {}", f.cyan());
} else {
println!("Page: {} (showing {} chunks)", page, chunks.len());
}
println!();
for chunk in &chunks {
print_chunk(chunk, full);
}
if file.is_none() {
println!();
println!(
"{} Use --page N to see more chunks, or --file PATH to filter by file",
"Tip:".dimmed()
);
}
Ok(())
}
fn show_single_chunk(storage: &Storage, chunk_id: i64) -> Result<()> {
match storage.get_chunk_by_id(chunk_id)? {
Some(chunk) => {
println!("{}", "Chunk Details".bold());
println!();
print_chunk(&chunk, true);
Ok(())
}
None => {
println!("{} Chunk with ID {} not found.", "!".yellow(), chunk_id);
Ok(())
}
}
}
fn print_chunk(chunk: &crate::storage::ChunkWithMetadata, full: bool) {
println!(
"{} {} [ID: {}, Lines {}-{}]",
format!("#{}", chunk.chunk_index).bold(),
chunk.file_path.cyan(),
chunk.id.to_string().dimmed(),
chunk.start_line,
chunk.end_line
);
if full {
println!();
for line in chunk.content.lines() {
println!(" {}", line.dimmed());
}
} else {
let preview_lines: Vec<&str> = chunk.content.lines().take(5).collect();
println!();
for line in &preview_lines {
let truncated = if line.len() > 100 {
format!("{}...", &line[..97])
} else {
line.to_string()
};
println!(" {}", truncated.dimmed());
}
let total_lines = chunk.content.lines().count();
if total_lines > 5 {
println!(
" {} more lines...",
format!("(+{})", total_lines - 5).dimmed()
);
}
}
println!();
}
pub async fn show_stats() -> Result<()> {
let root = find_vyctor_root()?;
let config = load_config()?;
let storage = open_storage_for_browse(&root, config.embedding.dimensions)?;
let stats = storage.get_stats()?;
println!("{}", "Index Overview".bold());
println!();
println!(" Files indexed: {}", stats.file_count.to_string().green());
println!(" Total chunks: {}", stats.chunk_count.to_string().cyan());
println!(
" Content size: {}",
format_bytes(stats.total_content_size)
);
if let Some(ref last) = stats.last_indexed {
println!(" Last indexed: {}", last.dimmed());
}
let ext_stats = storage.get_stats_by_extension()?;
if !ext_stats.is_empty() {
println!();
println!("{}", "By File Type".bold());
println!();
println!(
" {:>12} {:>8} {:>10} {:>12}",
"Extension".bold(),
"Files".bold(),
"Chunks".bold(),
"Size".bold()
);
println!(" {}", "-".repeat(46));
for stat in &ext_stats {
let ext_display = if stat.extension.len() > 12 {
format!("{}...", &stat.extension[..9])
} else {
stat.extension.clone()
};
println!(
" {:>12} {:>8} {:>10} {:>12}",
ext_display.cyan(),
stat.file_count.to_string().green(),
stat.chunk_count,
format_bytes(stat.total_size)
);
}
}
println!();
println!("{}", "Chunk Statistics".bold());
println!();
if stats.chunk_count > 0 {
let avg_chunk_size = stats.total_content_size / stats.chunk_count;
let avg_chunks_per_file = if stats.file_count > 0 {
stats.chunk_count as f64 / stats.file_count as f64
} else {
0.0
};
println!(" Avg chunk size: {}", format_bytes(avg_chunk_size));
println!(" Avg chunks per file: {:.1}", avg_chunks_per_file);
}
Ok(())
}
fn format_bytes(bytes: usize) -> String {
const KB: usize = 1024;
const MB: usize = KB * 1024;
const GB: usize = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}