logana 0.6.0

Turn any log source — files, compressed archives, Docker, or OTel streams — into structured data. Filter by pattern, field, or date range; annotate lines; bookmark findings; and export to Markdown, Jira, or AI assistants via the built-in MCP server.
Documentation
use super::App;
use super::TabState;
use super::VisibleLines;
use crate::db::LogManager;

impl App {
    pub(crate) fn apply_tab_defaults(&self, tab: &mut TabState) {
        tab.interaction.keybindings = self.keybindings.clone();
        tab.display.show_mode_bar = self.display.show_mode_bar;
        tab.display.show_borders = self.display.show_borders_default;
        tab.display.show_line_numbers = self.display.show_line_numbers;
        tab.display.show_sidebar = self.display.show_sidebar;
        tab.display.wrap = self.display.wrap;
        tab.display.sidebar_side = self.display.sidebar_side;
    }

    pub fn tab(&self) -> &TabState {
        &self.tabs[self.active_tab]
    }

    pub fn tab_mut(&mut self) -> &mut TabState {
        &mut self.tabs[self.active_tab]
    }

    pub async fn close_tab(&mut self) -> bool {
        use std::sync::atomic::Ordering;
        self.save_tab_context(&self.tabs[self.active_tab]).await;

        let tab = &self.tabs[self.active_tab];
        if let Some(ref h) = tab.search.handle {
            h.cancel.store(true, Ordering::Relaxed);
        }
        if let Some(ref h) = tab.filter.handle {
            h.cancel.store(true, Ordering::Relaxed);
        }

        if let Some(ref fls) = self.tabs[self.active_tab].load_state {
            fls.cancel.store(true, Ordering::Relaxed);
        }

        if self.tabs.len() <= 1 {
            return true;
        }
        self.tabs.remove(self.active_tab);
        if self.active_tab >= self.tabs.len() {
            self.active_tab = self.tabs.len() - 1;
        }
        false
    }

    pub(super) fn handle_open_merge_select(&mut self) {
        use crate::mode::merge_select_mode::MergeSelectMode;
        let (tabs, tab_indices): (Vec<(String, bool)>, Vec<usize>) = self
            .tabs
            .iter()
            .enumerate()
            .filter(|(_, t)| t.merged.is_none())
            .map(|(i, t)| ((t.title.clone(), i == self.active_tab), i))
            .unzip();
        if tabs.len() < 2 {
            self.tabs[self.active_tab].interaction.command_error =
                Some("No other tabs to merge".to_string());
            return;
        }
        self.tabs[self.active_tab].interaction.mode =
            Box::new(MergeSelectMode::new(tabs, tab_indices));
    }

    pub(crate) async fn open_merge_tab(&mut self, source_tab_indices: Vec<usize>) {
        use crate::ui::tab_state::merged::{MergedState, build_merged_index};

        let sources: Vec<crate::ingestion::FileReader> = source_tab_indices
            .iter()
            .map(|&i| self.tabs[i].file_reader.clone())
            .collect();
        let parsers: Vec<Option<std::sync::Arc<dyn crate::parser::LogFormatParser>>> =
            source_tab_indices
                .iter()
                .map(|&i| self.tabs[i].display.format.clone())
                .collect();
        let year_maps: Vec<Option<std::sync::Arc<crate::ui::tab_state::year_map::YearMap>>> =
            source_tab_indices
                .iter()
                .map(|&i| self.tabs[i].year_map.clone())
                .collect();
        let continuation_maps: Vec<Option<std::sync::Arc<Vec<usize>>>> = source_tab_indices
            .iter()
            .map(|&i| self.tabs[i].continuation_map.clone())
            .collect();
        let source_labels: Vec<String> = source_tab_indices
            .iter()
            .map(|&i| self.tabs[i].title.clone())
            .collect();

        let entries = build_merged_index(&sources, &parsers, &year_maps, &continuation_maps);
        let source_line_counts: Vec<usize> = sources.iter().map(|s| s.line_count()).collect();
        let label_col_width = source_labels.iter().map(|l| l.len()).max().unwrap_or(0);

        let visible_count = entries.len();
        let entries_arc = std::sync::Arc::new(entries);
        let sources_arc = std::sync::Arc::new(sources);
        let file_reader = crate::ingestion::FileReader::from_merged(entries_arc, sources_arc);

        let title = format!("merged({})", source_tab_indices.len());
        let log_manager = LogManager::new(self.db.clone(), None).await;
        let mut tab = TabState::new(file_reader, log_manager, title);
        tab.display.format = None;
        tab.continuation_map = None;
        tab.filter.visible_indices = VisibleLines::Filtered((0..visible_count).collect());
        tab.display.show_line_numbers = false;
        tab.merged = Some(MergedState {
            source_tab_indices,
            source_parsers: parsers,
            source_labels,
            source_line_counts,
            label_col_width,
            stopped: false,
        });
        self.apply_tab_defaults(&mut tab);

        self.tabs.push(tab);
        self.active_tab = self.tabs.len() - 1;
    }
}