seamless-glance 1.0.0

Fast, read-only AWS TUI for cloud infrastructure visibility and triage
use ratatui::{
    layout::Rect,
    style::Style,
    widgets::{Block, Borders, Paragraph},
    Frame,
};

use crate::app::{ActiveView, App};
use crate::ui::theme::ThemeName;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CommandGroup {
    Triage,
    Overview,
    Compute,
    Data,
    Messaging,
    Networking,
    Security,
    Observability,
}

impl CommandGroup {
    pub fn label(&self) -> &'static str {
        match self {
            CommandGroup::Triage => "Triage",
            CommandGroup::Overview => "Overview",
            CommandGroup::Compute => "Compute",
            CommandGroup::Data => "Data",
            CommandGroup::Messaging => "Messaging",
            CommandGroup::Networking => "Networking",
            CommandGroup::Security => "Security",
            CommandGroup::Observability => "Observability",
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Command {
    pub name: &'static str,
    pub description: &'static str,
    pub view: ActiveView,
    pub group: CommandGroup,
    pub shortcut: Option<char>,
    pub aliases: &'static [&'static str],
}

pub const COMMANDS: &[Command] = &[
    Command {
        name: "findings",
        description: "Priority findings and triage inbox",
        view: ActiveView::Findings,
        group: CommandGroup::Triage,
        shortcut: Some('f'),
        aliases: &["finding", "triage", "home"],
    },
    Command {
        name: "account",
        description: "Account overview",
        view: ActiveView::AccountOverview,
        group: CommandGroup::Overview,
        shortcut: Some('1'),
        aliases: &["overview", "account-overview", "summary"],
    },
    Command {
        name: "cost",
        description: "Cost overview",
        view: ActiveView::CostOverview,
        group: CommandGroup::Overview,
        shortcut: Some('2'),
        aliases: &["billing", "spend"],
    },
    Command {
        name: "savings",
        description: "Cost savings opportunities",
        view: ActiveView::CostSavings,
        group: CommandGroup::Overview,
        shortcut: Some('0'),
        aliases: &["cost-savings", "optimize", "optimization"],
    },
    Command {
        name: "ec2",
        description: "EC2 instances",
        view: ActiveView::Ec2,
        group: CommandGroup::Compute,
        shortcut: Some('4'),
        aliases: &["instances", "compute"],
    },
    Command {
        name: "lambda",
        description: "Lambda functions",
        view: ActiveView::Lambda,
        group: CommandGroup::Compute,
        shortcut: Some('6'),
        aliases: &["functions"],
    },
    Command {
        name: "ecs",
        description: "ECS clusters",
        view: ActiveView::Ecs,
        group: CommandGroup::Compute,
        shortcut: Some('8'),
        aliases: &["containers", "cluster"],
    },
    Command {
        name: "rds",
        description: "RDS instances",
        view: ActiveView::Rds,
        group: CommandGroup::Data,
        shortcut: None,
        aliases: &["database", "databases"],
    },
    Command {
        name: "sqs",
        description: "SQS queues",
        view: ActiveView::Sqs,
        group: CommandGroup::Messaging,
        shortcut: None,
        aliases: &["queue", "queues", "messaging"],
    },
    Command {
        name: "vpc",
        description: "VPCs",
        view: ActiveView::Vpc,
        group: CommandGroup::Networking,
        shortcut: Some('3'),
        aliases: &["network", "networks"],
    },
    Command {
        name: "apigw",
        description: "API Gateway APIs",
        view: ActiveView::Apigateway,
        group: CommandGroup::Networking,
        shortcut: Some('9'),
        aliases: &["api", "apigateway", "gateway"],
    },
    Command {
        name: "lb",
        description: "Load balancers",
        view: ActiveView::LoadBalancers,
        group: CommandGroup::Networking,
        shortcut: None,
        aliases: &["elbv2", "load-balancer", "loadbalancer"],
    },
    Command {
        name: "tg",
        description: "Target groups",
        view: ActiveView::TargetGroups,
        group: CommandGroup::Networking,
        shortcut: None,
        aliases: &["target-group", "targetgroups"],
    },
    Command {
        name: "sg",
        description: "Security groups",
        view: ActiveView::SecurityGroups,
        group: CommandGroup::Security,
        shortcut: None,
        aliases: &["security-group", "securitygroups"],
    },
    Command {
        name: "sm",
        description: "Secrets Manager",
        view: ActiveView::Secrets,
        group: CommandGroup::Security,
        shortcut: Some('7'),
        aliases: &["secrets", "secret", "secretsmanager"],
    },
    Command {
        name: "cw",
        description: "CloudWatch alarms",
        view: ActiveView::CloudWatch,
        group: CommandGroup::Observability,
        shortcut: Some('5'),
        aliases: &["cloudwatch", "alarms", "monitoring"],
    },
];

pub fn parse_command(input: &str) -> (&str, &str) {
    let trimmed = input.trim().trim_start_matches('/');

    if let Some((cmd, rest)) = trimmed.split_once(char::is_whitespace) {
        (cmd.trim(), rest.trim())
    } else {
        (trimmed, "")
    }
}

pub fn command_for_view(view: ActiveView) -> Option<&'static Command> {
    COMMANDS.iter().find(|command| command.view == view)
}

