gitee_cli_rs/cmd/
interactive.rs1use std::io::Write;
2use std::process::Command;
3
4use inquire::Text;
5
6use crate::config::Config;
7use crate::error::{GiteeError, Result};
8
9pub fn should_run_interactive_create(title: Option<&str>, fill: bool) -> bool {
11 title.is_none() && !fill
12}
13
14pub fn missing_title_usage(subcommand: &str, mention_fill: bool) -> GiteeError {
15 if mention_fill {
16 GiteeError::Usage(format!("{subcommand} needs --title (or --fill)"))
17 } else {
18 GiteeError::Usage(format!("{subcommand} needs --title"))
19 }
20}
21
22pub fn resolve_editor_command(
24 visual: Option<&str>,
25 editor_env: Option<&str>,
26 config_editor: Option<&str>,
27) -> Option<String> {
28 visual
29 .filter(|s| !s.trim().is_empty())
30 .or_else(|| editor_env.filter(|s| !s.trim().is_empty()))
31 .or_else(|| config_editor.filter(|s| !s.trim().is_empty()))
32 .map(str::to_string)
33}
34
35pub fn prompt_title(default: Option<&str>) -> Result<String> {
36 let mut prompt = Text::new("Title");
37 if let Some(d) = default.filter(|s| !s.is_empty()) {
38 prompt = prompt.with_default(d);
39 }
40 let title = prompt
41 .prompt()
42 .map_err(|e| GiteeError::Usage(format!("prompt cancelled: {e}")))?;
43 let title = title.trim().to_string();
44 if title.is_empty() {
45 return Err(GiteeError::Usage("title is required".into()));
46 }
47 Ok(title)
48}
49
50pub fn edit_body_in_editor(initial: &str, editor_cmd: &str) -> Result<Option<String>> {
52 use shell_words::split;
53
54 let mut file = tempfile::NamedTempFile::new()?;
55 file.write_all(initial.as_bytes())?;
56 file.flush()?;
57 let path = file.path().to_owned();
58
59 let mut parts: Vec<String> = split(editor_cmd)
60 .map_err(|e| GiteeError::Usage(format!("invalid editor command: {e}")))?;
61 if parts.is_empty() {
62 return Err(GiteeError::Usage("editor command is empty".into()));
63 }
64 parts.push(path.to_string_lossy().into_owned());
65
66 let program = &parts[0];
67 let args = &parts[1..];
68 let status = Command::new(program)
69 .args(args)
70 .status()
71 .map_err(|e| GiteeError::Usage(format!("failed to run editor `{program}`: {e}")))?;
72 if !status.success() {
73 return Err(GiteeError::Usage("editor exited with an error".into()));
74 }
75
76 let body = std::fs::read_to_string(&path)?;
77 let trimmed = body.trim();
78 Ok((!trimmed.is_empty()).then(|| trimmed.to_string()))
79}
80
81pub fn resolve_editor_from_env_and_config() -> Result<String> {
82 let settings = Config::load_settings()?;
83 resolve_editor_command(
84 std::env::var("VISUAL").ok().as_deref(),
85 std::env::var("EDITOR").ok().as_deref(),
86 settings.editor.as_deref(),
87 )
88 .ok_or_else(|| {
89 GiteeError::Usage(
90 "no editor: set $VISUAL, $EDITOR, or `gitee config set editor <cmd>`".into(),
91 )
92 })
93}
94
95pub fn stdin_is_tty() -> bool {
96 use std::io::IsTerminal;
97 std::io::stdin().is_terminal()
98}
99
100pub fn resolve_optional_body(
104 body: Option<String>,
105 prefill: &str,
106 usage_cmd: &str,
107) -> Result<String> {
108 if let Some(b) = body {
109 return Ok(b);
110 }
111 if !stdin_is_tty() {
112 return Err(GiteeError::Usage(format!("{usage_cmd} needs --body/-m")));
113 }
114 let editor = resolve_editor_from_env_and_config()?;
115 edit_body_in_editor(prefill, &editor)?.ok_or_else(|| {
116 GiteeError::Usage("comment body is empty".into())
117 })
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn should_run_interactive_when_title_missing_and_not_fill() {
126 assert!(should_run_interactive_create(None, false));
127 assert!(!should_run_interactive_create(Some("t"), false));
128 assert!(!should_run_interactive_create(None, true));
129 assert!(!should_run_interactive_create(Some("t"), true));
130 }
131
132 #[test]
133 fn missing_title_usage_messages() {
134 assert_eq!(
135 missing_title_usage("issue create", false).to_string(),
136 "issue create needs --title"
137 );
138 assert_eq!(
139 missing_title_usage("pr create", true).to_string(),
140 "pr create needs --title (or --fill)"
141 );
142 }
143
144 #[test]
145 fn resolve_editor_prefers_visual_then_editor_then_config() {
146 assert_eq!(
147 resolve_editor_command(Some("visual"), Some("editor"), Some("config")),
148 Some("visual".into())
149 );
150 assert_eq!(
151 resolve_editor_command(None, Some("editor"), Some("config")),
152 Some("editor".into())
153 );
154 assert_eq!(
155 resolve_editor_command(None, None, Some("config")),
156 Some("config".into())
157 );
158 assert_eq!(resolve_editor_command(None, None, None), None);
159 assert_eq!(
160 resolve_editor_command(Some(""), Some("editor"), None),
161 Some("editor".into())
162 );
163 assert_eq!(
164 resolve_editor_command(Some(" "), None, Some("config")),
165 Some("config".into())
166 );
167 }
168}