gh-download 0.3.0

Download a file or directory from a GitHub repository path.
Documentation
use crate::cli::ResolvedOptions;
use crate::download::format_remote_path;
use crate::error::UserFacingError;
use crate::i18n::Language;
use std::path::Path;
use std::sync::{Arc, Mutex};

#[derive(Debug, Clone)]
pub struct Output {
    color: bool,
    language: Language,
    io_lock: Arc<Mutex<()>>,
}

impl Output {
    pub fn new(color: bool, language: Language) -> Self {
        Self {
            color,
            language,
            io_lock: Arc::new(Mutex::new(())),
        }
    }

    pub fn startup(&self, options: &ResolvedOptions) {
        self.with_io_lock(|| {
            self.print_separator();
            println!(
                "{}{}",
                self.paint("34", self.label_repository()),
                options.repo
            );
            println!(
                "{}{}",
                self.paint("32", self.label_ref()),
                options
                    .git_ref
                    .as_deref()
                    .unwrap_or(self.default_ref_label())
            );
            println!(
                "{}{}",
                self.paint("33", self.label_remote()),
                format_remote_path(&options.remote_path)
            );
            println!(
                "{}{}",
                self.paint("35", self.label_local()),
                options.local_target.display()
            );
            self.print_separator();
        });
    }

    pub fn found_directory(&self, count: usize, remote_path: &str) {
        self.print_stdout_line(&self.message_found_files(count, remote_path));
    }

    pub fn created_directory(&self, path: &Path) {
        self.with_io_lock(|| {
            println!(
                "{}{}",
                self.paint("34", self.label_created_directory()),
                path.display()
            );
            self.print_separator();
        });
    }

    pub fn downloading(&self, path: &str) {
        self.print_stdout_line(&format!(
            "{}{}",
            self.paint("34", self.label_downloading()),
            path
        ));
    }

    pub fn warning(&self, message: &str) {
        self.print_stdout_line(&format!("{} {}", self.paint("33", ""), message));
    }

    pub fn success(&self, message: &str) {
        self.print_stdout_line(&format!("{} {}", self.paint("32", ""), message));
    }

    pub fn completion(
        &self,
        repo: &str,
        remote_path: &str,
        saved_path: &Path,
        files_downloaded: usize,
        skipped_entries: usize,
    ) {
        self.with_io_lock(|| {
            self.print_separator();
            println!("{}", self.message_completion(repo, remote_path, saved_path));
            if files_downloaded > 1 || skipped_entries > 0 {
                println!(
                    "{}",
                    self.message_download_stats(files_downloaded, skipped_entries)
                );
            }
        });
    }

    pub fn print_user_error(&self, error: &UserFacingError) {
        self.with_io_lock(|| {
            eprintln!("{}", self.paint("31", &error.title));
            eprintln!("{} {}", self.reason_label(), error.reason);
            if !error.suggestions.is_empty() {
                eprintln!("{}", self.suggestions_label());
                for suggestion in &error.suggestions {
                    eprintln!("- {}", suggestion);
                }
            }
        });
    }

    pub fn debug_line(&self, message: &str) {
        self.with_io_lock(|| eprintln!("{}", message));
    }

    fn label_repository(&self) -> &'static str {
        if self.language.is_chinese() {
            "📦 仓库:"
        } else {
            "📦 Repository:"
        }
    }

    fn label_ref(&self) -> &'static str {
        if self.language.is_chinese() {
            "🌿 分支:"
        } else {
            "🌿 Ref:"
        }
    }

    fn default_ref_label(&self) -> &'static str {
        if self.language.is_chinese() {
            "默认分支"
        } else {
            "default branch"
        }
    }

    fn label_remote(&self) -> &'static str {
        if self.language.is_chinese() {
            "📂 远端路径:"
        } else {
            "📂 Remote Path:"
        }
    }

    fn label_local(&self) -> &'static str {
        if self.language.is_chinese() {
            "💾 本地路径:"
        } else {
            "💾 Local Path:"
        }
    }

    fn label_created_directory(&self) -> &'static str {
        if self.language.is_chinese() {
            "📁 创建本地目录:"
        } else {
            "📁 Created Local Directory:"
        }
    }

    fn label_downloading(&self) -> &'static str {
        if self.language.is_chinese() {
            "⬇️ 下载:"
        } else {
            "⬇️ Download:"
        }
    }

    fn reason_label(&self) -> &'static str {
        if self.language.is_chinese() {
            "原因:"
        } else {
            "Reason:"
        }
    }

    fn suggestions_label(&self) -> &'static str {
        if self.language.is_chinese() {
            "建议:"
        } else {
            "Suggestions:"
        }
    }

    fn message_found_files(&self, count: usize, remote_path: &str) -> String {
        if self.language.is_chinese() {
            format!(
                "{} 发现 {} 个文件,目录:{}",
                self.paint("33", "🔎"),
                count,
                format_remote_path(remote_path)
            )
        } else {
            format!(
                "{} Found {} files in directory: {}",
                self.paint("33", "🔎"),
                count,
                format_remote_path(remote_path)
            )
        }
    }

    fn message_completion(&self, repo: &str, remote_path: &str, saved_path: &Path) -> String {
        if self.language.is_chinese() {
            format!(
                "{} 完成:{}{} 已保存到 {}",
                self.paint("32", ""),
                repo,
                format_remote_path(remote_path),
                saved_path.display()
            )
        } else {
            format!(
                "{} Done: {} {} saved to {}",
                self.paint("32", ""),
                repo,
                format_remote_path(remote_path),
                saved_path.display()
            )
        }
    }

    fn message_download_stats(&self, files_downloaded: usize, skipped_entries: usize) -> String {
        if self.language.is_chinese() {
            format!(
                "共下载 {} 个文件,跳过 {} 个条目",
                files_downloaded, skipped_entries
            )
        } else {
            format!(
                "Downloaded {} files, skipped {} entries",
                files_downloaded, skipped_entries
            )
        }
    }

    fn print_separator(&self) {
        println!(
            "{}",
            self.paint("90", "-------------------------------------")
        );
    }

    fn print_stdout_line(&self, message: &str) {
        self.with_io_lock(|| println!("{}", message));
    }

    fn with_io_lock<T>(&self, action: impl FnOnce() -> T) -> T {
        let _guard = self.io_lock.lock().expect("output lock");
        action()
    }

    fn paint(&self, code: &str, text: &str) -> String {
        if self.color {
            format!("\u{1b}[{}m{}\u{1b}[0m", code, text)
        } else {
            text.to_string()
        }
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::*;

    #[test]
    fn chinese_found_directory_mentions_count_and_remote_path() {
        let output = Output::new(false, Language::Zh);

        assert_eq!(
            output.message_found_files(11, "skills/baoyu-translate"),
            "🔎 发现 11 个文件,目录:skills/baoyu-translate"
        );
    }

    #[test]
    fn chinese_completion_mentions_repo_remote_path_and_saved_path() {
        let output = Output::new(false, Language::Zh);

        assert_eq!(
            output.message_completion(
                "jimliu/baoyu-skills",
                "skills/baoyu-translate",
                Path::new("/tmp/baoyu-translate")
            ),
            "✅ 完成:jimliu/baoyu-skills 的 skills/baoyu-translate 已保存到 /tmp/baoyu-translate"
        );
    }
}