pub fn next_command(current: ActiveView) -> &'static Command {
    let index = COMMANDS
        .iter()
        .position(|command| command.view == current)
        .unwrap_or(0);

    &COMMANDS[(index + 1) % COMMANDS.len()]
}

pub fn previous_command(current: ActiveView) -> &'static Command {
    let index = COMMANDS
        .iter()
        .position(|command| command.view == current)
        .unwrap_or(0);

    &COMMANDS[(index + COMMANDS.len() - 1) % COMMANDS.len()]
}

pub fn matching_commands(query: &str) -> Vec<&'static Command> {
    let normalized = query.trim().trim_start_matches('/').to_ascii_lowercase();

    let mut matches = COMMANDS
        .iter()
        .filter_map(|command| match_score(command, &normalized).map(|score| (score, command)))
        .collect::<Vec<_>>();

    matches.sort_by(|(left_score, left_command), (right_score, right_command)| {
        left_score
            .cmp(right_score)
            .then_with(|| left_command.group.cmp(&right_command.group))
            .then_with(|| left_command.name.cmp(right_command.name))
    });

    matches.into_iter().map(|(_, command)| command).collect()
}

fn match_score(command: &Command, query: &str) -> Option<u8> {
    if query.is_empty() {
        return Some(0);
    }

    let description = command.description.to_ascii_lowercase();

    if command.name == query {
        return Some(0);
    }

    if command.aliases.contains(&query) {
        return Some(1);
    }

    if command.name.starts_with(query) {
        return Some(2);
    }

    if command.aliases.iter().any(|alias| alias.starts_with(query)) {
        return Some(3);
    }

    if command.name.contains(query) {
        return Some(4);
    }

    if command.aliases.iter().any(|alias| alias.contains(query)) {
        return Some(5);
    }

    if description.contains(query) {
        return Some(6);
    }

    None
}

pub fn draw_command_palette(frame: &mut Frame, area: Rect, app: &App) {
    if !app.command_mode {
        return;
    }

    let input = app.command_input.trim();
    let (cmd, _args) = parse_command(input);
    let matches = matching_commands(cmd);

    let mut lines = vec![format!(":{}", input)];
    lines.push("  Jump to a view, or use `region <name>` / `rg <name>`.".into());

    if "region".starts_with(cmd) || "rg".starts_with(cmd) || cmd == "region" || cmd == "rg" {
        lines.push("".into());
        lines.push("  Regions".into());
        lines.push("  region <name>    Jump to a specific AWS region".into());
        lines.push("  region global    Jump to the synthetic global slot".into());
        lines.push("  rg <name>        Short alias for region".into());
    }

    if "theme".starts_with(cmd)
        || "themes".starts_with(cmd)
        || cmd == "theme"
        || cmd == "themes"
        || "th".starts_with(cmd)
    {
        lines.push("".into());
        lines.push("  Themes".into());
        lines.push("  theme <name>     Switch to a Seamless theme".into());

        for theme_name in ThemeName::ALL {
            lines.push(format!(
                "  theme {:<9} {}",
                theme_name.as_str(),
                theme_name.description()
            ));
        }
    }

    let mut last_group = None;
    for command in matches.iter().take(8) {
        if last_group != Some(command.group) {
            lines.push("".into());
            lines.push(format!("  {}", command.group.label()));
            last_group = Some(command.group);
        }

        let marker = if app.active_view == command.view {
            ">"
        } else {
            " "
        };
        let shortcut = command
            .shortcut
            .map(|shortcut| format!("[{}]", shortcut))
            .unwrap_or_else(|| "   ".into());

        lines.push(format!(
            "  {} {:<14} {:<4} {}",
            marker, command.name, shortcut, command.description
        ));
    }

    let is_special_query = cmd == "region"
        || cmd == "rg"
        || cmd == "theme"
        || cmd == "themes"
        || "th".starts_with(cmd);

    if matches.is_empty() && !cmd.is_empty() && !is_special_query {
        lines.push("".into());
        lines.push("  No matching view commands".into());
    }

    let cmd_ui = Paragraph::new(lines.join("\n"))
        .style(Style::default().fg(app.theme.accent))
        .block(
            Block::default()
                .borders(Borders::TOP)
                .title("Command Palette"),
        );

    frame.render_widget(cmd_ui, area);
}