feat(01-01): add Phase 1 deps and implement config/CLI loading

- Add signal-hook 0.4.3, toml 1.0.3, serde 1.0, clap 4.5 to Cargo.toml
- Create src/config.rs with Config struct (deny_unknown_fields, serde defaults)
- Implement load_config() with vault_path resolution relative to config file dir
- Implement resolve_config_path() using binary-adjacent bbs.toml as default
- Implement print_config_error() with BBS-themed error messages
- Implement detect_login_shell() and parse_cli() with argv[0] dash stripping
- Create src/terminal.rs with init_terminal(), restore_terminal(), install_panic_hook()
- Wire early startup path in src/main.rs before terminal initialization
This commit is contained in:
2026-02-28 21:12:29 +01:00
parent b258b6d262
commit 65313eac31
4 changed files with 2077 additions and 0 deletions
Generated
+1905
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "bbs-md"
version = "0.1.0"
edition = "2024"
[dependencies]
ratatui = "0.30.0"
signal-hook = "0.4.3"
toml = "1.0.3"
serde = { version = "1.0", features = ["derive"] }
clap = { version = "4.5", features = ["derive"] }
+158
View File
@@ -0,0 +1,158 @@
use serde::Deserialize;
use std::path::{Path, PathBuf};
// ── Config struct ────────────────────────────────────────────────────────────
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default = "default_vault_path")]
pub vault_path: PathBuf,
#[serde(default = "default_theme")]
#[allow(dead_code)]
pub theme: String,
}
fn default_vault_path() -> PathBuf {
PathBuf::from("./vault")
}
fn default_theme() -> String {
"default".to_string()
}
impl Default for Config {
fn default() -> Self {
Config {
vault_path: default_vault_path(),
theme: default_theme(),
}
}
}
// ── ConfigError ──────────────────────────────────────────────────────────────
#[allow(dead_code)]
pub enum ConfigError {
/// File exists but could not be read (permissions, I/O error)
ReadError(std::io::Error),
/// TOML syntax error or unknown field
ParseError(toml::de::Error),
/// vault_path does not exist as a directory
VaultNotFound(PathBuf),
}
// ── load_config ──────────────────────────────────────────────────────────────
/// Load configuration from `path`.
///
/// - If the file does not exist, returns `Ok(Config::default())`.
/// - Relative `vault_path` values are resolved against the config file's
/// parent directory (not cwd), because the config lives next to the binary.
pub fn load_config(path: &Path) -> Result<Config, ConfigError> {
if !path.exists() {
return Ok(Config::default());
}
let text = std::fs::read_to_string(path).map_err(ConfigError::ReadError)?;
let mut config: Config = toml::from_str(&text).map_err(ConfigError::ParseError)?;
// Resolve relative vault_path against the config file's parent directory.
if config.vault_path.is_relative() {
let base = path
.parent()
.unwrap_or_else(|| Path::new("."));
config.vault_path = base.join(&config.vault_path);
}
if !config.vault_path.exists() {
return Err(ConfigError::VaultNotFound(config.vault_path));
}
Ok(config)
}
// ── resolve_config_path ──────────────────────────────────────────────────────
/// Determine where to look for `bbs.toml`.
///
/// - If the user supplied `--config <FILE>`, use that path.
/// - Otherwise, look next to the running binary.
pub fn resolve_config_path(cli_config: Option<&Path>) -> PathBuf {
if let Some(p) = cli_config {
return p.to_path_buf();
}
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|d| d.join("bbs.toml")))
.unwrap_or_else(|| PathBuf::from("bbs.toml"))
}
// ── print_config_error ───────────────────────────────────────────────────────
/// Print a BBS-themed error message to stderr.
///
/// Called before terminal raw mode is active, so `eprintln!` is safe.
pub fn print_config_error(err: &ConfigError) {
match err {
ConfigError::ParseError(e) => {
eprintln!(
"SYSTEM ERROR: Configuration file corrupted. SysOp intervention required.\n\
Detail: {}",
e
);
}
ConfigError::VaultNotFound(path) => {
eprintln!(
"SYSTEM ERROR: Vault directory not found at '{}'. \
SysOp must verify vault_path in bbs.toml.",
path.display()
);
}
ConfigError::ReadError(_) => {
eprintln!(
"SYSTEM ERROR: Cannot read configuration file. Check file permissions."
);
}
}
}
// ── CLI ──────────────────────────────────────────────────────────────────────
#[derive(clap::Parser, Debug)]
#[command(name = "bbs-md", about = "BBS-style markdown vault reader")]
pub struct Cli {
/// Path to bbs.toml configuration file
#[arg(long = "config", short = 'c', value_name = "FILE")]
pub config: Option<PathBuf>,
}
// ── Login shell detection ────────────────────────────────────────────────────
/// Returns `true` when the process was launched as a login shell.
///
/// POSIX convention: the kernel prefixes argv[0] with '-' when a program is
/// used as a login shell (e.g. `-bbs-md` instead of `bbs-md`).
pub fn detect_login_shell() -> bool {
std::env::args_os()
.next()
.map(|a| a.to_string_lossy().starts_with('-'))
.unwrap_or(false)
}
/// Parse CLI arguments, stripping any leading dash from argv[0] first.
///
/// Must be called **after** `detect_login_shell()` has captured the original
/// argv[0], because this function mutates the collected argument list.
pub fn parse_cli() -> Cli {
use clap::Parser as _;
let mut args: Vec<std::ffi::OsString> = std::env::args_os().collect();
if let Some(first) = args.first_mut() {
let stripped = first.to_string_lossy().trim_start_matches('-').to_string();
*first = stripped.into();
}
Cli::parse_from(args)
}
+3
View File
@@ -18,6 +18,9 @@
//! Do NOT call `ratatui::restore()` — it enters alternate screen via `LeaveAlternateScreen`, //! Do NOT call `ratatui::restore()` — it enters alternate screen via `LeaveAlternateScreen`,
//! which we never entered. Use `restore_terminal()` from this module instead. //! which we never entered. Use `restore_terminal()` from this module instead.
// Functions in this module are used in Plan 02 and 03; suppress Phase 1 dead code warnings.
#![allow(dead_code)]
use std::io::stdout; use std::io::stdout;
use ratatui::crossterm::{ use ratatui::crossterm::{
execute, execute,