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 crate::filters::{FilterOptions, FilterType};
use crate::ui::App;

impl App {
    pub(super) fn cmd_tail(&mut self) {
        let tab = &mut self.tabs[self.active_tab];
        tab.stream.tail_mode = !tab.stream.tail_mode;
        if tab.stream.tail_mode {
            let new_count = tab.filter.visible_indices.len();
            tab.scroll.scroll_offset = new_count.saturating_sub(1);
        }
    }

    pub(super) fn cmd_stop(&mut self) {
        self.stdin_load_state = None;
        let tab = &mut self.tabs[self.active_tab];
        if let Some(merged) = &mut tab.merged {
            merged.stopped = true;
        } else {
            tab.stream.watch = None;
        }
    }

    pub(super) fn cmd_pause(&mut self) {
        self.tabs[self.active_tab].stream.paused = true;
    }

    pub(super) fn cmd_resume(&mut self) {
        self.tabs[self.active_tab].stream.paused = false;
    }

    pub(super) async fn cmd_reset(&mut self) -> Result<bool, String> {
        self.db
            .reset_all()
            .await
            .map_err(|e| format!("Failed to reset database: {e}"))?;

        self.display.show_mode_bar = true;
        self.display.show_borders_default = false;
        self.display.show_line_numbers = true;
        self.display.show_sidebar = true;
        self.display.wrap = false;
        self.session.restore_policy = crate::config::RestoreSessionPolicy::Ask;
        self.session.restore_file_policy = crate::config::RestoreSessionPolicy::Ask;

        for tab in &mut self.tabs {
            tab.display.show_mode_bar = true;
            tab.display.show_borders = false;
            tab.display.show_line_numbers = true;
            tab.display.show_sidebar = true;
            tab.display.wrap = false;
            tab.reset_tab_state();
        }
        Ok(false)
    }

    pub(super) async fn cmd_date_filter(
        &mut self,
        expr: Vec<String>,
        fg: Option<String>,
        bg: Option<String>,
        line_mode: bool,
    ) -> Result<bool, String> {
        let tab = &self.tabs[self.active_tab];
        if tab.display.format.is_none() && !self.session.startup_filters {
            return Err(
                "No log format detected — date filter requires structured timestamps".to_string(),
            );
        }
        let expression = expr.join(" ");
        crate::filters::parse_date_filter(&expression)
            .map_err(|e| format!("Invalid date filter: {}", e))?;
        let pattern = format!("{}{}", crate::filters::DATE_PREFIX, expression);
        let mut opts = FilterOptions::default();
        if line_mode {
            opts = opts.line_mode();
        }
        if let Some(ref c) = fg {
            opts = opts.fg(c);
        }
        if let Some(ref c) = bg {
            opts = opts.bg(c);
        }
        if let Some(old_id) = self.tabs[self.active_tab].filter.editing_filter_id.take() {
            self.tabs[self.active_tab]
                .log_manager
                .update_filter(old_id, pattern, FilterType::Include, opts)
                .await;
        } else {
            self.tabs[self.active_tab]
                .log_manager
                .add_filter_with_color(pattern, FilterType::Include, opts)
                .await;
        }
        self.tabs[self.active_tab].begin_filter_refresh();
        Ok(false)
    }

