lios-bar 0.1.9

A native GTK4/VTE sidebar workspace for navigating live Lios terminal sessions.
Documentation
use crate::config::SidebarSide;
use std::path::PathBuf;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Action {
    Run {
        sidebar: Option<SidebarSide>,
        working_directory: Option<PathBuf>,
        no_gpu: bool,
    },
    Help,
    Version,
    Check,
    Install,
    Uninstall,
}

pub(crate) fn parse(args: impl IntoIterator<Item = String>) -> Result<Action, String> {
    let mut args = args.into_iter();
    let mut sidebar = None;
    let mut working_directory = None;
    let mut no_gpu = false;

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "-h" | "--help" => return Ok(Action::Help),
            "-V" | "--version" => return Ok(Action::Version),
            "--check" | "--diagnostics" => {
                if sidebar.is_some() || working_directory.is_some() || no_gpu {
                    return Err("--check cannot be combined with launch options".to_string());
                }
                ensure_no_more(args)?;
                return Ok(Action::Check);
            }
            "install" | "--install" | "---install" => {
                ensure_no_more(args)?;
                return Ok(Action::Install);
            }
            "uninstall" | "--uninstall" | "---uninstall" => {
                ensure_no_more(args)?;
                return Ok(Action::Uninstall);
            }
            "--left" => set_sidebar(&mut sidebar, SidebarSide::Left)?,
            "--no-gpu" => no_gpu = true,
            "--right" => set_sidebar(&mut sidebar, SidebarSide::Right)?,
            "--sidebar" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--sidebar requires left or right".to_string())?;
                set_sidebar(&mut sidebar, SidebarSide::parse(&value)?)?;
            }
            "--working-directory" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--working-directory requires a path".to_string())?;
                if working_directory.replace(PathBuf::from(value)).is_some() {
                    return Err("--working-directory may be supplied only once".to_string());
                }
            }
            _ if arg.starts_with('-') => return Err(format!("unknown option: {arg}")),
            _ => return Err(format!("unexpected argument: {arg}")),
        }
    }

    Ok(Action::Run {
        sidebar,
        working_directory,
        no_gpu,
    })
}

fn set_sidebar(destination: &mut Option<SidebarSide>, value: SidebarSide) -> Result<(), String> {
    if destination.replace(value).is_some() {
        return Err("sidebar position may be supplied only once".to_string());
    }
    Ok(())
}

fn ensure_no_more(mut args: impl Iterator<Item = String>) -> Result<(), String> {
    match args.next() {
        Some(extra) => Err(format!("unexpected argument after action: {extra}")),
        None => Ok(()),
    }
}

pub(crate) fn help() -> &'static str {
    "LiosBar — navigate live terminal sessions from a sidebar

Usage:
  lios-bar [--sidebar left|right] [--working-directory PATH]
  lios-bar --left
  lios-bar --right
  lios-bar --install
  lios-bar --uninstall
  lios-bar --check                 Report toolkit/display compatibility; no shell
  lios-bar --no-gpu                Use Cairo when GPU drivers cause rendering problems

Shortcuts:
  Ctrl+Shift+T       New session
  Ctrl+PageDown      Next session
  Ctrl+PageUp        Previous session
  Ctrl+Shift+PageUp  Move active session up (without switching)
  Ctrl+Shift+PageDown Move active session down (without switching)
  Ctrl+Shift+W       Close selected session
  Ctrl+Shift+R       Rename selected session
  Ctrl+Shift+B       Move sidebar left/right
  Ctrl+Shift+L       Filter sessions by name/title (Enter opens, Esc returns)
  Ctrl+Shift+F       Find text in the selected terminal
  Enter / Shift+Enter  Next / previous match while editing the query
  F3 / Shift+F3      Next / previous match while Find is open
  Escape            Close Find and return to the terminal
  Ctrl+Shift+C       Copy selection
  Ctrl+Shift+V       Guarded paste
  Shift+Insert       Guarded paste while the terminal owns focus

Sidebar layout: use the gear beside SESSIONS for saved compact rows, titles,
keyboard hints, and a layout-only reset.
Drag session rows to reorder within the current workspace.
"
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn defaults_to_run_without_overrides() {
        assert_eq!(
            parse(Vec::<String>::new()).unwrap(),
            Action::Run {
                sidebar: None,
                working_directory: None,
                no_gpu: false,
            }
        );
    }

    #[test]
    fn parses_right_sidebar_and_directory() {
        assert_eq!(
            parse([
                "--sidebar".to_string(),
                "right".to_string(),
                "--working-directory".to_string(),
                "/tmp/project".to_string()
            ])
            .unwrap(),
            Action::Run {
                sidebar: Some(SidebarSide::Right),
                working_directory: Some(PathBuf::from("/tmp/project")),
                no_gpu: false,
            }
        );
    }

    #[test]
    fn rejects_commands_and_duplicate_position() {
        assert!(parse(["bash".to_string()]).is_err());
        assert!(parse(["--left".to_string(), "--right".to_string()]).is_err());
    }

    #[test]
    fn install_rejects_trailing_arguments() {
        assert!(parse(["--install".to_string(), "--right".to_string()]).is_err());
    }

    #[test]
    fn check_is_read_only_and_rejects_launch_options() {
        for alias in ["--check", "--diagnostics"] {
            assert_eq!(parse([alias.to_string()]).unwrap(), Action::Check);
            assert!(parse([alias.to_string(), "--right".to_string()]).is_err());
            assert!(parse(["--right".to_string(), alias.to_string()]).is_err());
            assert!(parse(["--no-gpu".to_string(), alias.to_string()]).is_err());
        }
    }

    #[test]
    fn software_rendering_is_an_opt_in_launch_override() {
        assert_eq!(
            parse(["--no-gpu".to_string(), "--right".to_string()]).unwrap(),
            Action::Run {
                sidebar: Some(SidebarSide::Right),
                working_directory: None,
                no_gpu: true,
            }
        );
    }
}