//! Константы оформления окна и сценариев. use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::sync::OnceLock; /// Ширина окна при запуске. pub const WINDOW_WIDTH: f32 = 900.0; /// Высота окна при запуске. pub const WINDOW_HEIGHT: f32 = 600.0; /// Минимальная ширина окна. pub const MIN_WINDOW_WIDTH: f32 = 420.0; /// Минимальная высота окна. pub const MIN_WINDOW_HEIGHT: f32 = 280.0; /// Подпись окна в панели задач и в заголовке. pub const WINDOW_TITLE: &str = "Custom Window App"; /// Ширина одного уровня отступа в редакторе. pub const TAB_SIZE: usize = 4; /// Ширина правой панели при открытии. pub const RIGHT_PANEL_WIDTH: f32 = 240.0; /// Ширина левой панели при открытии. pub const LEFT_PANEL_WIDTH: f32 = 220.0; /// Минимальная ширина левой панели при перетаскивании разделителя. pub const MIN_LEFT_PANEL_WIDTH: f32 = 120.0; /// Минимальная ширина области редактора при изменении левой панели. pub const MIN_MAIN_AREA_WIDTH: f32 = 240.0; /// Высота нижней панели при открытии. pub const BOTTOM_PANEL_HEIGHT: f32 = 160.0; /// Каталог, в котором приложение ищет Lua-сценарии по умолчанию. /// /// Это текущий рабочий каталог процесса — именно там приложение ожидает /// `init.lua`, если не передан аргумент `--config`. Конкретный каталог всегда /// возвращает [`script_dir`]. pub const SCRIPT_DIR: &str = "."; /// Каталог сценариев, заданный через `--config`. static SCRIPT_DIR_OVERRIDE: OnceLock = OnceLock::new(); /// Файл, который нужно открыть при старте (первый позиционный аргумент). static STARTUP_FILE: OnceLock = OnceLock::new(); /// Разобранные аргументы командной строки. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct CommandLine { /// Каталог, из которого брать `init.lua`. pub config_dir: Option, /// Файл, открываемый при старте как `vim file.go`. pub file: Option, } /// Каталог, из которого загружаются `init.lua`, `include` и `require`. /// /// Возвращает значение `--config`, если оно было задано, иначе текущий /// рабочий каталог. pub fn script_dir() -> &'static Path { SCRIPT_DIR_OVERRIDE .get() .map_or_else(|| Path::new(SCRIPT_DIR), PathBuf::as_path) } /// Файл, который надо открыть при старте редактора. pub fn startup_file() -> Option<&'static Path> { STARTUP_FILE.get().map(PathBuf::as_path) } /// Запоминает каталог сценариев из аргумента `--config`. /// /// Путь канонизируется, поэтому относительный `--config ./scripts` работает /// относительно каталога запуска, а `include`/`require` видят абсолютные пути. pub fn set_script_dir(path: PathBuf) -> Result<(), String> { let canonical = std::fs::canonicalize(&path).map_err(|err| { format!( "не удалось открыть каталог конфигурации {}: {err}", path.display() ) })?; if !canonical.is_dir() { return Err(format!( "--config: {} не является каталогом", canonical.display() )); } SCRIPT_DIR_OVERRIDE .set(canonical) .map_err(|_| "каталог конфигурации уже задан".to_owned()) } /// Запоминает файл, открываемый при старте. pub fn set_startup_file(path: PathBuf) -> Result<(), String> { STARTUP_FILE .set(path) .map_err(|_| "файл для открытия уже задан".to_owned()) } /// Разбирает аргументы командной строки. /// /// Поддерживаются: /// * `файл` — первый позиционный аргумент, как в `vim file.go`; /// * `--config <каталог>` и `--config=<каталог>`; /// * `--` — всё после него считается позиционным (для имён, начинающихся с `-`). /// /// Неизвестные флаги игнорируются, чтобы запуск не ломался от лишних /// аргументов окружения/обёрток. pub fn parse_command_line(args: I) -> Result where I: IntoIterator, { let mut command_line = CommandLine::default(); let mut args = args.into_iter(); let mut positional_only = false; while let Some(arg) = args.next() { let text = arg.to_string_lossy(); if positional_only { if command_line.file.is_none() { command_line.file = Some(PathBuf::from(arg)); } continue; } if text == "--" { positional_only = true; } else if text == "--config" { let value = args .next() .ok_or_else(|| "--config требует путь к каталогу".to_owned())?; command_line.config_dir = Some(PathBuf::from(value)); } else if let Some(value) = text.strip_prefix("--config=") { if value.is_empty() { return Err("--config требует путь к каталогу".to_owned()); } command_line.config_dir = Some(PathBuf::from(value)); } else if !text.starts_with('-') && command_line.file.is_none() { command_line.file = Some(PathBuf::from(arg)); } } Ok(command_line) } /// Имя стартового сценария, который выполняется при запуске приложения. pub const STARTUP_SCRIPT: &str = "init.lua"; #[cfg(test)] mod tests { use super::*; fn args(values: &[&str]) -> Vec { values.iter().map(OsString::from).collect() } #[test] fn config_dir_is_read_from_both_argument_forms() { assert_eq!( parse_command_line(args(&["--config", "/tmp/project"])) .unwrap() .config_dir, Some(PathBuf::from("/tmp/project")) ); assert_eq!( parse_command_line(args(&["--config=/tmp/project"])) .unwrap() .config_dir, Some(PathBuf::from("/tmp/project")) ); assert!(parse_command_line(args(&[])).unwrap().config_dir.is_none()); } #[test] fn config_dir_requires_a_value() { assert!(parse_command_line(args(&["--config"])).is_err()); assert!(parse_command_line(args(&["--config="])).is_err()); } #[test] fn unknown_arguments_do_not_hide_the_config_dir() { let parsed = parse_command_line(args(&["--verbose", "--config", "cfg"])).unwrap(); assert_eq!(parsed.config_dir, Some(PathBuf::from("cfg"))); } #[test] fn first_positional_argument_is_the_file_to_open() { let parsed = parse_command_line(args(&["main.go"])).unwrap(); assert_eq!(parsed.file, Some(PathBuf::from("main.go"))); let parsed = parse_command_line(args(&["--config", "cfg", "main.go"])).unwrap(); assert_eq!(parsed.config_dir, Some(PathBuf::from("cfg"))); assert_eq!(parsed.file, Some(PathBuf::from("main.go"))); let parsed = parse_command_line(args(&["main.go", "--config=cfg"])).unwrap(); assert_eq!(parsed.config_dir, Some(PathBuf::from("cfg"))); assert_eq!(parsed.file, Some(PathBuf::from("main.go"))); } #[test] fn double_dash_allows_file_names_starting_with_dash() { let parsed = parse_command_line(args(&["--", "-weird.go"])).unwrap(); assert_eq!(parsed.file, Some(PathBuf::from("-weird.go"))); } #[test] fn only_the_first_positional_argument_is_opened() { let parsed = parse_command_line(args(&["first.go", "second.go"])).unwrap(); assert_eq!(parsed.file, Some(PathBuf::from("first.go"))); } }