    pub(super) fn cmd_docker(&mut self) -> Result<bool, String> {
        let output = std::process::Command::new("docker")
            .args([
                "ps",
                "--format",
                "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}",
            ])
            .output();
        match output {
            Ok(out) if out.status.success() => {
                let text = String::from_utf8_lossy(&out.stdout);
                let containers: Vec<crate::mode::docker_select_mode::DockerContainer> = text
                    .lines()
                    .filter(|l| !l.is_empty())
                    .filter_map(|line| {
                        let parts: Vec<&str> = line.splitn(4, '\t').collect();
                        if parts.len() == 4 {
                            Some(crate::mode::docker_select_mode::DockerContainer {
                                id: parts[0].to_string(),
                                name: parts[1].to_string(),
                                image: parts[2].to_string(),
                                status: parts[3].to_string(),
                            })
                        } else {
                            None
                        }
                    })
                    .collect();
                if containers.is_empty() {
                    self.tabs[self.active_tab].interaction.mode = Box::new(
                        crate::mode::docker_select_mode::DockerSelectMode::with_error(
                            "No running containers found".to_string(),
                        ),
                    );
                } else {
                    self.tabs[self.active_tab].interaction.mode = Box::new(
                        crate::mode::docker_select_mode::DockerSelectMode::new(containers),
                    );
                }
                Ok(true)
            }
            Ok(out) => {
                let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
                self.tabs[self.active_tab].interaction.mode = Box::new(
                    crate::mode::docker_select_mode::DockerSelectMode::with_error(
                        if stderr.is_empty() {
                            "docker ps failed".to_string()
                        } else {
                            stderr
                        },
                    ),
                );
                Ok(true)
            }
            Err(e) => {
                self.tabs[self.active_tab].interaction.mode = Box::new(
                    crate::mode::docker_select_mode::DockerSelectMode::with_error(format!(
                        "Failed to run docker: {}",
                        e
                    )),
                );
                Ok(true)
            }
        }
    }

    pub(super) fn cmd_value_colors(&mut self) -> Result<bool, String> {
        use crate::mode::value_colors_mode::{ValueColorEntry, ValueColorGroup as VCGroup};
        let disabled = &self.theme.value_colors.disabled;
        let process_representative = self.theme.process_colors.first().copied();
        let groups: Vec<VCGroup> = self
            .theme
            .value_colors
            .grouped_categories(process_representative)
            .into_iter()
            .map(|g| VCGroup {
                label: g.label.to_string(),
                children: g
                    .children
                    .into_iter()
                    .map(|(key, label, color)| ValueColorEntry {
                        key: key.to_string(),
                        label: label.to_string(),
                        color,
                        enabled: !disabled.contains(key),
                    })
                    .collect(),
            })
            .collect();
        let original_disabled = disabled.clone();
        self.tabs[self.active_tab].interaction.mode = Box::new(
            crate::mode::value_colors_mode::ValueColorsMode::new(groups, original_disabled),
        );
        Ok(true)
    }

    pub(super) fn cmd_dlt(&mut self) -> Result<bool, String> {
        let devices = self.dlt_devices.clone();
        self.tabs[self.active_tab].interaction.mode =
            Box::new(crate::mode::dlt_select_mode::DltSelectMode::new(devices));
        Ok(true)
    }

    pub(super) async fn cmd_otel(&mut self, http: bool, port: Option<u16>) -> Result<bool, String> {
        if http {
            let port = port.unwrap_or(4318);
            self.open_otlp_stream(port).await;
        } else {
            let port = port.unwrap_or(4317);
            self.open_otlp_grpc_stream(port).await;
        }
        Ok(true)
    }

    pub(super) async fn cmd_enable_mcp(&mut self, port: Option<u16>) -> Result<bool, String> {
        let p = port.unwrap_or_else(|| self.mcp.port.unwrap_or(9876));
        match self.start_mcp(p).await {
            Ok(()) => {
                self.tabs[self.active_tab].interaction.notification =
                    Some(format!("MCP server listening on port {p}"));
                self.tabs[self.active_tab].interaction.notification_set_at =
                    Some(std::time::Instant::now());
            }
            Err(e) => {
                self.tabs[self.active_tab].interaction.command_error =
                    Some(format!("Failed to start MCP server on port {p}: {e}"));
            }
        }
        Ok(false)
    }

    pub(super) fn cmd_disable_mcp(&mut self) {
        self.stop_mcp();
        self.tabs[self.active_tab].interaction.notification =
            Some("MCP server stopped".to_string());
        self.tabs[self.active_tab].interaction.notification_set_at =
            Some(std::time::Instant::now());
    }

    pub(super) async fn cmd_run(&mut self, command: Vec<String>) -> Result<bool, String> {
        if command.is_empty() {
            return Err(":run requires a command".to_string());
        }
        self.open_run_command(command).await;
        Ok(true)
    }
}