tidalcycles_rs/
supercollider_sc3_plugins.rs

1use std::fs;
2use std::fs::File;
3use std::io;
4use std::path::Path;
5use std::process::Command;
6use zip::ZipArchive;
7
8fn download_sc3_plugins(tmp_dir: &str) -> io::Result<String> {
9    // Ensure tmp_dir exists
10    fs::create_dir_all(tmp_dir)?;
11
12    if let None = crate::install::ensure_gh_installed() {
13        return Err(io::Error::new(
14            io::ErrorKind::Other,
15            "gh CLI is not installed",
16        ));
17    }
18
19    // Download the latest release zip using gh CLI with --clobber
20    let status = Command::new("gh")
21        .args([
22            "release",
23            "download",
24            "--repo",
25            "supercollider/sc3-plugins",
26            "--pattern",
27            "*Windows-64bit.zip",
28            "--dir",
29            tmp_dir,
30            "--clobber",
31        ])
32        .status()?;
33
34    if !status.success() {
35        return Err(io::Error::new(
36            io::ErrorKind::Other,
37            "Failed to download sc3-plugins",
38        ));
39    }
40
41    // Find the downloaded zip file
42    let zip_file = fs::read_dir(tmp_dir)?
43        .filter_map(|entry| entry.ok())
44        .find(|entry| {
45            entry
46                .file_name()
47                .to_string_lossy()
48                .ends_with("Windows-64bit.zip")
49        })
50        .map(|entry| entry.path())
51        .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Zip file not found"))?;
52
53    Ok(zip_file.to_string_lossy().to_string())
54}
55
56fn extract_zip(zip_path: &str, extract_to: &str) -> io::Result<()> {
57    let file = File::open(zip_path)?;
58    let mut archive = ZipArchive::new(file)?;
59    fs::create_dir_all(extract_to)?;
60
61    for i in 0..archive.len() {
62        let mut file = archive.by_index(i)?;
63        let outpath = Path::new(extract_to).join(file.name());
64
65        if file.is_dir() {
66            fs::create_dir_all(&outpath)?;
67        } else {
68            if let Some(parent) = outpath.parent() {
69                fs::create_dir_all(parent)?;
70            }
71            let mut outfile = File::create(&outpath)?;
72            io::copy(&mut file, &mut outfile)?;
73        }
74    }
75    Ok(())
76}
77
78fn copy_sc3_plugins(extracted_dir: &str, sc_user_plugins_dir: &str) -> io::Result<()> {
79    // Copy all plugin files to SuperCollider's user plugins directory
80    fs::create_dir_all(sc_user_plugins_dir)?;
81    for entry in fs::read_dir(extracted_dir)? {
82        let entry = entry?;
83        let path = entry.path();
84        if path.is_dir() {
85            let dest = Path::new(sc_user_plugins_dir).join(entry.file_name());
86            if dest.exists() {
87                fs::remove_dir_all(&dest)?;
88            }
89            fs::rename(&path, &dest)?;
90        }
91    }
92    Ok(())
93}
94
95// Example usage
96pub fn install_sc3_plugins() -> io::Result<()> {
97    let tmp_dir = "tmp_gh";
98    let sc_user_plugins_dir = dirs::home_dir()
99        .expect("Could not find home directory")
100        .join("AppData/Local/SuperCollider/Extensions/sc3-plugins");
101
102    // Use a scope to ensure cleanup even if error occurs
103    let result = (|| {
104        let zip_path = download_sc3_plugins(tmp_dir)?;
105        let extract_to = format!("{}/extracted", tmp_dir);
106        extract_zip(&zip_path, &extract_to)?;
107        copy_sc3_plugins(&extract_to, sc_user_plugins_dir.to_str().unwrap())?;
108        Ok(())
109    })();
110
111    // Cleanup tmp_gh directory
112    if let Err(e) = fs::remove_dir_all(tmp_dir) {
113        eprintln!("Warning: failed to remove temp dir {}: {}", tmp_dir, e);
114    }
115
116    if result.is_ok() {
117        println!("sc3-plugins installed successfully.");
118    }
119    result
120}
121
122pub fn is_sc3_plugins_installed() -> bool {
123    let sc_user_plugins_dir = match dirs::home_dir() {
124        Some(home) => home.join("AppData/Local/SuperCollider/Extensions/sc3-plugins"),
125        None => return false,
126    };
127    sc_user_plugins_dir.exists()
128        && sc_user_plugins_dir
129            .read_dir()
130            .map(|mut d| d.next().is_some())
131            .unwrap_or(false)
132}