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::fs;

use gshell::{
    completion::{ShellCompleter, ShellHinter},
    shell::ShellState,
};
use reedline::{Completer, Hinter, History, HistoryItem};

#[tokio::test]
async fn command_completion_includes_builtins() {
    let state = ShellState::shared().await.expect("state should initialize");
    let mut completer = ShellCompleter::new(state);

    let suggestions = completer.complete("ec", 2);
    let values = suggestions.into_iter().map(|s| s.value).collect::<Vec<_>>();

    assert!(values.iter().any(|v| v == "echo"));
}

#[tokio::test]
async fn env_completion_suggests_shell_variables() {
    let state = ShellState::shared().await.expect("state should initialize");
    state.write().await.set_env_var("GSHELL_DEMO", "1");

    let mut completer = ShellCompleter::new(state);
    let suggestions = completer.complete("$GSH", 4);
    let values = suggestions.into_iter().map(|s| s.value).collect::<Vec<_>>();

    assert!(values.iter().any(|v| v == "$GSHELL_DEMO"));
}

#[tokio::test]
async fn path_completion_suggests_matching_files() {
    let dir = tempfile::tempdir().expect("temp dir should be created");
    fs::write(dir.path().join("alpha.txt"), "x").expect("file should be writable");
    fs::write(dir.path().join("alpine.txt"), "x").expect("file should be writable");
    fs::write(dir.path().join("beta.txt"), "x").expect("file should be writable");

    let state = ShellState::shared().await.expect("state should initialize");
    state.write().await.set_cwd(dir.path().to_path_buf());

    let mut completer = ShellCompleter::new(state);
    let suggestions = completer.complete("./al", 4);
    let values = suggestions.into_iter().map(|s| s.value).collect::<Vec<_>>();

    assert!(values.iter().any(|v| v == "./alpha.txt"));
    assert!(values.iter().any(|v| v == "./alpine.txt"));
    assert!(!values.iter().any(|v| v == "./beta.txt"));
}

#[tokio::test]
async fn path_completion_does_not_append_whitespace() {
    let dir = tempfile::tempdir().expect("temp dir should be created");
    fs::write(dir.path().join("alpha.txt"), "x").expect("file should be writable");

    let state = ShellState::shared().await.expect("state should initialize");
    state.write().await.set_cwd(dir.path().to_path_buf());

    let mut completer = ShellCompleter::new(state);
    let suggestions = completer.complete("./al", 4);

    assert!(suggestions.iter().all(|s| !s.append_whitespace));
}

#[tokio::test]
async fn command_completion_reads_executables_from_path() {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;

        let dir = tempfile::tempdir().expect("temp dir should be created");
        let cmd = dir.path().join("demo-tool");
        fs::write(&cmd, "#!/bin/sh\n").expect("stub command should be writable");

        let mut perms = fs::metadata(&cmd)
            .expect("metadata should load")
            .permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&cmd, perms).expect("permissions should update");

        let state = ShellState::shared().await.expect("state should initialize");
        state
            .write()
            .await
            .set_env_var("PATH", dir.path().display().to_string());

        let mut completer = ShellCompleter::new(state);
        let suggestions = completer.complete("dem", 3);
        let values = suggestions.into_iter().map(|s| s.value).collect::<Vec<_>>();

        assert!(values.iter().any(|v| v == "demo-tool"));
    }
}

#[tokio::test]
async fn hinter_returns_suffix_for_history_match() {
    let state = ShellState::shared().await.expect("state should initialize");
    let history_path = state.read().await.history().path().to_path_buf();

    let mut history = reedline::FileBackedHistory::with_file(100, history_path)
        .expect("history should initialize");

    history
        .save(HistoryItem::from_command_line("echo hello world"))
        .expect("history entry should save");

    let mut hinter = ShellHinter::default();
    let hint = hinter.handle("echo h", 6, &history, false, "");

    assert_eq!(hint, "ello world");
}

#[tokio::test]
async fn hinter_returns_nothing_when_cursor_is_not_at_end() {
    let state = ShellState::shared().await.expect("state should initialize");
    let history_path = state.read().await.history().path().to_path_buf();

    let mut history = reedline::FileBackedHistory::with_file(100, history_path)
        .expect("history should initialize");

    history
        .save(HistoryItem::from_command_line("echo hello world"))
        .expect("history entry should save");

    let mut hinter = ShellHinter::default();
    let hint = hinter.handle("echo h", 2, &history, false, "");

    assert!(hint.is_empty());
}