gshell 1.0.0

gshell is a shell for people who live in the terminal. It pairs familiar Unix behavior with a tighter core, fast interaction, and an interface built to stay out of the way.
Documentation
use std::sync::Arc;

use gshell::{
    ast::{CommandNode, ShellExpr, SimpleCommand},
    builtins::{Builtin, BuiltinFuture, BuiltinRegistry},
    shell::{CommandOutput, ExitCode, SharedShellState, ShellAction, ShellState},
};

struct TestBuiltin;

impl Builtin for TestBuiltin {
    fn name(&self) -> &'static str {
        "test"
    }

    fn execute<'a>(&'a self, _state: SharedShellState, _args: &'a [String]) -> BuiltinFuture<'a> {
        Box::pin(async { Ok(ShellAction::continue_with(CommandOutput::success())) })
    }
}

#[test]
fn shell_state_initialization_defaults() {
    let state = ShellState::new().expect("shell state should initialize");

    assert_eq!(state.last_exit_status(), ExitCode::SUCCESS);
    assert!(state.cwd().is_absolute());
    assert!(state.history().entries().is_empty());
    assert!(state.aliases().get("missing").is_none());
    assert!(state.functions().get("missing").is_none());
    assert!(state.jobs().is_empty());
}

#[test]
fn shell_state_environment_read_and_write() {
    let mut state = ShellState::new().expect("shell state should initialize");

    state.set_env_var("GSHELL_TEST_KEY", "value-1");
    assert_eq!(state.env_var("GSHELL_TEST_KEY"), Some("value-1"));

    state.set_env_var("GSHELL_TEST_KEY", "value-2");
    assert_eq!(state.env_var("GSHELL_TEST_KEY"), Some("value-2"));

    let removed = state.remove_env_var("GSHELL_TEST_KEY");
    assert_eq!(removed.as_deref(), Some("value-2"));
    assert_eq!(state.env_var("GSHELL_TEST_KEY"), None);
}

#[test]
fn alias_store_supports_set_get_remove_and_sorted_entries() {
    let mut state = ShellState::new().expect("shell state should initialize");

    state.aliases_mut().set("z", "echo z");
    state.aliases_mut().set("a", "echo a");

    assert_eq!(state.aliases().get("a"), Some("echo a"));
    assert_eq!(
        state.aliases().entries(),
        vec![("a", "echo a"), ("z", "echo z")]
    );

    assert_eq!(state.aliases_mut().remove("a"), Some("echo a".to_string()));
    assert!(state.aliases().get("a").is_none());
}

#[test]
fn function_store_supports_set_get_remove_and_sorted_names() {
    let mut state = ShellState::new().expect("shell state should initialize");
    let body = ShellExpr::Command(CommandNode::Simple(SimpleCommand::new(Vec::new())));

    state.functions_mut().set("z", body.clone());
    state.functions_mut().set("a", body.clone());

    assert!(state.functions().get("a").is_some());
    assert_eq!(
        state.functions().names(),
        vec!["a".to_string(), "z".to_string()]
    );

    assert!(state.functions_mut().remove("a").is_some());
    assert!(state.functions().get("a").is_none());
}

#[test]
fn shell_state_last_exit_status_updates() {
    let mut state = ShellState::new().expect("shell state should initialize");

    assert_eq!(state.last_exit_status(), ExitCode::SUCCESS);

    state.set_last_exit_status(ExitCode::new(42));
    assert_eq!(state.last_exit_status(), ExitCode::new(42));
}

#[test]
fn builtin_registry_basics() {
    let mut registry = BuiltinRegistry::new();

    assert!(registry.is_empty());

    registry.register(Arc::new(TestBuiltin));

    assert_eq!(registry.len(), 1);
    assert!(registry.contains("test"));
    assert!(registry.get("test").is_some());
    assert!(registry.get("missing").is_none());
}