stackql-deploy 2.0.6

Infrastructure-as-code framework for declarative cloud resource management using StackQL
// commands/shell.rs

//! # Shell Command Module
//!
//! This module provides the `shell` command for the StackQL Deploy application.
//! The `shell` command launches an interactive shell where users can execute queries
//! against a StackQL server. Queries can be entered across multiple lines and are
//! only executed when terminated with a semicolon (`;`).
//!
//! ## Features
//! - Interactive query input with line history support.
//! - Multi-line query handling using a semicolon (`;`) to indicate query completion.
//! - Automatic server startup if not running.
//! - Connection handling using a global connection function (`create_client`).
//!
//! ## Example Usage
//! ```bash
//! ./stackql-deploy shell
//! ```
//!

use clap::{ArgMatches, Command};
use colored::*;
use rustyline::error::ReadlineError;
use rustyline::Editor;

use crate::globals::{server_host, server_port};
use crate::utils::connection::create_client;
use crate::utils::display::print_unicode_box;
use crate::utils::query::{execute_query, QueryResult};
use crate::utils::server::check_and_start_server;

/// Configures the `shell` command for the CLI application.
pub fn command() -> Command {
    Command::new("shell").about("Launch the interactive shell")
}

/// Executes the `shell` command, launching an interactive query interface.
pub fn execute(_matches: &ArgMatches) {
    print_unicode_box(
        "Launching interactive shell...",
        crate::utils::display::BorderColor::Cyan,
    );

    let host = server_host();
    let port = server_port();

    check_and_start_server();

    // Connect to the server using the global host and port
    let mut stackql_client_conn = create_client();

    println!("Type 'exit' to quit the shell");
    println!("---");

    let mut rl = Editor::<()>::new().unwrap();
    let _ = rl.load_history("stackql_history.txt");

    let mut query_buffer = String::new(); // Accumulates input until a semicolon is found

    loop {
        let prompt = if query_buffer.is_empty() {
            format!("stackql ({}:{})=> ", host, port)
        } else {
            "... ".to_string()
        };

        let readline = rl.readline(&prompt);

        match readline {
            Ok(line) => {
                let input = line.trim();

                if input.eq_ignore_ascii_case("exit") || input.eq_ignore_ascii_case("quit") {
                    println!("Goodbye");
                    break;
                }

                // Accumulate the query
                query_buffer.push_str(input);
                query_buffer.push(' ');

                if input.ends_with(';') {
                    let normalized_input = normalize_query(&query_buffer);
                    rl.add_history_entry(&normalized_input);

                    match execute_query(&normalized_input, &mut stackql_client_conn) {
                        Ok(result) => match result {
                            QueryResult::Data {
                                columns,
                                rows,
                                notices,
                            } => {
                                print_table(columns, rows);

                                // Display notices if any
                                if !notices.is_empty() {
                                    println!("\n{}", "Notices:".yellow().bold());
                                    for notice in notices {
                                        // Split notice text by newlines to format each line
                                        for line in notice.lines() {
                                            println!("  {}", line.yellow());
                                        }
                                    }
                                }
                            }
                            QueryResult::Command(cmd) => {
                                println!("{}", cmd.green());
                            }
                            QueryResult::Empty => {
                                println!("{}", "Query executed successfully. No results.".green());
                            }
                        },
                        Err(e) => {
                            eprintln!("{}", format!("Error: {}", e).red());
                        }
                    }

                    query_buffer.clear();
                }
            }
            Err(ReadlineError::Interrupted) => {
                println!("CTRL-C");
                query_buffer.clear();
                continue;
            }
            Err(ReadlineError::Eof) => {
                println!("Goodbye");
                break;
            }
            Err(err) => {
                eprintln!("Error: {:?}", err);
                break;
            }
        }
    }

    let _ = rl.save_history("stackql_history.txt");
}

/// Normalizes a query by trimming whitespace and combining lines.
fn normalize_query(input: &str) -> String {
    input
        .split('\n')
        .map(|line| line.trim())
        .filter(|line| !line.is_empty())
        .collect::<Vec<_>>()
        .join(" ")
}

/// Prints the query result in a tabular format.
fn print_table(
    columns: Vec<crate::utils::query::QueryResultColumn>,
    rows: Vec<crate::utils::query::QueryResultRow>,
) {
    let mut column_widths: Vec<usize> = columns.iter().map(|col| col.name.len()).collect();

    for row in &rows {
        for (i, value) in row.values.iter().enumerate() {
            if i < column_widths.len() && value.len() > column_widths[i] {
                column_widths[i] = value.len();
            }
        }
    }

    // Print header border
    print!("+");
    for width in &column_widths {
        print!("{}+", "-".repeat(width + 2));
    }
    println!();

    // Print column headers
    print!("|");
    for (i, col) in columns.iter().enumerate() {
        print!(
            " {}{} |",
            col.name,
            " ".repeat(column_widths[i] - col.name.len())
        );
    }
    println!();

    // Print border after header
    print!("+");
    for width in &column_widths {
        print!("{}+", "-".repeat(width + 2));
    }
    println!();

    // Print each row with a border after it
    let row_count = rows.len();
    for row in rows {
        print!("|");
        for (i, value) in row.values.iter().enumerate() {
            if i < column_widths.len() {
                print!(" {}{} |", value, " ".repeat(column_widths[i] - value.len()));
            }
        }
        println!();

        // Print border after each row
        print!("+");
        for width in &column_widths {
            print!("{}+", "-".repeat(width + 2));
        }
        println!();
    }

    if row_count > 0 {
        println!("{} rows returned", row_count);
    }
}