complition

This commit is contained in:
2026-10-30 20:50:04 +03:00
parent 56115302df
commit 55c3955897
15 changed files with 4103 additions and 32 deletions
+179 -3
View File
@@ -1,5 +1,9 @@
//! Константы оформления окна и сценариев.
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
/// Ширина окна при запуске.
pub const WINDOW_WIDTH: f32 = 900.0;
@@ -33,11 +37,183 @@ pub const MIN_MAIN_AREA_WIDTH: f32 = 240.0;
/// Высота нижней панели при открытии.
pub const BOTTOM_PANEL_HEIGHT: f32 = 160.0;
/// Каталог, в котором приложение ищет Lua-сценарии.
/// Каталог, в котором приложение ищет Lua-сценарии по умолчанию.
///
/// По умолчанию это текущий рабочий каталог процесса — именно там приложение
/// ожидает `init.lua`.
/// Это текущий рабочий каталог процесса — именно там приложение ожидает
/// `init.lua`, если не передан аргумент `--config`. Конкретный каталог всегда
/// возвращает [`script_dir`].
pub const SCRIPT_DIR: &str = ".";
/// Каталог сценариев, заданный через `--config`.
static SCRIPT_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
/// Файл, который нужно открыть при старте (первый позиционный аргумент).
static STARTUP_FILE: OnceLock<PathBuf> = OnceLock::new();
/// Разобранные аргументы командной строки.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CommandLine {
/// Каталог, из которого брать `init.lua`.
pub config_dir: Option<PathBuf>,
/// Файл, открываемый при старте как `vim file.go`.
pub file: Option<PathBuf>,
}
/// Каталог, из которого загружаются `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<I>(args: I) -> Result<CommandLine, String>
where
I: IntoIterator<Item = OsString>,
{
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<OsString> {
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")));
}
}