use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayConfig {
#[serde(default = "default_mode")]
pub mode: String,
#[serde(default = "default_scale")]
pub scale: String,
}
fn default_mode() -> String {
"80x25".to_string()
}
fn default_scale() -> String {
"auto".to_string()
}
impl Default for DisplayConfig {
fn default() -> Self {
Self {
mode: default_mode(),
scale: default_scale(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FontConfig {
#[serde(default = "default_font_name")]
pub name: String,
}
fn default_font_name() -> String {
"Unifont-APL8x16".to_string()
}
impl Default for FontConfig {
fn default() -> Self {
Self {
name: default_font_name(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MouseConfig {
#[serde(default)]
pub device: Option<String>,
#[serde(default)]
pub invert_x: bool,
#[serde(default)]
pub invert_y: bool,
#[serde(default)]
pub swap_buttons: bool,
#[serde(default)]
pub sensitivity: Option<f32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FramebufferConfig {
#[serde(default)]
pub display: DisplayConfig,
#[serde(default)]
pub font: FontConfig,
#[serde(default)]
pub mouse: MouseConfig,
}
impl FramebufferConfig {
pub fn config_path() -> Option<PathBuf> {
let config_dir = dirs::config_dir()?;
let app_config_dir = config_dir.join("term39");
Some(app_config_dir.join("fb.toml"))
}
pub fn exists() -> bool {
Self::config_path().map(|p| p.exists()).unwrap_or(false)
}
pub fn load() -> Self {
let path = match Self::config_path() {
Some(p) => p,
None => return Self::default(),
};
if !path.exists() {
return Self::default();
}
match fs::read_to_string(&path) {
Ok(contents) => toml::from_str(&contents).unwrap_or_default(),
Err(_) => Self::default(),
}
}
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let path = Self::config_path().ok_or("Could not determine config path")?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let toml_string = toml::to_string_pretty(self)?;
let content = format!(
"# term39 Framebuffer Configuration\n\
# Generated by term39 --fb-setup\n\
# Edit this file to customize framebuffer settings\n\
# or run term39 --fb-setup to use the configuration wizard\n\n\
{}\n",
toml_string
);
fs::write(path, content)?;
Ok(())
}
pub const TEXT_MODES: [&'static str; 8] = [
"40x25", "80x25", "80x43", "80x50", "160x50", "160x100", "320x100", "320x200",
];
pub const TEXT_MODE_DESCRIPTIONS: [&'static str; 8] = [
"16x16 cells",
"8x16 cells - Standard DOS",
"8x11 cells",
"8x8 cells - High density",
"8x16 cells - Double-wide",
"8x16 cells - High resolution",
"8x16 cells - Ultra-wide",
"8x8 cells - Maximum",
];
pub const SCALE_OPTIONS: [&'static str; 5] = ["auto", "1", "2", "3", "4"];
pub const MOUSE_DEVICE_OPTIONS: [&'static str; 6] = [
"auto",
"/dev/input/mice",
"/dev/input/event0",
"/dev/input/event1",
"/dev/input/event2",
"/dev/input/event3",
];
pub const TEXT_MODE_FONT_DIMS: [(usize, usize); 8] = [
(16, 16), (8, 16), (8, 11), (8, 8), (8, 16), (8, 16), (8, 16), (8, 8), ];
pub fn get_mode_font_dims(mode_index: usize) -> (usize, usize) {
Self::TEXT_MODE_FONT_DIMS
.get(mode_index)
.copied()
.unwrap_or((8, 16))
}
pub fn mode_index(&self) -> usize {
Self::TEXT_MODES
.iter()
.position(|&m| m == self.display.mode)
.unwrap_or(1) }
pub fn scale_index(&self) -> usize {
Self::SCALE_OPTIONS
.iter()
.position(|&s| s == self.display.scale)
.unwrap_or(0) }
pub fn set_mode_by_index(&mut self, index: usize) {
if index < Self::TEXT_MODES.len() {
self.display.mode = Self::TEXT_MODES[index].to_string();
}
}
pub fn set_scale_by_index(&mut self, index: usize) {
if index < Self::SCALE_OPTIONS.len() {
self.display.scale = Self::SCALE_OPTIONS[index].to_string();
}
}
pub fn cycle_scale(&mut self) {
let current = self.scale_index();
let next = (current + 1) % Self::SCALE_OPTIONS.len();
self.set_scale_by_index(next);
}
pub fn cycle_scale_reverse(&mut self) {
let current = self.scale_index();
let prev = if current == 0 {
Self::SCALE_OPTIONS.len() - 1
} else {
current - 1
};
self.set_scale_by_index(prev);
}
pub fn toggle_invert_x(&mut self) {
self.mouse.invert_x = !self.mouse.invert_x;
}
pub fn toggle_invert_y(&mut self) {
self.mouse.invert_y = !self.mouse.invert_y;
}
pub fn toggle_swap_buttons(&mut self) {
self.mouse.swap_buttons = !self.mouse.swap_buttons;
}
pub fn device_index(&self) -> usize {
match &self.mouse.device {
None => 0, Some(dev) => Self::MOUSE_DEVICE_OPTIONS
.iter()
.position(|&d| d == dev)
.unwrap_or(0),
}
}
pub fn set_device_by_index(&mut self, index: usize) {
if index < Self::MOUSE_DEVICE_OPTIONS.len() {
let device = Self::MOUSE_DEVICE_OPTIONS[index];
if device == "auto" {
self.mouse.device = None;
} else {
self.mouse.device = Some(device.to_string());
}
}
}
pub fn cycle_device(&mut self) {
let current = self.device_index();
let next = (current + 1) % Self::MOUSE_DEVICE_OPTIONS.len();
self.set_device_by_index(next);
}
pub fn cycle_device_reverse(&mut self) {
let current = self.device_index();
let prev = if current == 0 {
Self::MOUSE_DEVICE_OPTIONS.len() - 1
} else {
current - 1
};
self.set_device_by_index(prev);
}
pub fn device_display_name(&self) -> &str {
match &self.mouse.device {
None => "auto",
Some(dev) => dev,
}
}
pub fn get_mouse_device(&self) -> String {
match &self.mouse.device {
None => "/dev/input/mice".to_string(),
Some(dev) => dev.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = FramebufferConfig::default();
assert_eq!(config.display.mode, "80x25");
assert_eq!(config.display.scale, "auto");
assert_eq!(config.font.name, "Unifont-APL8x16");
assert!(!config.mouse.invert_x);
assert!(!config.mouse.invert_y);
assert!(config.mouse.device.is_none());
}
#[test]
fn test_mode_index() {
let config = FramebufferConfig::default();
assert_eq!(config.mode_index(), 1); }
#[test]
fn test_scale_index() {
let config = FramebufferConfig::default();
assert_eq!(config.scale_index(), 0); }
#[test]
fn test_set_mode_by_index() {
let mut config = FramebufferConfig::default();
config.set_mode_by_index(3); assert_eq!(config.display.mode, "80x50");
}
#[test]
fn test_cycle_scale() {
let mut config = FramebufferConfig::default();
assert_eq!(config.display.scale, "auto");
config.cycle_scale();
assert_eq!(config.display.scale, "1");
config.cycle_scale();
assert_eq!(config.display.scale, "2");
}
#[test]
fn test_toggle_invert() {
let mut config = FramebufferConfig::default();
assert!(!config.mouse.invert_x);
config.toggle_invert_x();
assert!(config.mouse.invert_x);
config.toggle_invert_x();
assert!(!config.mouse.invert_x);
}
#[test]
fn test_serialization() {
let config = FramebufferConfig::default();
let toml_str = toml::to_string(&config).unwrap();
let parsed: FramebufferConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(config.display.mode, parsed.display.mode);
assert_eq!(config.font.name, parsed.font.name);
}
}