use std::fs;
use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum ViewSwitchError {
#[error("Failed to read main.rs: {source}")]
MainFileRead {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("Failed to write main.rs: {source}")]
MainFileWrite {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("Failed to read directory {path}: {source}")]
DirectoryRead {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("Could not find Message enum in main.rs")]
MessageEnumNotFound,
#[error("Could not find #[dampen_app] macro in main.rs")]
DampenAppMacroNotFound,
}
pub fn detect_second_window(project_root: &Path) -> Result<bool, ViewSwitchError> {
let ui_dir = project_root.join("src/ui");
if !ui_dir.exists() {
return Ok(false);
}
let entries = fs::read_dir(&ui_dir).map_err(|source| ViewSwitchError::DirectoryRead {
path: ui_dir.clone(),
source,
})?;
let mut rs_file_count = 0;
for entry in entries {
let entry = entry.map_err(|source| ViewSwitchError::DirectoryRead {
path: ui_dir.clone(),
source,
})?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
if path.file_name().is_some_and(|name| name != "mod.rs") {
rs_file_count += 1;
if rs_file_count >= 2 {
return Ok(true);
}
}
}
}
Ok(false)
}
pub fn activate_view_switching(project_root: &Path) -> Result<(), ViewSwitchError> {
let main_path = project_root.join("src/main.rs");
let content =
fs::read_to_string(&main_path).map_err(|source| ViewSwitchError::MainFileRead {
path: main_path.clone(),
source,
})?;
let content = uncomment_or_add_switch_message(&content)?;
let content = add_switch_variant_to_macro(&content)?;
fs::write(&main_path, content).map_err(|source| ViewSwitchError::MainFileWrite {
path: main_path.clone(),
source,
})?;
Ok(())
}
fn uncomment_or_add_switch_message(content: &str) -> Result<String, ViewSwitchError> {
if content.contains("SwitchToView(CurrentView)")
&& !content.contains("// SwitchToView(CurrentView)")
{
return Ok(content.to_string());
}
if content.contains("// SwitchToView(CurrentView)") {
let result = content.replace(
"// SwitchToView(CurrentView),",
"SwitchToView(CurrentView),",
);
return Ok(result);
}
let enum_pattern = "enum Message {";
if let Some(enum_pos) = content.find(enum_pattern) {
let insert_pos = enum_pos + enum_pattern.len();
let mut result = String::with_capacity(content.len() + 100);
result.push_str(&content[..insert_pos]);
result.push_str("\n /// View switching\n SwitchToView(CurrentView),");
result.push_str(&content[insert_pos..]);
return Ok(result);
}
Err(ViewSwitchError::MessageEnumNotFound)
}
fn add_switch_variant_to_macro(content: &str) -> Result<String, ViewSwitchError> {
if content.contains(r#"switch_view_variant = "SwitchToView""#)
&& !content.contains(r#"// switch_view_variant = "SwitchToView""#)
{
return Ok(content.to_string());
}
if content.contains(r#"// switch_view_variant = "SwitchToView""#) {
let result = content
.replace(
r#"// switch_view_variant = "SwitchToView","#,
r#"switch_view_variant = "SwitchToView","#,
)
.replace(
r#"// switch_view_variant = "SwitchToView""#,
r#"switch_view_variant = "SwitchToView","#,
);
return Ok(result);
}
if let Some(macro_start) = content.find("#[dampen_app(")
&& let Some(macro_end) = content[macro_start..].find(")]")
{
let absolute_macro_end = macro_start + macro_end;
let before_close = &content[..absolute_macro_end];
let lines: Vec<&str> = before_close.lines().collect();
let last_non_empty_idx = lines.iter().rposition(|line| !line.trim().is_empty());
if let Some(idx) = last_non_empty_idx {
let mut result = String::with_capacity(content.len() + 100);
for (i, line) in lines.iter().enumerate() {
result.push_str(line);
result.push('\n');
if i == idx {
if !line.trim_end().ends_with(',') {
result.pop(); result.push(',');
result.push('\n');
}
result.push_str(r#" switch_view_variant = "SwitchToView","#);
result.push('\n');
}
}
result.push_str(&content[absolute_macro_end..]);
return Ok(result);
}
let mut result = String::with_capacity(content.len() + 100);
result.push_str(&content[..absolute_macro_end]);
result.push_str(r#" switch_view_variant = "SwitchToView","#);
result.push('\n');
result.push_str(&content[absolute_macro_end..]);
return Ok(result);
}
Err(ViewSwitchError::DampenAppMacroNotFound)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_detect_second_window_no_ui_dir() {
let temp = TempDir::new().unwrap();
let result = detect_second_window(temp.path());
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_detect_second_window_only_one_file() {
let temp = TempDir::new().unwrap();
let ui_dir = temp.path().join("src/ui");
fs::create_dir_all(&ui_dir).unwrap();
fs::write(ui_dir.join("window.rs"), "").unwrap();
fs::write(ui_dir.join("mod.rs"), "").unwrap();
let result = detect_second_window(temp.path());
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_detect_second_window_two_files() {
let temp = TempDir::new().unwrap();
let ui_dir = temp.path().join("src/ui");
fs::create_dir_all(&ui_dir).unwrap();
fs::write(ui_dir.join("window.rs"), "").unwrap();
fs::write(ui_dir.join("settings.rs"), "").unwrap();
fs::write(ui_dir.join("mod.rs"), "").unwrap();
let result = detect_second_window(temp.path());
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_uncomment_switch_message() {
let input = r#"
enum Message {
// SwitchToView(CurrentView),
Handler(HandlerMessage),
}
"#;
let result = uncomment_or_add_switch_message(input).unwrap();
assert!(result.contains("SwitchToView(CurrentView),"));
assert!(!result.contains("// SwitchToView(CurrentView),"));
}
#[test]
fn test_add_switch_message_when_missing() {
let input = r#"
enum Message {
Handler(HandlerMessage),
}
"#;
let result = uncomment_or_add_switch_message(input).unwrap();
assert!(result.contains("SwitchToView(CurrentView),"));
assert!(result.contains("/// View switching"));
}
#[test]
fn test_uncomment_switch_variant_in_macro() {
let input = r#"
#[dampen_app(
ui_dir = "src/ui",
message_type = "Message",
// switch_view_variant = "SwitchToView",
)]
"#;
let result = add_switch_variant_to_macro(input).unwrap();
assert!(result.contains(r#"switch_view_variant = "SwitchToView","#));
assert!(!result.contains(r#"// switch_view_variant"#));
}
#[test]
fn test_add_switch_variant_when_missing() {
let input = r#"
#[dampen_app(
ui_dir = "src/ui",
message_type = "Message"
)]
"#;
let result = add_switch_variant_to_macro(input).unwrap();
assert!(result.contains(r#"switch_view_variant = "SwitchToView","#));
assert!(result.contains(r#"message_type = "Message","#));
}
#[test]
fn test_add_switch_variant_when_missing_with_comma() {
let input = r#"
#[dampen_app(
ui_dir = "src/ui",
message_type = "Message",
)]
"#;
let result = add_switch_variant_to_macro(input).unwrap();
assert!(result.contains(r#"switch_view_variant = "SwitchToView","#));
assert!(result.contains(r#"message_type = "Message","#));
}
#[test]
fn test_already_activated_no_changes() {
let input = r#"
enum Message {
SwitchToView(CurrentView),
Handler(HandlerMessage),
}
#[dampen_app(
ui_dir = "src/ui",
switch_view_variant = "SwitchToView",
)]
"#;
let result1 = uncomment_or_add_switch_message(input).unwrap();
let result2 = add_switch_variant_to_macro(&result1).unwrap();
assert_eq!(result1, input);
assert_eq!(result2, result1);
}
}