alma 0.1.0

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Application startup.

use crate::{
    buffer::{BufferOpenError, launch_file_argument, open_launch_buffer},
    ecs::{
        BufferPlugin, EditorCorePlugin, FocusPlugin, InitialEditorBuffer, LayoutPlugin,
        TextShapePlugin,
    },
    features::{
        ui::{ChromeStatusLine, UiFeaturePlugin},
        vim::{VimFeaturePlugin, VimInputState},
    },
    fs_utils::FilesystemConfig,
    render::TextRenderPlugin,
    scene::EditorViewPlugin,
    vim::VimConfig,
};
use bevy::{
    input::{ButtonInput, keyboard::KeyboardInput},
    prelude::{App, DefaultPlugins, KeyCode, MinimalPlugins, Plugin},
};
use haalka::prelude::HaalkaPlugin;
use std::env;

/// Initial UTF-8 byte stream rendered by the managed text node.
const INITIAL_TEXT_STREAM: &str = include_str!("../lib.rs");

/// Headless editor dataflow plugins.
#[derive(Clone, Copy, Debug, Default)]
struct EditorDataflowPlugins;

impl Plugin for EditorDataflowPlugins {
    fn build(&self, app: &mut App) {
        let _app = app
            .add_plugins(EditorCorePlugin)
            .add_plugins(BufferPlugin)
            .add_plugins(FocusPlugin)
            .add_plugins(LayoutPlugin)
            .add_plugins(TextShapePlugin)
            .add_plugins(VimFeaturePlugin);
    }
}

/// Windowed presentation plugins.
#[derive(Clone, Copy, Debug, Default)]
struct EditorPresentationPlugins;

impl Plugin for EditorPresentationPlugins {
    fn build(&self, app: &mut App) {
        let _app = app
            .add_plugins(HaalkaPlugin::new())
            .add_plugins(EditorViewPlugin)
            .add_plugins(TextRenderPlugin)
            .add_plugins(UiFeaturePlugin);
    }
}

/// Runs the Alma Bevy application.
///
/// # Errors
///
/// Returns [`BufferOpenError`] when the launch file cannot be opened or read as UTF-8.
pub fn run() -> Result<(), BufferOpenError> {
    if env::var_os("ALMA_HEADLESS").is_some() {
        return run_headless();
    }

    let _exit = build_windowed_app()?.run();

    Ok(())
}

/// Runs the Alma editor dataflow without windowing or rendering plugins.
///
/// This is intended for CI smoke tests, deterministic replay, and non-interactive validation.
///
/// # Errors
///
/// Returns [`BufferOpenError`] when the launch file cannot be opened or read as UTF-8.
pub fn run_headless() -> Result<(), BufferOpenError> {
    let update_count = env::var("ALMA_HEADLESS_UPDATES")
        .ok()
        .and_then(|value| value.parse::<usize>().ok())
        .unwrap_or(1);
    let mut app = build_headless_app()?;

    for _update in 0..update_count {
        app.update();
    }

    Ok(())
}

/// Builds the normal interactive application.
fn build_windowed_app() -> Result<App, BufferOpenError> {
    let filesystem_config = FilesystemConfig::default();
    let (text_stream, buffer_file) = open_launch_buffer(
        launch_file_argument(env::args_os().skip(1)),
        INITIAL_TEXT_STREAM,
        &filesystem_config,
    )?;

    let mut app = App::new();
    let _app = app
        .insert_resource(filesystem_config)
        .insert_resource(InitialEditorBuffer {
            stream: text_stream,
            file: buffer_file,
        })
        .insert_resource(VimConfig::default())
        .insert_resource(VimInputState::default())
        .insert_resource(ChromeStatusLine::default())
        .add_plugins(DefaultPlugins)
        .add_plugins(EditorDataflowPlugins)
        .add_plugins(EditorPresentationPlugins);

    Ok(app)
}

/// Builds the non-rendering editor dataflow application.
fn build_headless_app() -> Result<App, BufferOpenError> {
    let filesystem_config = FilesystemConfig::default();
    let (text_stream, buffer_file) = open_launch_buffer(
        launch_file_argument(env::args_os().skip(1)),
        INITIAL_TEXT_STREAM,
        &filesystem_config,
    )?;

    let mut app = App::new();
    let _app = app
        .insert_resource(filesystem_config)
        .insert_resource(InitialEditorBuffer {
            stream: text_stream,
            file: buffer_file,
        })
        .insert_resource(VimConfig::default())
        .insert_resource(VimInputState::default())
        .insert_resource(ChromeStatusLine::default())
        .init_resource::<ButtonInput<KeyCode>>()
        .add_message::<KeyboardInput>()
        .add_plugins(MinimalPlugins)
        .add_plugins(EditorDataflowPlugins);

    Ok(app)
}

#[cfg(test)]
mod tests {
    use super::build_headless_app;
    use crate::ecs::components::buffer::EditorBuffer;
    use bevy::prelude::{Entity, With};

    #[test]
    fn headless_app_starts_editor_dataflow_without_windowing() {
        let mut app = build_headless_app().expect("headless app should build");

        app.update();

        let mut query = app
            .world_mut()
            .query_filtered::<Entity, With<EditorBuffer>>();
        let buffers = query.iter(app.world()).collect::<Vec<_>>();
        assert_eq!(buffers.len(), 1);
    }
}