Skip to main content

sz_rust_cli/
interactive.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4//! 交互式命令行补全
5//!
6//! 对应 design.md 第 2.2.2.3 节,使用 dialoguer 提供 TTY 环境下的交互式输入。
7
8use std::io::IsTerminal;
9
10use crate::error::CliError;
11
12/// 交互式提示器
13pub struct InteractivePrompt;
14
15impl InteractivePrompt {
16    /// 检测当前是否为 TTY 环境
17    ///
18    /// 使用 `std::io::IsTerminal` trait(Rust 1.81+)
19    pub fn is_tty() -> bool {
20        std::io::stdin().is_terminal()
21    }
22
23    /// 提示输入字段定义(格式 `name:Type,...`)
24    pub fn prompt_fields() -> Result<String, CliError> {
25        if !Self::is_tty() {
26            return Err(CliError::Generic(
27                "not a TTY environment, cannot prompt for fields".to_string(),
28            ));
29        }
30
31        let input = dialoguer::Input::<String>::new()
32            .with_prompt("Enter field definitions (format: name:Type,name2:Type2,...)")
33            .with_initial_text("id:i32:pk,name:String,age:i32")
34            .interact_text();
35
36        match input {
37            Ok(value) => {
38                if value.trim().is_empty() {
39                    Err(CliError::Generic(
40                        "user entered empty field definition".to_string(),
41                    ))
42                } else {
43                    Ok(value.trim().to_string())
44                }
45            }
46            Err(e) => Err(CliError::Generic(format!("user cancelled input: {e}"))),
47        }
48    }
49
50    /// 提示输入表名,带默认值
51    pub fn prompt_table(default: &str) -> Result<String, CliError> {
52        if !Self::is_tty() {
53            return Err(CliError::Generic(
54                "not a TTY environment, cannot prompt for table name".to_string(),
55            ));
56        }
57
58        let input = dialoguer::Input::<String>::new()
59            .with_prompt("Enter table name")
60            .with_initial_text(default)
61            .interact_text();
62
63        match input {
64            Ok(value) => {
65                let value = value.trim().to_string();
66                if value.is_empty() {
67                    Ok(default.to_string())
68                } else {
69                    Ok(value)
70                }
71            }
72            Err(e) => Err(CliError::Generic(format!("user cancelled input: {e}"))),
73        }
74    }
75
76    /// 让用户从可用模板列表中选择
77    pub fn prompt_template(available: &[String]) -> Result<String, CliError> {
78        if available.is_empty() {
79            return Err(CliError::Generic("no templates available".to_string()));
80        }
81
82        if !Self::is_tty() {
83            return Err(CliError::Generic(
84                "not a TTY environment, cannot prompt for template selection".to_string(),
85            ));
86        }
87
88        let select = dialoguer::FuzzySelect::new()
89            .with_prompt("Select template type")
90            .items(available)
91            .interact();
92
93        match select {
94            Ok(idx) => Ok(available[idx].clone()),
95            Err(e) => Err(CliError::Generic(format!("user cancelled selection: {e}"))),
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_is_tty_returns_bool() {
106        let result = InteractivePrompt::is_tty();
107        assert!(result || !result); // 布尔恒真:验证 is_tty 返回 bool 而非 panic
108    }
109
110    #[test]
111    fn test_prompt_fields_non_tty() {
112        if !InteractivePrompt::is_tty() {
113            let result = InteractivePrompt::prompt_fields();
114            assert!(result.is_err());
115            let err = result.unwrap_err();
116            assert!(err.to_string().contains("not a TTY"));
117        }
118    }
119
120    #[test]
121    fn test_prompt_table_non_tty() {
122        if !InteractivePrompt::is_tty() {
123            let result = InteractivePrompt::prompt_table("users");
124            assert!(result.is_err());
125            let err = result.unwrap_err();
126            assert!(err.to_string().contains("not a TTY"));
127        }
128    }
129
130    #[test]
131    fn test_prompt_template_non_tty() {
132        if !InteractivePrompt::is_tty() {
133            let result = InteractivePrompt::prompt_template(&[
134                "crud".to_string(),
135                "master-slave".to_string(),
136            ]);
137            assert!(result.is_err());
138            let err = result.unwrap_err();
139            assert!(err.to_string().contains("not a TTY"));
140        }
141    }
142
143    #[test]
144    fn test_prompt_template_empty_list() {
145        let result = InteractivePrompt::prompt_template(&[]);
146        assert!(result.is_err());
147        let err = result.unwrap_err();
148        assert!(err.to_string().contains("no templates"));
149    }
150}