use std::env;
use std::fs;
use std::path::PathBuf;
#[derive(Clone)]
pub struct Settings {
pub language: String,
pub spawn_accel: String,
pub close_accel: String,
pub focus_left_accel: String,
pub focus_right_accel: String,
pub focus_up_accel: String,
pub focus_down_accel: String,
pub equalize_panes_accel: String,
pub palette: String,
pub custom_foreground: String,
pub custom_background: String,
pub custom_colors: [String; 16],
pub font: String,
pub font_weight: i32,
pub cursor_style: String,
pub cursor_color: String,
pub cursor_blink: bool,
pub cursor_blink_on: i32,
pub cursor_blink_off: i32,
pub selection_foreground: String,
pub selection_background: String,
pub suggestion_color: String,
pub pane_gap: i32,
pub pane_layout: String,
pub max_panes: i32,
pub persist_dividers: bool,
pub divider_columns: String,
pub divider_rows: String,
pub base_width: i32,
pub base_height: i32,
pub focus_color: String,
pub focus_width: i32,
pub focus_style: String,
pub focus_animation: bool,
pub background_opacity: i32,
pub colors: bool,
pub center_on_expand: bool,
pub copy_on_select: bool,
pub middle_click_paste: bool,
pub bracketed_paste: bool,
pub trim_selection: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
language: "en".to_string(),
spawn_accel: platform_accelerator("<Alt>Return", "<Super>Return"),
close_accel: platform_accelerator("<Alt>w", "<Super>w"),
focus_left_accel: "<Alt>Left".to_string(),
focus_right_accel: "<Alt>Right".to_string(),
focus_up_accel: "<Alt>Up".to_string(),
focus_down_accel: "<Alt>Down".to_string(),
equalize_panes_accel: platform_accelerator("<Alt>e", "<Super>e"),
palette: "catppuccin-mocha".to_string(),
custom_foreground: "#e8e8e8".to_string(),
custom_background: "#000000".to_string(),
custom_colors: [
"#1a1a1a", "#ff5555", "#50fa7b", "#f1fa8c", "#8be9fd", "#bd93f9", "#8be9fd",
"#e8e8e8", "#555555", "#ff6e6e", "#69ff94", "#ffffa5", "#d6acff", "#ff92df",
"#a4ffff", "#ffffff",
]
.map(str::to_string),
font: "JetBrains Mono 11".to_string(),
font_weight: 500,
cursor_style: "application".to_string(),
cursor_color: "#f0b35a".to_string(),
cursor_blink: true,
cursor_blink_on: 600,
cursor_blink_off: 400,
selection_foreground: "#ffffff".to_string(),
selection_background: "#5a78b4".to_string(),
suggestion_color: "#7f849c".to_string(),
pane_gap: 3,
pane_layout: "auto".to_string(),
max_panes: 4,
persist_dividers: true,
divider_columns: String::new(),
divider_rows: String::new(),
base_width: 820,
base_height: 500,
focus_color: "#f0b35a".to_string(),
focus_width: 1,
focus_style: "solid".to_string(),
focus_animation: true,
background_opacity: 100,
colors: true,
center_on_expand: true,
copy_on_select: false,
middle_click_paste: true,
bracketed_paste: true,
trim_selection: true,
}
}
}
fn platform_accelerator(other: &str, macos: &str) -> String {
if cfg!(target_os = "macos") {
macos.to_string()
} else {
other.to_string()
}
}
impl Settings {
pub fn load() -> Self {
let mut settings = Self::default();
let Ok(contents) = fs::read_to_string(config_path()) else {
return settings;
};
for line in contents.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key {
"language" => settings.language = value.to_string(),
"spawn_accel" => settings.spawn_accel = value.to_string(),
"close_accel" => settings.close_accel = value.to_string(),
"focus_left_accel" => settings.focus_left_accel = value.to_string(),
"focus_right_accel" => settings.focus_right_accel = value.to_string(),
"focus_up_accel" => settings.focus_up_accel = value.to_string(),
"focus_down_accel" => settings.focus_down_accel = value.to_string(),
"equalize_panes_accel" => settings.equalize_panes_accel = value.to_string(),
"palette" => settings.palette = value.to_string(),
"custom_foreground" => settings.custom_foreground = value.to_string(),
"custom_background" => settings.custom_background = value.to_string(),
key if key.starts_with("custom_color_") => {
if let Ok(index) = key[13..].parse::<usize>()
&& index < 16
{
settings.custom_colors[index] = value.to_string();
}
}
"font" => settings.font = value.to_string(),
"font_weight" => settings.font_weight = parse_range(value, 100, 900, 500),
"cursor_style" => settings.cursor_style = value.to_string(),
"cursor_color" => settings.cursor_color = value.to_string(),
"cursor_blink" => settings.cursor_blink = value == "true",
"cursor_blink_on" => {
settings.cursor_blink_on = parse_range(value, 100, 2_000, 600);
}
"cursor_blink_off" => {
settings.cursor_blink_off = parse_range(value, 100, 2_000, 400);
}
"selection_foreground" => settings.selection_foreground = value.to_string(),
"selection_background" => settings.selection_background = value.to_string(),
"suggestion_color" => settings.suggestion_color = value.to_string(),
"pane_gap" => settings.pane_gap = parse_range(value, 0, 32, 3),
"pane_layout" => settings.pane_layout = value.to_string(),
"max_panes" => settings.max_panes = parse_range(value, 1, 9, 4),
"persist_dividers" => settings.persist_dividers = value == "true",
"divider_columns" => settings.divider_columns = value.to_string(),
"divider_rows" => settings.divider_rows = value.to_string(),
"base_width" => settings.base_width = parse_range(value, 320, 10_000, 820),
"base_height" => settings.base_height = parse_range(value, 200, 10_000, 500),
"focus_color" => settings.focus_color = value.to_string(),
"focus_width" => settings.focus_width = parse_range(value, 1, 12, 1),
"focus_style" => settings.focus_style = value.to_string(),
"focus_animation" => settings.focus_animation = value == "true",
"background_opacity" => {
settings.background_opacity = parse_range(value, 10, 100, 100);
}
"colors" => settings.colors = value == "true",
"center_on_expand" => settings.center_on_expand = value == "true",
"copy_on_select" => settings.copy_on_select = value == "true",
"middle_click_paste" => settings.middle_click_paste = value == "true",
"bracketed_paste" => settings.bracketed_paste = value == "true",
"trim_selection" => settings.trim_selection = value == "true",
_ => {}
}
}
settings
}
pub fn save(&self) -> Result<(), String> {
let path = config_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
}
let custom = self
.custom_colors
.iter()
.enumerate()
.map(|(index, value)| format!("custom_color_{index}={value}\n"))
.collect::<String>();
let contents = format!(
"language={}\nspawn_accel={}\nclose_accel={}\nfocus_left_accel={}\nfocus_right_accel={}\nfocus_up_accel={}\nfocus_down_accel={}\nequalize_panes_accel={}\npalette={}\ncustom_foreground={}\ncustom_background={}\n{}font={}\nfont_weight={}\ncursor_style={}\ncursor_color={}\ncursor_blink={}\ncursor_blink_on={}\ncursor_blink_off={}\nselection_foreground={}\nselection_background={}\nsuggestion_color={}\npane_gap={}\npane_layout={}\nmax_panes={}\npersist_dividers={}\ndivider_columns={}\ndivider_rows={}\nbase_width={}\nbase_height={}\nfocus_color={}\nfocus_width={}\nfocus_style={}\nfocus_animation={}\nbackground_opacity={}\ncolors={}\ncenter_on_expand={}\ncopy_on_select={}\nmiddle_click_paste={}\nbracketed_paste={}\ntrim_selection={}\n",
self.language,
self.spawn_accel,
self.close_accel,
self.focus_left_accel,
self.focus_right_accel,
self.focus_up_accel,
self.focus_down_accel,
self.equalize_panes_accel,
self.palette,
self.custom_foreground,
self.custom_background,
custom,
self.font,
self.font_weight,
self.cursor_style,
self.cursor_color,
self.cursor_blink,
self.cursor_blink_on,
self.cursor_blink_off,
self.selection_foreground,
self.selection_background,
self.suggestion_color,
self.pane_gap,
self.pane_layout,
self.max_panes,
self.persist_dividers,
self.divider_columns,
self.divider_rows,
self.base_width,
self.base_height,
self.focus_color,
self.focus_width,
self.focus_style,
self.focus_animation,
self.background_opacity,
self.colors,
self.center_on_expand,
self.copy_on_select,
self.middle_click_paste,
self.bracketed_paste,
self.trim_selection
);
let temporary = path.with_extension(format!("tmp-{}", std::process::id()));
fs::write(&temporary, contents).map_err(|error| error.to_string())?;
replace_file(&temporary, &path).map_err(|error| {
let _ = fs::remove_file(&temporary);
error.to_string()
})
}
}
#[cfg(not(target_os = "windows"))]
fn replace_file(source: &std::path::Path, destination: &std::path::Path) -> std::io::Result<()> {
fs::rename(source, destination)
}
#[cfg(target_os = "windows")]
fn replace_file(source: &std::path::Path, destination: &std::path::Path) -> std::io::Result<()> {
if destination.exists() {
fs::remove_file(destination)?;
}
fs::rename(source, destination)
}
#[cfg(target_os = "windows")]
fn config_path() -> PathBuf {
env::var_os("APPDATA")
.map(PathBuf::from)
.map(|path| path.join("Arreliny/config"))
.unwrap_or_else(|| PathBuf::from("arreliny.conf"))
}
#[cfg(target_os = "macos")]
fn config_path() -> PathBuf {
env::var_os("HOME")
.map(PathBuf::from)
.map(|path| path.join("Library/Application Support/Arreliny/config"))
.unwrap_or_else(|| PathBuf::from("arreliny.conf"))
}
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
fn config_path() -> PathBuf {
env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.map(|path| path.join("arreliny/config"))
.or_else(|| {
env::var_os("HOME")
.map(PathBuf::from)
.map(|path| path.join(".config/arreliny/config"))
})
.unwrap_or_else(|| PathBuf::from("arreliny.conf"))
}
fn parse_range(value: &str, minimum: i32, maximum: i32, fallback: i32) -> i32 {
value
.parse::<i32>()
.ok()
.filter(|value| (minimum..=maximum).contains(value))
.unwrap_or(fallback)
}