use config::Config;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug, Deserialize, Default)]
pub struct GlobalConfig {
#[serde(default)]
pub notebook: String,
}
#[derive(Debug, Deserialize)]
pub struct Alias {
pub cmd: String,
#[serde(default)]
pub desc: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SmaranaConfig {
#[serde(default = "default_editor")]
pub editor: String,
#[serde(default = "default_shell")]
pub shell: String,
#[serde(default = "default_title")]
pub default_title: String,
#[serde(default = "default_filename_gen")]
pub filename_gen: String,
}
impl Default for SmaranaConfig {
fn default() -> Self {
Self {
editor: default_editor(),
shell: default_shell(),
default_title: default_title(),
filename_gen: default_filename_gen(),
}
}
}
fn default_editor() -> String {
std::env::var("EDITOR").unwrap_or_else(|_| "nvim".to_string())
}
fn default_shell() -> String {
"/bin/bash".to_string()
}
fn default_title() -> String {
"Untitled Note".to_string()
}
fn default_filename_gen() -> String {
"tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]\\+/-/g;s/^-//;s/-$//' | sed \"s/^/$(date +%F)-/\"".to_string()
}
#[derive(Debug, Deserialize)]
pub struct FrontmatterConfig {
pub template: String,
#[serde(flatten)]
pub variables: HashMap<String, String>,
}
impl Default for FrontmatterConfig {
fn default() -> Self {
Self {
template: "default.typ".to_string(),
variables: HashMap::new(),
}
}
}
#[derive(Debug, Deserialize, Default)]
pub struct AppConfig {
#[serde(default)]
pub smarana: SmaranaConfig,
#[serde(default)]
pub frontmatter: FrontmatterConfig,
#[serde(default)]
pub alias: HashMap<String, Alias>,
}
impl GlobalConfig {
pub fn load() -> Self {
let path = global_config_path();
if !path.exists() {
return GlobalConfig::default();
}
let settings = Config::builder()
.add_source(config::File::from(path).required(false))
.build();
match settings {
Ok(cfg) => cfg.try_deserialize().unwrap_or_else(|e| {
eprintln!("Warning: failed to parse global config: {e}");
GlobalConfig::default()
}),
Err(e) => {
eprintln!("Warning: failed to load global config: {e}");
GlobalConfig::default()
}
}
}
pub fn notebook_path(&self) -> Option<PathBuf> {
if self.notebook.is_empty() {
return None;
}
let expanded = expand_home_var(&self.notebook);
Some(PathBuf::from(expanded))
}
}
impl AppConfig {
pub fn load() -> Self {
let path = notebook_config_path();
if !path.exists() {
return AppConfig::default();
}
let settings = Config::builder()
.add_source(config::File::from(path).required(false))
.build();
match settings {
Ok(cfg) => cfg.try_deserialize().unwrap_or_else(|e| {
eprintln!("Warning: failed to parse notebook config: {e}");
AppConfig::default()
}),
Err(e) => {
eprintln!("Warning: failed to load notebook config: {e}");
AppConfig::default()
}
}
}
pub fn list_aliases(&self) {
if self.alias.is_empty() {
println!("No aliases configured.");
return;
}
let mut aliases: Vec<_> = self.alias.iter().collect();
aliases.sort_by_key(|(k, _)| (*k).clone());
for (name, alias) in aliases {
if let Some(desc) = &alias.desc {
println!("{name} - {desc}");
} else {
println!("{name}");
}
}
}
pub fn run_alias(&self, name: &str) -> bool {
if let Some(alias) = self.alias.get(name) {
let status = Command::new("sh")
.arg("-c")
.arg(&alias.cmd)
.status();
match status {
Ok(s) => {
if !s.success() {
std::process::exit(s.code().unwrap_or(1));
}
}
Err(e) => {
eprintln!("Failed to run alias '{name}': {e}");
std::process::exit(1);
}
}
true
} else {
false
}
}
}
pub fn global_config_path() -> PathBuf {
let config_home = std::env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
format!("{home}/.config")
});
PathBuf::from(config_home)
.join("smarana")
.join("config.toml")
}
pub fn expand_home_var(path: &str) -> String {
if let Ok(home) = std::env::var("HOME") {
path.replace("$HOME", &home)
} else {
path.to_string()
}
}
pub fn notebook_config_path() -> PathBuf {
let global = GlobalConfig::load();
if let Some(notebook) = global.notebook_path() {
notebook.join(".smarana").join("config.toml")
} else {
let mut path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
path.push(".smarana");
path.push("config.toml");
path
}
}
pub fn open_config() {
let path = notebook_config_path();
if !path.exists() {
eprintln!(
"No notebook config found at: {}\nRun `sma -i` to initialize a notebook first.",
path.display()
);
return;
}
let app_config = AppConfig::load();
let editor = app_config.smarana.editor;
let status = Command::new(&editor)
.arg(&path)
.status();
match status {
Ok(s) if !s.success() => {
eprintln!("Editor exited with non-zero status");
}
Err(e) => {
eprintln!("Failed to open editor '{editor}': {e}");
}
_ => {}
}
}