use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::PathBuf;
use crate::process::session::{ProcessSession, ProcessStatus};
pub struct ServiceLogView {
pub session: ProcessSession,
pub log_path: PathBuf,
pub log_content: String,
pub file_position: u64,
pub scroll_offset: usize,
pub search_query: Option<String>,
}
impl ServiceLogView {
pub fn new(session: ProcessSession, log_path: PathBuf) -> Result<Self, std::io::Error> {
let mut view = Self {
session,
log_path,
log_content: String::new(),
file_position: 0,
scroll_offset: 0,
search_query: None,
};
view.update()?;
Ok(view)
}
pub fn update(&mut self) -> Result<(), std::io::Error> {
let mut file = match File::open(&self.log_path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(());
}
Err(e) => return Err(e),
};
file.seek(SeekFrom::Start(self.file_position))?;
let mut buffer = Vec::new();
let bytes_read = file.read_to_end(&mut buffer)?;
if bytes_read > 0 {
self.log_content.push_str(&String::from_utf8_lossy(&buffer));
self.file_position += bytes_read as u64;
const MAX_CONTENT_SIZE: usize = 50 * 1024;
if self.log_content.len() > MAX_CONTENT_SIZE {
let raw_start = self.log_content.len() - MAX_CONTENT_SIZE;
let mut start = raw_start;
while start < self.log_content.len() && !self.log_content.is_char_boundary(start) {
start += 1;
}
if let Some(newline_offset) = self.log_content[start..].find('\n') {
start += newline_offset + 1;
}
self.log_content = self.log_content[start..].to_string();
}
}
Ok(())
}
pub fn name(&self) -> &str {
&self.session.name
}
pub fn status(&self) -> &ProcessStatus {
&self.session.status
}
pub fn content(&self) -> &str {
&self.log_content
}
pub fn filtered_content(&self) -> String {
match &self.search_query {
Some(query) if !query.is_empty() => {
let q_lower = query.to_lowercase();
let matching_lines: Vec<&str> = self
.log_content
.lines()
.filter(|line| line.to_lowercase().contains(&q_lower))
.collect();
if matching_lines.is_empty() {
format!("[No lines match filter: \"{}\"]", query)
} else {
matching_lines.join("\n")
}
}
_ => self.log_content.clone(),
}
}
pub fn match_count(&self) -> Option<usize> {
let query = self.search_query.as_deref()?;
if query.is_empty() {
return None;
}
let q_lower = query.to_lowercase();
Some(
self.log_content
.lines()
.filter(|line| line.to_lowercase().contains(&q_lower))
.count(),
)
}
pub fn set_search(&mut self, query: Option<String>) {
self.search_query = query;
self.scroll_offset = 0;
}
pub fn scroll_up(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_add(lines);
}
pub fn scroll_down(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_sub(lines);
}
pub fn scroll_to_bottom(&mut self) {
self.scroll_offset = 0;
}
pub fn detected_url(&self) -> Option<String> {
for line in self.log_content.lines().rev() {
if let Some(pos) = line
.find("http://localhost:")
.or_else(|| line.find("http://127.0.0.1:"))
.or_else(|| line.find("https://localhost:"))
{
let tail = &line[pos..];
let end = tail
.find(|c: char| c.is_whitespace() || c == '"' || c == '\'' || c == ')' || c == ']' || c == '>')
.unwrap_or(tail.len());
let url = &tail[..end];
let clean = url.trim_end_matches(['.', ';', ',']);
if clean.len() >= 16 {
return Some(clean.to_string());
}
}
}
None
}
}