use crossterm::event::{Event, KeyCode, KeyModifiers};
use ratatui::{
backend::Backend,
layout::{Constraint, Direction, Layout},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Terminal,
};
use std::{io, time::Duration};
use crate::shared::config::Config;
use crate::shared::display::log_tabs::{render_log_tabs, LogTabs};
use crate::shared::display::log_view::{render_log_view, LogLevel, LogView};
use crate::shared::docker::docker_log_watcher::DockerLogManager;
use crate::shared::input::input_watcher::InputWatcher;
pub fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
log_manager: &mut DockerLogManager,
) -> Result<(), io::Error> {
let input_watcher = InputWatcher::new();
let config = Config::default();
let mut log_views: Vec<LogView> = Vec::new();
let container_names: Vec<String> = (0..log_manager.watcher_count())
.filter_map(|i| {
log_manager
.get_watcher(i)
.map(|w| w.container_name().to_string())
})
.collect();
for _ in 0..container_names.len() {
log_views.push(LogView::new(1000));
}
let mut log_tabs = LogTabs::new(container_names);
let mut log_area_height = 0;
loop {
terminal.draw(|f| {
let size = f.area();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), ]
.as_ref(),
)
.split(size);
render_log_tabs::<B>(f, &log_tabs, chunks[0], &config);
if let Some(active_tab) = log_tabs.index.checked_sub(0) {
if let Some(watcher) = log_manager.get_watcher(active_tab) {
if active_tab < log_views.len() {
let current_log_view = &mut log_views[active_tab];
let current_logs = watcher.get_logs();
if current_log_view.get_log_count() != current_logs.len() {
let scroll_pos = current_log_view.get_scroll_position();
*current_log_view = LogView::new(1000);
for log_line in current_logs {
current_log_view.add_log(log_line, LogLevel::Info);
}
if scroll_pos < current_log_view.get_log_count() {
current_log_view.set_scroll_position(scroll_pos);
}
}
render_log_view::<B>(f, &mut *current_log_view, chunks[1], &config);
log_area_height = chunks[1].height as usize;
}
}
}
let help_text = vec![
Span::styled(
"q/Ctrl+C",
Style::default()
.fg(config.get_color("message_error"))
.add_modifier(Modifier::BOLD),
),
Span::raw(": Quit | "),
Span::styled(
"←/→",
Style::default()
.fg(config.get_color("message_warning"))
.add_modifier(Modifier::BOLD),
),
Span::raw(": Switch Container | "),
Span::styled(
"↑/↓",
Style::default()
.fg(config.get_color("message_warning"))
.add_modifier(Modifier::BOLD),
),
Span::raw(": Scroll | "),
Span::styled(
"PgUp/PgDn",
Style::default()
.fg(config.get_color("message_warning"))
.add_modifier(Modifier::BOLD),
),
Span::raw(": Page Scroll | "),
Span::styled(
"Esc",
Style::default()
.fg(config.get_color("message_success"))
.add_modifier(Modifier::BOLD),
),
Span::raw(": Follow | "),
Span::styled(
"r",
Style::default()
.fg(config.get_color("message_success"))
.add_modifier(Modifier::BOLD),
),
Span::raw(": Refresh"),
];
let help_bar = Paragraph::new(Line::from(help_text))
.block(Block::default().borders(Borders::ALL).title("Hotkeys"));
f.render_widget(help_bar, chunks[2]);
})?;
if let Ok(Event::Key(key)) = input_watcher.try_recv() {
match key.code {
KeyCode::Char('q') => {
return Ok(());
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
return Ok(());
}
KeyCode::Right => log_tabs.next(),
KeyCode::Left => log_tabs.previous(),
KeyCode::Up => {
if let Some(active_tab) = log_tabs.index.checked_sub(0) {
if active_tab < log_views.len() {
log_views[active_tab].scroll_up();
}
}
}
KeyCode::Down => {
if let Some(active_tab) = log_tabs.index.checked_sub(0) {
if active_tab < log_views.len() {
log_views[active_tab].scroll_down();
}
}
}
KeyCode::Home => {
if let Some(active_tab) = log_tabs.index.checked_sub(0) {
if active_tab < log_views.len() {
log_views[active_tab].scroll_to_top();
}
}
}
KeyCode::End => {
if let Some(active_tab) = log_tabs.index.checked_sub(0) {
if active_tab < log_views.len() {
log_views[active_tab].scroll_to_bottom();
}
}
}
KeyCode::Char('r') => {
log_manager.refresh()?;
let container_names: Vec<String> = (0..log_manager.watcher_count())
.filter_map(|i| {
log_manager
.get_watcher(i)
.map(|w| w.container_name().to_string())
})
.collect();
log_tabs = LogTabs::new(container_names.clone());
log_views.clear();
for _ in 0..container_names.len() {
log_views.push(LogView::new(1000));
}
}
KeyCode::PageUp => {
if let Some(active_tab) = log_tabs.index.checked_sub(0) {
if active_tab < log_views.len() {
log_views[active_tab].page_up(log_area_height);
}
}
}
KeyCode::PageDown => {
if let Some(active_tab) = log_tabs.index.checked_sub(0) {
if active_tab < log_views.len() {
log_views[active_tab].page_down(log_area_height);
}
}
}
KeyCode::Esc => {
if let Some(active_tab) = log_tabs.index.checked_sub(0) {
if active_tab < log_views.len() {
log_views[active_tab].enable_follow();
}
}
}
_ => {}
}
}
std::thread::sleep(Duration::from_millis(10));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::docker::docker_log_watcher::DockerLogManager;
use std::sync::{Arc, Mutex};
use std::thread;
#[test]
fn test_log_manager_creation() {
let log_manager = DockerLogManager::new();
assert_eq!(log_manager.watcher_count(), 0);
}
#[test]
fn test_log_view_scrolling() {
let mut log_view = LogView::new(10);
for i in 0..20 {
log_view.add_log(format!("Log line {}", i), LogLevel::Info);
}
log_view.scroll_to_top();
assert_eq!(log_view.get_scroll_position(), 0);
log_view.scroll_up();
log_view.scroll_up();
assert_eq!(log_view.get_scroll_position(), 0);
log_view.scroll_down();
assert_eq!(log_view.get_scroll_position(), 1);
log_view.scroll_to_bottom();
assert!(log_view.get_scroll_position() > 0);
log_view.scroll_to_top();
assert_eq!(log_view.get_scroll_position(), 0);
}
#[test]
fn test_log_tabs_navigation() {
let container_names = vec![
"container1".to_string(),
"container2".to_string(),
"container3".to_string(),
];
let mut log_tabs = LogTabs::new(container_names);
assert_eq!(log_tabs.index, 0);
log_tabs.next();
assert_eq!(log_tabs.index, 1);
log_tabs.previous();
assert_eq!(log_tabs.index, 0);
}
#[test]
fn test_log_manager_refresh() {
let _log_manager = DockerLogManager::new();
struct MockLogManager {
refresh_called: Arc<Mutex<bool>>,
}
impl MockLogManager {
fn new() -> Self {
MockLogManager {
refresh_called: Arc::new(Mutex::new(false)),
}
}
fn refresh(&self) {
let mut called = self.refresh_called.lock().unwrap();
*called = true;
}
fn was_refresh_called(&self) -> bool {
*self.refresh_called.lock().unwrap()
}
}
let mock = MockLogManager::new();
let mock_ref = Arc::new(mock);
let mock_clone = Arc::clone(&mock_ref);
thread::spawn(move || {
mock_clone.refresh();
})
.join()
.unwrap();
assert!(mock_ref.was_refresh_called());
}
}