opendev_tui/controllers/
approval.rs1use tokio::sync::oneshot;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ApprovalDecision {
16 pub approved: bool,
18 pub choice: String,
20 pub command: String,
22}
23
24#[derive(Debug, Clone)]
26pub struct ApprovalOption {
27 pub choice: String,
29 pub label: String,
31 pub description: String,
33 pub approved: bool,
35}
36
37pub struct ApprovalController {
39 active: bool,
40 options: Vec<ApprovalOption>,
41 selected_index: usize,
42 command: String,
43 working_dir: String,
44 response_tx: Option<oneshot::Sender<ApprovalDecision>>,
45}
46
47impl ApprovalController {
48 pub fn new() -> Self {
50 Self {
51 active: false,
52 options: Vec::new(),
53 selected_index: 0,
54 command: String::new(),
55 working_dir: String::from("."),
56 response_tx: None,
57 }
58 }
59
60 pub fn active(&self) -> bool {
62 self.active
63 }
64
65 pub fn command(&self) -> &str {
67 &self.command
68 }
69
70 pub fn working_dir(&self) -> &str {
72 &self.working_dir
73 }
74
75 pub fn options(&self) -> &[ApprovalOption] {
77 &self.options
78 }
79
80 pub fn selected_index(&self) -> usize {
82 self.selected_index
83 }
84
85 pub fn start(
89 &mut self,
90 command: String,
91 working_dir: String,
92 ) -> oneshot::Receiver<ApprovalDecision> {
93 let base_prefix = command.split_whitespace().next().unwrap_or("").to_string();
94
95 let auto_desc = if !base_prefix.is_empty() {
96 format!(
97 "Automatically approve commands starting with '{}' in {}.",
98 base_prefix, working_dir
99 )
100 } else {
101 format!("Automatically approve future commands in {}.", working_dir)
102 };
103
104 self.options = vec![
105 ApprovalOption {
106 choice: "1".into(),
107 label: "Yes".into(),
108 description: "Run this command now.".into(),
109 approved: true,
110 },
111 ApprovalOption {
112 choice: "2".into(),
113 label: "Yes, and don't ask again".into(),
114 description: auto_desc,
115 approved: true,
116 },
117 ApprovalOption {
118 choice: "3".into(),
119 label: "No".into(),
120 description: "Cancel and adjust your request.".into(),
121 approved: false,
122 },
123 ];
124
125 self.command = command;
126 self.working_dir = working_dir;
127 self.selected_index = 0;
128 self.active = true;
129
130 let (tx, rx) = oneshot::channel();
131 self.response_tx = Some(tx);
132 rx
133 }
134
135 pub fn move_selection(&mut self, delta: i32) {
137 if !self.active || self.options.is_empty() {
138 return;
139 }
140 let len = self.options.len() as i32;
141 let new_idx = ((self.selected_index as i32) + delta).rem_euclid(len);
142 self.selected_index = new_idx as usize;
143 }
144
145 pub fn confirm(&mut self) {
147 if !self.active {
148 return;
149 }
150
151 let option = &self.options[self.selected_index];
152 let decision = ApprovalDecision {
153 approved: option.approved,
154 choice: option.choice.clone(),
155 command: self.command.clone(),
156 };
157
158 if let Some(tx) = self.response_tx.take() {
159 let _ = tx.send(decision);
160 }
161
162 self.cleanup();
163 }
164
165 pub fn cancel(&mut self) {
167 if !self.active || self.options.is_empty() {
168 return;
169 }
170 self.selected_index = self.options.len() - 1;
172 self.confirm();
173 }
174
175 fn cleanup(&mut self) {
177 self.active = false;
178 self.options.clear();
179 self.selected_index = 0;
180 self.command.clear();
181 self.response_tx = None;
182 }
183}
184
185impl Default for ApprovalController {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191#[cfg(test)]
192#[path = "approval_tests.rs"]
193mod tests;