t-rec 0.9.0-preview2

Blazingly fast terminal recorder that generates animated gif images for the web written in rust.
//! t-rec - Terminal Recorder
//!
//! A blazingly fast terminal recorder that generates GIFs and MP4s.

// CLI-specific modules
mod cli;

// Use the library's core module
use t_rec::core;

// Platform-specific imports
#[cfg(any(target_os = "linux", target_os = "netbsd"))]
use core::linux::*;
#[cfg(target_os = "macos")]
use core::macos::*;
#[cfg(target_os = "windows")]
use core::windows::*;

use crate::cli::utils::parse_delay;
use crate::cli::{
    expand_home, handle_init_config, handle_list_profiles, init_logging, launch,
    print_recording_summary, resolve_profiled_settings, CliArgs, OutputGenerator, ProfileSettings,
    RecordingSession, SessionConfig,
};
use core::common::{Platform, PlatformApiFactory};
use core::generators::{check_for_gif, check_for_mp4};
use core::wallpapers::{resolve_wallpaper, Wallpaper};
use core::PlatformApi;

use anyhow::{bail, Context};
use image::DynamicImage;
use std::env;
use std::time::Duration;

// Re-export core types for use in this crate
pub use core::{Image, ImageOnHeap, Result, WindowId, WindowList, WindowListEntry};

fn main() -> Result<()> {
    init_logging();

    let args = launch();

    // Handle config-related commands first
    if args.init_config {
        return handle_init_config();
    }
    if args.list_profiles {
        return handle_list_profiles();
    }

    let mut api = Platform::setup()?;
    if args.list_windows {
        return ls_win(&api);
    }

    // TODO: 1. this whole block could be hidden inside of `SessionConfig::from_args()
    // Resolve settings from CLI args and config file
    let settings = resolve_profiled_settings(&args)?;

    validate_prerequisites(&settings)?;

    // Determine shell program
    let program = args
        .program
        .clone()
        .unwrap_or_else(|| env::var("SHELL").unwrap_or_else(|_| DEFAULT_SHELL.to_owned()));

    // Get window ID and optional name
    let (win_id, window_name) = current_win_id(&api, &args)?;
    api.calibrate(win_id)?;

    // Validate wallpaper BEFORE recording starts
    let wallpaper_config = validate_wallpaper_config(&settings, &api, win_id)?;

    // Parse delay settings
    let (start_delay, end_delay, idle_pause) = (
        parse_delay(settings.start_pause.as_deref(), "start-pause")?,
        parse_delay(settings.end_pause.as_deref(), "end-pause")?,
        parse_delay(Some(settings.idle_pause()), "idle-pause")?,
    );

    // Build session configuration
    let session_config = SessionConfig::builder()
        .win_id(win_id)
        .window_name(window_name)
        .program(program)
        .using_profile(&settings)
        .idle_pause(idle_pause)
        .start_delay(start_delay.unwrap_or(Duration::ZERO))
        .end_delay(end_delay.unwrap_or(Duration::ZERO))
        .wallpaper(wallpaper_config.clone())
        .build();
    // TODO: 1. ending (start see above)

    // Run recording session
    let session = RecordingSession::new(
        session_config,
        Box::new(api),
        cli::recorder::runtime::Runtime::new(),
    )?;
    let output_config = session.output_config();
    let result = session.run()?;

    // Print recording summary
    print_recording_summary(&settings, result.frame_count);

    // Generate outputs (GIF, MP4, screenshots)
    OutputGenerator::new(result, output_config).process()?;

    Ok(())
}

/// Validate required tools
fn validate_prerequisites(settings: &ProfileSettings) -> Result<()> {
    if !settings.video_only() {
        check_for_gif()?;
    }
    if settings.video() || settings.video_only() {
        check_for_mp4()?;
    }

    Ok(())
}

/// Validates and loads the wallpaper configuration before recording starts.
///
/// Returns `Some((wallpaper, padding))` if wallpaper is configured, `None` otherwise.
/// Fails early with a clear error message if the wallpaper is invalid or too small.
fn validate_wallpaper_config(
    settings: &ProfileSettings,
    api: &impl PlatformApi,
    win_id: WindowId,
) -> Result<Option<(DynamicImage, u32)>> {
    let wp_value = match &settings.wallpaper {
        Some(v) => v,
        None => return Ok(None),
    };

    // Expand $HOME in wallpaper path and parse into Wallpaper type
    let wp_value = expand_home(wp_value);
    let wallpaper_type: Wallpaper = wp_value.parse().unwrap(); // Infallible
    let padding = settings.wallpaper_padding();

    // Capture a screenshot to get terminal dimensions
    let screenshot = api.capture_window_screenshot(win_id)?;
    let terminal_width = screenshot.layout.width;
    let terminal_height = screenshot.layout.height;

    // Resolve and validate the wallpaper
    let wallpaper = resolve_wallpaper(&wallpaper_type, terminal_width, terminal_height, padding)?;

    Ok(Some((wallpaper, padding)))
}

/// Determines the WindowId either by env var 'WINDOWID'
/// or by the env var 'TERM_PROGRAM' and then asking the window manager for all visible windows
/// and finding the Terminal in that list.
fn current_win_id(api: &impl PlatformApi, args: &CliArgs) -> Result<(WindowId, Option<String>)> {
    match args.win_id.ok_or_else(|| env::var("WINDOWID")) {
        Ok(win_id) => Ok((win_id, None)),
        Err(_) => {
            let terminal = env::var("TERM_PROGRAM").context(
                "Env variable 'TERM_PROGRAM' was empty but is needed for figure out the WindowId. Please set it to e.g. TERM_PROGRAM=alacitty",
            );
            if let Ok(terminal) = terminal {
                let (win_id, name) = get_window_id_for(api, terminal).context(
                    "Cannot determine the WindowId of this terminal. Please set env variable 'WINDOWID' and try again.",
                )?;
                Ok((win_id, Some(name)))
            } else {
                let win_id = api.get_active_window()?;
                Ok((win_id, None))
            }
        }
    }
}

/// Finds the window id for a given terminal / program by name.
pub fn get_window_id_for(api: &impl PlatformApi, terminal: String) -> Result<(WindowId, String)> {
    for term in terminal.to_lowercase().split('.') {
        for (window_owner, window_id) in api.window_list()? {
            if let Some(window_owner) = window_owner {
                let window = &window_owner.to_lowercase();
                let terminal = &terminal.to_lowercase();
                if window.contains(term) || terminal.contains(window) {
                    return Ok((window_id, terminal.to_owned()));
                }
            }
        }
    }

    bail!("Cannot determine the window id from the available window list.")
}

/// Lists all windows with name and id.
pub fn ls_win(api: &impl PlatformApi) -> Result<()> {
    let mut list = api.window_list()?;
    list.sort();

    println!("Window | Id");
    for (window_owner, window_id) in list.iter() {
        if let (Some(window_owner), window_id) = (window_owner, window_id) {
            println!("{} | {}", window_owner, window_id)
        }
    }

    Ok(())
}