kotoba_cli/
utils.rs

1//! ユーティリティ関数
2//!
3//! Merkle DAG: cli_interface -> ProgressBar component
4
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8/// ファイルの存在チェック
9pub fn file_exists(path: &Path) -> bool {
10    path.exists() && path.is_file()
11}
12
13/// ディレクトリの存在チェック
14pub fn dir_exists(path: &Path) -> bool {
15    path.exists() && path.is_dir()
16}
17
18/// ファイルの拡張子を取得
19pub fn get_file_extension(path: &Path) -> Option<String> {
20    path.extension()
21        .and_then(|ext| ext.to_str())
22        .map(|ext| ext.to_string())
23}
24
25/// ファイルサイズを取得
26pub fn get_file_size(path: &Path) -> Result<u64, Box<dyn std::error::Error>> {
27    let metadata = std::fs::metadata(path)?;
28    Ok(metadata.len())
29}
30
31/// ディレクトリ内のファイルを再帰的に検索
32pub fn find_files(dir: &Path, extension: Option<&str>) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
33    let mut files = Vec::new();
34
35    fn visit_dir(dir: &Path, extension: Option<&str>, files: &mut Vec<PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
36        if dir.is_dir() {
37            for entry in std::fs::read_dir(dir)? {
38                let entry = entry?;
39                let path = entry.path();
40
41                if path.is_dir() {
42                    visit_dir(&path, extension, files)?;
43                } else if let Some(ext) = extension {
44                    if let Some(file_ext) = get_file_extension(&path) {
45                        if file_ext == ext {
46                            files.push(path);
47                        }
48                    }
49                } else {
50                    files.push(path);
51                }
52            }
53        }
54        Ok(())
55    }
56
57    visit_dir(dir, extension, &mut files)?;
58    Ok(files)
59}
60
61/// コマンドを実行
62pub fn execute_command(command: &str, args: &[&str], cwd: Option<&Path>) -> Result<String, Box<dyn std::error::Error>> {
63    let mut cmd = Command::new(command);
64    cmd.args(args);
65
66    if let Some(dir) = cwd {
67        cmd.current_dir(dir);
68    }
69
70    let output = cmd.output()?;
71
72    if output.status.success() {
73        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
74        Ok(stdout)
75    } else {
76        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
77        Err(format!("Command failed: {}", stderr).into())
78    }
79}
80
81/// プロセスが実行中かどうかをチェック
82pub fn is_process_running(pid: u32) -> bool {
83    // Unix系システムでの実装
84    #[cfg(unix)]
85    {
86        use std::process::Command;
87        Command::new("kill")
88            .args(&["-0", &pid.to_string()])
89            .output()
90            .map(|output| output.status.success())
91            .unwrap_or(false)
92    }
93
94    // Windowsでの実装
95    #[cfg(windows)]
96    {
97        use std::process::Command;
98        Command::new("tasklist")
99            .args(&["/FI", &format!("PID eq {}", pid)])
100            .output()
101            .map(|output| {
102                let stdout = String::from_utf8_lossy(&output.stdout);
103                stdout.contains(&pid.to_string())
104            })
105            .unwrap_or(false)
106    }
107
108    // その他のプラットフォーム
109    #[cfg(not(any(unix, windows)))]
110    {
111        false
112    }
113}
114
115/// 利用可能なポートを見つける
116pub fn find_available_port(start_port: u16) -> Option<u16> {
117    use std::net::TcpListener;
118
119    for port in start_port..65535 {
120        if TcpListener::bind(("127.0.0.1", port)).is_ok() {
121            return Some(port);
122        }
123    }
124    None
125}
126
127/// バージョン比較
128pub fn compare_versions(version1: &str, version2: &str) -> std::cmp::Ordering {
129    let v1_parts: Vec<&str> = version1.split('.').collect();
130    let v2_parts: Vec<&str> = version2.split('.').collect();
131
132    for (v1, v2) in v1_parts.iter().zip(v2_parts.iter()) {
133        let v1_num = v1.parse::<u32>().unwrap_or(0);
134        let v2_num = v2.parse::<u32>().unwrap_or(0);
135
136        match v1_num.cmp(&v2_num) {
137            std::cmp::Ordering::Equal => continue,
138            other => return other,
139        }
140    }
141
142    v1_parts.len().cmp(&v2_parts.len())
143}
144
145/// バイト数を人間が読みやすい形式に変換
146pub fn format_bytes(bytes: u64) -> String {
147    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
148
149    if bytes == 0 {
150        return "0 B".to_string();
151    }
152
153    let base = 1024_f64;
154    let log = (bytes as f64).log(base).floor() as usize;
155    let unit_index = log.min(UNITS.len() - 1);
156    let value = bytes as f64 / base.powi(unit_index as i32);
157
158    format!("{:.1} {}", value, UNITS[unit_index])
159}
160
161/// 時間を人間が読みやすい形式に変換
162pub fn format_duration(duration: std::time::Duration) -> String {
163    let total_seconds = duration.as_secs();
164
165    if total_seconds < 60 {
166        format!("{}s", total_seconds)
167    } else if total_seconds < 3600 {
168        let minutes = total_seconds / 60;
169        let seconds = total_seconds % 60;
170        format!("{}m {}s", minutes, seconds)
171    } else {
172        let hours = total_seconds / 3600;
173        let minutes = (total_seconds % 3600) / 60;
174        format!("{}h {}m", hours, minutes)
175    }
176}
177
178/// 文字列をキャメルケースに変換
179pub fn to_camel_case(s: &str) -> String {
180    let mut result = String::new();
181    let mut capitalize_next = false;
182
183    for (i, ch) in s.chars().enumerate() {
184        if ch == '_' || ch == '-' {
185            capitalize_next = true;
186        } else if capitalize_next || i == 0 {
187            result.extend(ch.to_uppercase());
188            capitalize_next = false;
189        } else {
190            result.extend(ch.to_lowercase());
191        }
192    }
193
194    result
195}
196
197/// 文字列をスネークケースに変換
198pub fn to_snake_case(s: &str) -> String {
199    let mut result = String::new();
200
201    for (i, ch) in s.chars().enumerate() {
202        if ch.is_uppercase() && i > 0 {
203            result.push('_');
204        }
205        result.extend(ch.to_lowercase());
206    }
207
208    result
209}
210
211/// 環境変数を取得(デフォルト値付き)
212pub fn get_env_var(key: &str, default: &str) -> String {
213    std::env::var(key).unwrap_or_else(|_| default.to_string())
214}
215
216/// 一時ファイルを作成
217pub fn create_temp_file(prefix: &str, suffix: &str) -> Result<std::fs::File, Box<dyn std::error::Error>> {
218    use std::fs::File;
219    use std::io::Write;
220
221    let temp_path = std::env::temp_dir().join(format!("{}{}", prefix, suffix));
222    let file = File::create(&temp_path)?;
223    Ok(file)
224}
225
226/// プラットフォーム固有の改行文字を取得
227pub fn get_line_ending() -> &'static str {
228    if cfg!(windows) {
229        "\r\n"
230    } else {
231        "\n"
232    }
233}
234
235/// 現在のプラットフォーム名を取得
236pub fn get_platform_name() -> &'static str {
237    if cfg!(windows) {
238        "windows"
239    } else if cfg!(macos) {
240        "macos"
241    } else if cfg!(linux) {
242        "linux"
243    } else {
244        "unknown"
245    }
246}
247
248/// プログレスバー表示
249/// Merkle DAG: cli_interface -> ProgressBar component
250pub struct ProgressBar {
251    total: usize,
252    current: usize,
253    width: usize,
254    title: String,
255}
256
257impl ProgressBar {
258    /// 新しいプログレスバーを作成
259    pub fn new(total: usize, title: impl Into<String>) -> Self {
260        Self {
261            total,
262            current: 0,
263            width: 50,
264            title: title.into(),
265        }
266    }
267
268    /// プログレスバーを更新
269    pub fn update(&mut self, current: usize) {
270        self.current = current.min(self.total);
271        self.display();
272    }
273
274    /// プログレスバーをインクリメント
275    pub fn inc(&mut self) {
276        self.update(self.current + 1);
277    }
278
279    /// プログレスバーを完了状態にする
280    pub fn finish(&mut self) {
281        self.update(self.total);
282        println!(); // 改行
283    }
284
285    /// プログレスバーを表示
286    fn display(&self) {
287        let percentage = if self.total > 0 {
288            (self.current as f64 / self.total as f64 * 100.0) as usize
289        } else {
290            100
291        };
292
293        let filled = (self.current as f64 / self.total as f64 * self.width as f64) as usize;
294        let filled = filled.min(self.width);
295
296        let bar = "█".repeat(filled) + &"░".repeat(self.width - filled);
297
298        print!("\r{} [{:<width$}] {}/{} ({}%)",
299               self.title,
300               bar,
301               self.current,
302               self.total,
303               percentage,
304               width = self.width
305        );
306        std::io::Write::flush(&mut std::io::stdout()).ok();
307    }
308}
309
310impl Drop for ProgressBar {
311    fn drop(&mut self) {
312        println!(); // ドロップ時に改行
313    }
314}