use crate::gtk::prelude::ListModelExtManual;
use crate::widget::{
unit_list::UnitCuratedList, unit_properties_selector::data_selection::UnitPropertySelection,
};
use serde::{Deserialize, Serialize};
use std::{
env,
fmt::Display,
fs::{self, File},
io::Write,
path::Path,
};
use tracing::{debug, error, info, warn};
const UNIT_COLUMNS: &str = "unit_columns.toml";
#[derive(Serialize, Deserialize, Debug)]
#[serde(default)]
pub struct UnitColumn {
pub id: String,
pub title: Option<String>,
#[serde(rename = "width")]
pub fixed_width: i32,
pub expands: bool,
pub resizable: bool,
pub visible: bool,
#[serde(rename = "type")]
pub prop_type: Option<String>,
pub sort: Option<SortType>,
}
impl Default for UnitColumn {
fn default() -> Self {
Self {
id: "".to_owned(),
title: None,
fixed_width: -1,
expands: false,
resizable: false,
visible: true,
prop_type: None,
sort: None,
}
}
}
impl UnitColumn {
pub fn from(data: &UnitPropertySelection) -> Self {
Self {
id: data.id().map(|s| s.to_string()).unwrap_or_default(),
title: data.title().map(|s| s.to_string()),
fixed_width: data.fixed_width(),
expands: data.expands(),
resizable: data.resizable(),
visible: data.visible(),
prop_type: data.prop_type(),
sort: None,
}
}
pub(crate) fn new(id: &str, arg: &str) -> Self {
Self {
id: id.to_owned(),
prop_type: Some(arg.to_owned()),
..Default::default()
}
}
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq, glib::Enum)]
#[enum_type(name = "SortType")]
pub enum SortType {
#[default]
Unset,
Asc,
Desc,
}
impl Display for SortType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match &self {
SortType::Asc => "Asc",
SortType::Desc => "Desc",
SortType::Unset => "",
};
write!(f, "{s}")
}
}
impl From<SortType> for Option<gtk::SortType> {
fn from(val: SortType) -> Self {
match val {
SortType::Unset => None,
SortType::Asc => Some(gtk::SortType::Ascending),
SortType::Desc => Some(gtk::SortType::Descending),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MyConfig {
#[serde(rename = "column")]
pub columns: Vec<UnitColumn>,
}
impl MyConfig {
pub fn is_empty(&self) -> bool {
self.columns.is_empty()
}
}
pub fn save_column_config(
columns: Option<&gio::ListModel>,
data: &mut [UnitPropertySelection],
view: UnitCuratedList,
) {
order_columns(columns, data);
let data_list: Vec<UnitColumn> = data.iter().map(UnitColumn::from).collect();
let config = MyConfig { columns: data_list };
let sysd_manager_config_dir = get_sysd_manager_config_dir();
if let Err(e) = fs::create_dir_all(&sysd_manager_config_dir) {
error!(
"Failed to create config directory {:?}: {}",
sysd_manager_config_dir, e
);
return;
}
let file_name = file_name(view);
let config_path = sysd_manager_config_dir.join(file_name);
if let Err(e) = save_to_toml_file(&config, &config_path) {
error!(
"Failed to save column config to TOML file: {:?} {:?}",
config_path, e
);
} else {
info!("Column config saved to {:?}", config_path);
}
}
fn file_name(view: UnitCuratedList) -> String {
match view {
UnitCuratedList::Custom => UNIT_COLUMNS.to_owned(),
_ => format!("{}_{}", view.id(), UNIT_COLUMNS),
}
}
pub fn order_columns(columns: Option<&gio::ListModel>, data: &mut [UnitPropertySelection]) {
let Some(columns) = columns else {
return;
};
let ids: Vec<_> = columns
.iter::<gtk::ColumnViewColumn>()
.filter_map(|result| result.inspect_err(|err| warn!("Error: {err:?}")).ok())
.filter_map(|column| column.id())
.collect();
if ids.len() != data.len() {
warn!("Lenght not in sync");
return;
}
for (index, id) in ids.iter().enumerate() {
let mut index_op: Option<usize> = None;
for (sub_index, ps) in data.iter().enumerate().skip(index) {
let Some(sub_id) = ps.id() else {
continue;
};
debug!("-- {index} {id} -- {sub_index} {sub_id} ");
if *id == sub_id {
if index != sub_index {
index_op = Some(sub_index);
}
break;
}
}
if let Some(index_op) = index_op {
debug!("Swap {id} {index} {index_op} ");
data.swap(index, index_op);
}
}
}
pub(crate) fn get_sysd_manager_config_dir() -> std::path::PathBuf {
let xdg_config_home = get_xdg_config_home();
Path::new(&xdg_config_home).join("sysd-manager")
}
fn get_xdg_config_home() -> String {
env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| {
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
format!("{}/.config", home)
})
}
pub(crate) fn save_to_toml_file<T>(data: &T, path: &Path) -> std::io::Result<()>
where
T: Serialize,
{
let toml_str = toml::to_string_pretty(data).expect("Failed to serialize data to TOML");
let mut file = File::create(path)?;
file.write_all(toml_str.as_bytes())?;
Ok(())
}
pub fn load_column_config(view: UnitCuratedList) -> Option<MyConfig> {
let sysd_manager_config_dir = get_sysd_manager_config_dir();
if !sysd_manager_config_dir.exists() {
info!(
"Config directory {:?} does not exist. Using default configuration.",
sysd_manager_config_dir
);
return None;
}
let file_name = file_name(view);
let config_path = sysd_manager_config_dir.join(file_name);
if !config_path.exists() {
info!(
"Config file {:?} does not exist. Using default configuration.",
config_path
);
return None;
}
match fs::read_to_string(&config_path) {
Ok(toml_str) => match toml::from_str::<MyConfig>(&toml_str) {
Ok(config) => {
if config.is_empty() {
warn!("Loaded config is empty, FALLBACK on default");
None
} else {
Some(config)
}
}
Err(e) => {
error!("Failed to parse TOML from {:?}: {}", config_path, e);
None
}
},
Err(e) => {
error!("Failed to read config file {:?}: {}", config_path, e);
None
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_save_array_of_structs_to_toml() {
let data_list = vec![
UnitColumn {
id: "alpha".to_string(),
title: Some("Alpha Title".to_string()),
fixed_width: 1,
expands: true,
resizable: false,
visible: true,
..Default::default()
},
UnitColumn {
id: "beta".to_string(),
title: Some("Beta Title".to_string()),
fixed_width: 2,
expands: false,
resizable: true,
visible: false,
..Default::default()
},
UnitColumn {
id: "gamma".to_string(),
title: Some("Gamma Title".to_string()),
fixed_width: 3,
expands: true,
resizable: true,
visible: true,
prop_type: Some("i".to_string()),
sort: Some(SortType::Asc),
},
UnitColumn {
id: "".to_string(),
title: None,
fixed_width: 3,
expands: true,
resizable: true,
visible: true,
..Default::default()
},
];
let config = MyConfig { columns: data_list };
let toml_str = toml::to_string_pretty(&config).expect("Failed to serialize array to TOML");
println!("{}", toml_str);
assert!(toml_str.contains("id = \"alpha\""));
assert!(toml_str.contains("id = \"beta\""));
assert!(toml_str.contains("title = \"Gamma Title\""));
assert!(toml_str.matches("[").count() >= 4); }
#[test]
fn test_toml_save_empty() {
let data_list = vec![];
let config = MyConfig { columns: data_list };
let toml_str = toml::to_string_pretty(&config).expect("Failed to serialize array to TOML");
println!("{}", toml_str);
}
#[test]
fn test_load_multiple_structs_from_toml_file() {
let toml_content = r#"
[[column]]
id = "alpha"
title = "Alpha Title"
fixed_width = 1
expands = true
resizable = false
visible = true
[[column]]
id = "beta"
title = "Beta Title"
width = 2
expands = false
resizable = true
visible = false
[[column]]
id = "gamma"
title = "Gamma Title"
width = 3
expands = true
resizable = true
visible = true
[[column]]
expands = true
resizable = true
visible = true
type = "i"
"#;
let config: MyConfig = toml::from_str(toml_content).expect("Failed to parse TOML");
assert!(config.columns.len() >= 4);
assert_eq!(config.columns[0].id.as_str(), "alpha");
assert_eq!(config.columns[1].fixed_width, 2);
assert!(config.columns[2].visible);
assert_eq!(config.columns[3].title, None);
assert_eq!(config.columns[3].fixed_width, -1);
assert_eq!(config.columns[3].prop_type, Some("i".to_string()));
}
#[test]
fn test_iter() {
let data = ['a', 'b', 'c', 'd'];
for (sub_index, ps) in data[2..].iter().enumerate() {
println!("{sub_index} {ps}")
}
}
}