grok-cli-acp 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +42 -0
- package/.github/workflows/ci.yml +30 -0
- package/.github/workflows/rust.yml +22 -0
- package/.grok/.env.example +85 -0
- package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
- package/.grok/ENV_CONFIG_GUIDE.md +173 -0
- package/.grok/QUICK_REFERENCE.md +180 -0
- package/.grok/README.md +104 -0
- package/.grok/TESTING_GUIDE.md +393 -0
- package/CHANGELOG.md +465 -0
- package/CODE_REVIEW_SUMMARY.md +414 -0
- package/COMPLETE_FIX_SUMMARY.md +415 -0
- package/CONFIGURATION.md +489 -0
- package/CONTEXT_FILES_GUIDE.md +419 -0
- package/CONTRIBUTING.md +55 -0
- package/CURSOR_POSITION_FIX.md +206 -0
- package/Cargo.toml +88 -0
- package/ERROR_HANDLING_REPORT.md +361 -0
- package/FINAL_FIX_SUMMARY.md +462 -0
- package/FIXES.md +37 -0
- package/FIXES_SUMMARY.md +87 -0
- package/GROK_API_MIGRATION_SUMMARY.md +111 -0
- package/LICENSE +22 -0
- package/MIGRATION_TO_GROK_API.md +223 -0
- package/README.md +504 -0
- package/REVIEW_COMPLETE.md +416 -0
- package/REVIEW_QUICK_REFERENCE.md +173 -0
- package/SECURITY.md +463 -0
- package/SECURITY_AUDIT.md +661 -0
- package/SETUP.md +287 -0
- package/TESTING_TOOLS.md +88 -0
- package/TESTING_TOOL_EXECUTION.md +239 -0
- package/TOOL_EXECUTION_FIX.md +491 -0
- package/VERIFICATION_CHECKLIST.md +419 -0
- package/docs/API.md +74 -0
- package/docs/CHAT_LOGGING.md +39 -0
- package/docs/CURSOR_FIX_DEMO.md +306 -0
- package/docs/ERROR_HANDLING_GUIDE.md +547 -0
- package/docs/FILE_OPERATIONS.md +449 -0
- package/docs/INTERACTIVE.md +401 -0
- package/docs/PROJECT_CREATION_GUIDE.md +570 -0
- package/docs/QUICKSTART.md +378 -0
- package/docs/QUICK_REFERENCE.md +691 -0
- package/docs/RELEASE_NOTES_0.1.2.md +240 -0
- package/docs/TOOLS.md +459 -0
- package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
- package/docs/ZED_INTEGRATION.md +371 -0
- package/docs/extensions.md +464 -0
- package/docs/settings.md +293 -0
- package/examples/extensions/logging-hook/README.md +91 -0
- package/examples/extensions/logging-hook/extension.json +22 -0
- package/package.json +30 -0
- package/scripts/test_acp.py +252 -0
- package/scripts/test_acp.sh +143 -0
- package/scripts/test_acp_simple.sh +72 -0
- package/src/acp/mod.rs +741 -0
- package/src/acp/protocol.rs +323 -0
- package/src/acp/security.rs +298 -0
- package/src/acp/tools.rs +697 -0
- package/src/bin/banner_demo.rs +216 -0
- package/src/bin/docgen.rs +18 -0
- package/src/bin/installer.rs +217 -0
- package/src/cli/app.rs +310 -0
- package/src/cli/commands/acp.rs +721 -0
- package/src/cli/commands/chat.rs +485 -0
- package/src/cli/commands/code.rs +513 -0
- package/src/cli/commands/config.rs +394 -0
- package/src/cli/commands/health.rs +442 -0
- package/src/cli/commands/history.rs +421 -0
- package/src/cli/commands/mod.rs +14 -0
- package/src/cli/commands/settings.rs +1384 -0
- package/src/cli/mod.rs +166 -0
- package/src/config/mod.rs +2212 -0
- package/src/display/ascii_art.rs +139 -0
- package/src/display/banner.rs +289 -0
- package/src/display/components/input.rs +323 -0
- package/src/display/components/mod.rs +2 -0
- package/src/display/components/settings_list.rs +306 -0
- package/src/display/interactive.rs +1255 -0
- package/src/display/mod.rs +62 -0
- package/src/display/terminal.rs +42 -0
- package/src/display/tips.rs +316 -0
- package/src/grok_client_ext.rs +177 -0
- package/src/hooks/loader.rs +407 -0
- package/src/hooks/mod.rs +158 -0
- package/src/lib.rs +174 -0
- package/src/main.rs +65 -0
- package/src/mcp/client.rs +195 -0
- package/src/mcp/config.rs +20 -0
- package/src/mcp/mod.rs +6 -0
- package/src/mcp/protocol.rs +67 -0
- package/src/utils/auth.rs +41 -0
- package/src/utils/chat_logger.rs +568 -0
- package/src/utils/context.rs +390 -0
- package/src/utils/mod.rs +16 -0
- package/src/utils/network.rs +320 -0
- package/src/utils/rate_limiter.rs +166 -0
- package/src/utils/session.rs +73 -0
- package/src/utils/shell_permissions.rs +389 -0
- package/src/utils/telemetry.rs +41 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use crossterm::cursor::{self, MoveTo, MoveToColumn, MoveToNextLine, MoveToPreviousLine};
|
|
3
|
+
use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|
4
|
+
use crossterm::style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor};
|
|
5
|
+
use crossterm::terminal::{self, Clear, ClearType};
|
|
6
|
+
use crossterm::{ExecutableCommand, QueueableCommand};
|
|
7
|
+
use regex::Regex;
|
|
8
|
+
use std::io::{stdout, Write};
|
|
9
|
+
|
|
10
|
+
pub struct Suggestion {
|
|
11
|
+
pub text: String,
|
|
12
|
+
pub description: String,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn strip_ansi(s: &str) -> String {
|
|
16
|
+
let re = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
|
|
17
|
+
re.replace_all(s, "").to_string()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn read_input_with_suggestions(prompt: &str, suggestions: &[Suggestion]) -> Result<String> {
|
|
21
|
+
let mut buffer = String::new();
|
|
22
|
+
let mut cursor_pos = 0;
|
|
23
|
+
let mut suggestion_index: Option<usize> = None;
|
|
24
|
+
let mut scroll_offset = 0;
|
|
25
|
+
let mut horizontal_scroll = 0; // For scrolling long input text
|
|
26
|
+
let mut stdout = stdout();
|
|
27
|
+
|
|
28
|
+
// Box drawing characters
|
|
29
|
+
let box_top_left = '╭';
|
|
30
|
+
let box_top_right = '╮';
|
|
31
|
+
let box_bottom_left = '╰';
|
|
32
|
+
let box_bottom_right = '╯';
|
|
33
|
+
let box_horizontal = '─';
|
|
34
|
+
let box_vertical = '│';
|
|
35
|
+
|
|
36
|
+
terminal::enable_raw_mode()?;
|
|
37
|
+
|
|
38
|
+
let mut is_first_render = true;
|
|
39
|
+
|
|
40
|
+
loop {
|
|
41
|
+
// 0. Preparation
|
|
42
|
+
let (cols, _) = terminal::size()?;
|
|
43
|
+
let cols = cols as usize;
|
|
44
|
+
|
|
45
|
+
// Calculate visual widths
|
|
46
|
+
let prompt_stripped = strip_ansi(prompt);
|
|
47
|
+
let prompt_width = prompt_stripped.chars().count();
|
|
48
|
+
// Box content: "│ " + prompt + buffer + " │"
|
|
49
|
+
// Inner width needed: 1 (space) + prompt + buffer + 1 (space)
|
|
50
|
+
// But we want the box to extend to the right reasonably
|
|
51
|
+
let content_width = prompt_width + buffer.len() + 2;
|
|
52
|
+
let min_box_width = 60;
|
|
53
|
+
let box_width = (content_width + 2) // +2 for borders
|
|
54
|
+
.max(min_box_width)
|
|
55
|
+
.min(cols);
|
|
56
|
+
|
|
57
|
+
// 1. Clear Previous Frame
|
|
58
|
+
if !is_first_render {
|
|
59
|
+
// We assume cursor is at the input line (middle of box)
|
|
60
|
+
stdout.execute(MoveToPreviousLine(1))?;
|
|
61
|
+
stdout.execute(MoveToColumn(0))?;
|
|
62
|
+
stdout.execute(Clear(ClearType::FromCursorDown))?;
|
|
63
|
+
}
|
|
64
|
+
is_first_render = false;
|
|
65
|
+
|
|
66
|
+
// Capture start position (Top of box)
|
|
67
|
+
let (_, start_row) = cursor::position()?;
|
|
68
|
+
|
|
69
|
+
// 2. Render Box
|
|
70
|
+
// Top Border
|
|
71
|
+
let top_border_len = box_width.saturating_sub(2);
|
|
72
|
+
let top_border = format!(
|
|
73
|
+
"{}{}{}",
|
|
74
|
+
box_top_left,
|
|
75
|
+
box_horizontal.to_string().repeat(top_border_len),
|
|
76
|
+
box_top_right
|
|
77
|
+
);
|
|
78
|
+
stdout.execute(Print(top_border))?;
|
|
79
|
+
stdout.execute(MoveToNextLine(1))?;
|
|
80
|
+
stdout.execute(MoveToColumn(0))?;
|
|
81
|
+
|
|
82
|
+
// Middle Line (Prompt + Input)
|
|
83
|
+
// Calculate available space for input text
|
|
84
|
+
let available_width = box_width
|
|
85
|
+
.saturating_sub(4) // 2 for borders + 2 for spaces around content
|
|
86
|
+
.saturating_sub(prompt_width);
|
|
87
|
+
|
|
88
|
+
// Calculate horizontal scroll to keep cursor visible
|
|
89
|
+
if cursor_pos < horizontal_scroll {
|
|
90
|
+
horizontal_scroll = cursor_pos;
|
|
91
|
+
} else if cursor_pos >= horizontal_scroll + available_width {
|
|
92
|
+
horizontal_scroll = cursor_pos.saturating_sub(available_width) + 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get the visible portion of the buffer
|
|
96
|
+
let visible_buffer: String = buffer
|
|
97
|
+
.chars()
|
|
98
|
+
.skip(horizontal_scroll)
|
|
99
|
+
.take(available_width)
|
|
100
|
+
.collect();
|
|
101
|
+
|
|
102
|
+
stdout.execute(Print(format!("{} ", box_vertical)))?;
|
|
103
|
+
stdout.execute(Print(prompt))?;
|
|
104
|
+
stdout.execute(Print(&visible_buffer))?;
|
|
105
|
+
|
|
106
|
+
// Fill remaining space with spaces
|
|
107
|
+
let current_inner_len = 1 + prompt_width + visible_buffer.len(); // " " + prompt + visible_buffer
|
|
108
|
+
let remaining_space = box_width
|
|
109
|
+
.saturating_sub(2)
|
|
110
|
+
.saturating_sub(current_inner_len);
|
|
111
|
+
if remaining_space > 0 {
|
|
112
|
+
stdout.execute(Print(" ".repeat(remaining_space)))?;
|
|
113
|
+
}
|
|
114
|
+
stdout.execute(Print(format!("{}", box_vertical)))?;
|
|
115
|
+
|
|
116
|
+
stdout.execute(MoveToNextLine(1))?;
|
|
117
|
+
stdout.execute(MoveToColumn(0))?;
|
|
118
|
+
|
|
119
|
+
// Bottom Border
|
|
120
|
+
let bottom_border = format!(
|
|
121
|
+
"{}{}{}",
|
|
122
|
+
box_bottom_left,
|
|
123
|
+
box_horizontal.to_string().repeat(top_border_len),
|
|
124
|
+
box_bottom_right
|
|
125
|
+
);
|
|
126
|
+
stdout.execute(Print(bottom_border))?;
|
|
127
|
+
stdout.execute(MoveToNextLine(1))?;
|
|
128
|
+
stdout.execute(MoveToColumn(0))?;
|
|
129
|
+
|
|
130
|
+
// 3. Render Suggestions
|
|
131
|
+
let filtered_suggestions: Vec<&Suggestion> = if let Some(search) = buffer.strip_prefix('/')
|
|
132
|
+
{
|
|
133
|
+
suggestions
|
|
134
|
+
.iter()
|
|
135
|
+
.filter(|s| s.text.starts_with('/') && s.text[1..].starts_with(search))
|
|
136
|
+
.collect()
|
|
137
|
+
} else {
|
|
138
|
+
Vec::new()
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if !filtered_suggestions.is_empty() {
|
|
142
|
+
// Update selection index wrap-around
|
|
143
|
+
if let Some(idx) = suggestion_index {
|
|
144
|
+
if idx >= filtered_suggestions.len() {
|
|
145
|
+
suggestion_index = Some(0);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
suggestion_index = Some(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let idx = suggestion_index.unwrap();
|
|
152
|
+
let list_height = 8; // Max visible suggestions
|
|
153
|
+
|
|
154
|
+
// Adjust scroll
|
|
155
|
+
if idx < scroll_offset {
|
|
156
|
+
scroll_offset = idx;
|
|
157
|
+
} else if idx >= scroll_offset + list_height {
|
|
158
|
+
scroll_offset = idx - list_height + 1;
|
|
159
|
+
}
|
|
160
|
+
// Ensure scroll is valid
|
|
161
|
+
if scroll_offset + list_height > filtered_suggestions.len() {
|
|
162
|
+
scroll_offset = filtered_suggestions.len().saturating_sub(list_height);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Render Up Arrow
|
|
166
|
+
if scroll_offset > 0 {
|
|
167
|
+
stdout.queue(MoveToColumn(2))?;
|
|
168
|
+
stdout.queue(SetForegroundColor(Color::DarkGrey))?;
|
|
169
|
+
stdout.queue(Print("▲"))?;
|
|
170
|
+
stdout.queue(ResetColor)?;
|
|
171
|
+
stdout.queue(MoveToNextLine(1))?;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Render Items
|
|
175
|
+
let _end_index = (scroll_offset + list_height).min(filtered_suggestions.len());
|
|
176
|
+
for (i, suggestion) in filtered_suggestions
|
|
177
|
+
.iter()
|
|
178
|
+
.enumerate()
|
|
179
|
+
.skip(scroll_offset)
|
|
180
|
+
.take(list_height)
|
|
181
|
+
{
|
|
182
|
+
stdout.queue(MoveToColumn(2))?; // Indent
|
|
183
|
+
if Some(i) == suggestion_index {
|
|
184
|
+
stdout.queue(SetForegroundColor(Color::Black))?;
|
|
185
|
+
stdout.queue(SetBackgroundColor(Color::White))?;
|
|
186
|
+
} else {
|
|
187
|
+
stdout.queue(SetForegroundColor(Color::Cyan))?;
|
|
188
|
+
stdout.queue(SetBackgroundColor(Color::Reset))?;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let text = format!("{} - {}", suggestion.text, suggestion.description);
|
|
192
|
+
// Truncate if too long (simple check)
|
|
193
|
+
let max_text_len = cols.saturating_sub(4);
|
|
194
|
+
let text_preview = if text.len() > max_text_len {
|
|
195
|
+
format!("{}...", &text[..max_text_len.saturating_sub(3)])
|
|
196
|
+
} else {
|
|
197
|
+
text
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
stdout.queue(Print(text_preview))?;
|
|
201
|
+
stdout.queue(ResetColor)?;
|
|
202
|
+
stdout.queue(MoveToNextLine(1))?;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Render Down Arrow
|
|
206
|
+
if scroll_offset + list_height < filtered_suggestions.len() {
|
|
207
|
+
stdout.queue(MoveToColumn(2))?;
|
|
208
|
+
stdout.queue(SetForegroundColor(Color::DarkGrey))?;
|
|
209
|
+
stdout.queue(Print("▼"))?;
|
|
210
|
+
stdout.queue(ResetColor)?;
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
suggestion_index = None;
|
|
214
|
+
scroll_offset = 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 4. Position Cursor
|
|
218
|
+
// We want cursor at Middle Line of box.
|
|
219
|
+
// Start row was Top Line.
|
|
220
|
+
// Middle Line is start_row + 1.
|
|
221
|
+
let cursor_row = start_row + 1;
|
|
222
|
+
// Col: "│ " (2 chars) + prompt_width + (cursor_pos - horizontal_scroll)
|
|
223
|
+
let visible_cursor_pos = cursor_pos.saturating_sub(horizontal_scroll);
|
|
224
|
+
let cursor_col = 2 + prompt_width + visible_cursor_pos;
|
|
225
|
+
|
|
226
|
+
stdout.execute(MoveTo(cursor_col as u16, cursor_row))?;
|
|
227
|
+
stdout.flush()?;
|
|
228
|
+
|
|
229
|
+
// 5. Handle Input
|
|
230
|
+
if let Event::Key(KeyEvent {
|
|
231
|
+
code,
|
|
232
|
+
modifiers,
|
|
233
|
+
kind,
|
|
234
|
+
..
|
|
235
|
+
}) = read()?
|
|
236
|
+
&& kind == KeyEventKind::Press {
|
|
237
|
+
match code {
|
|
238
|
+
KeyCode::Enter => {
|
|
239
|
+
if let Some(idx) = suggestion_index
|
|
240
|
+
&& !filtered_suggestions.is_empty() {
|
|
241
|
+
buffer = filtered_suggestions[idx].text.clone();
|
|
242
|
+
// Select and break
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
if !buffer.is_empty() {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
KeyCode::Char(c) => {
|
|
250
|
+
if modifiers == KeyModifiers::CONTROL && c == 'c' {
|
|
251
|
+
buffer.clear();
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
if cursor_pos < buffer.len() {
|
|
255
|
+
buffer.insert(cursor_pos, c);
|
|
256
|
+
} else {
|
|
257
|
+
buffer.push(c);
|
|
258
|
+
}
|
|
259
|
+
cursor_pos += 1;
|
|
260
|
+
}
|
|
261
|
+
KeyCode::Backspace => {
|
|
262
|
+
if cursor_pos > 0 {
|
|
263
|
+
buffer.remove(cursor_pos - 1);
|
|
264
|
+
cursor_pos -= 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
KeyCode::Left => {
|
|
268
|
+
cursor_pos = cursor_pos.saturating_sub(1);
|
|
269
|
+
}
|
|
270
|
+
KeyCode::Right => {
|
|
271
|
+
if cursor_pos < buffer.len() {
|
|
272
|
+
cursor_pos += 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
KeyCode::Up => {
|
|
276
|
+
if let Some(idx) = suggestion_index {
|
|
277
|
+
if idx > 0 {
|
|
278
|
+
suggestion_index = Some(idx - 1);
|
|
279
|
+
}
|
|
280
|
+
} else if !filtered_suggestions.is_empty() {
|
|
281
|
+
// If no selection but list exists, select last?
|
|
282
|
+
suggestion_index = Some(filtered_suggestions.len() - 1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
KeyCode::Down => {
|
|
286
|
+
if let Some(idx) = suggestion_index {
|
|
287
|
+
if idx < filtered_suggestions.len().saturating_sub(1) {
|
|
288
|
+
suggestion_index = Some(idx + 1);
|
|
289
|
+
}
|
|
290
|
+
} else if !filtered_suggestions.is_empty() {
|
|
291
|
+
suggestion_index = Some(0);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
KeyCode::Tab => {
|
|
295
|
+
if let Some(idx) = suggestion_index
|
|
296
|
+
&& !filtered_suggestions.is_empty() {
|
|
297
|
+
buffer = filtered_suggestions[idx].text.clone();
|
|
298
|
+
cursor_pos = buffer.len();
|
|
299
|
+
// Don't break, just fill
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
KeyCode::Esc => {
|
|
303
|
+
suggestion_index = None;
|
|
304
|
+
}
|
|
305
|
+
_ => {}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Cleanup
|
|
311
|
+
// Move to Top of box and clear down to leave a clean state
|
|
312
|
+
if !is_first_render {
|
|
313
|
+
stdout.execute(MoveToPreviousLine(1))?;
|
|
314
|
+
stdout.execute(MoveToColumn(0))?;
|
|
315
|
+
stdout.execute(Clear(ClearType::FromCursorDown))?;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Print final committed line (simple text, no box, for history)
|
|
319
|
+
stdout.execute(Print(format!("{}{}{}", prompt, buffer, '\n')))?;
|
|
320
|
+
|
|
321
|
+
terminal::disable_raw_mode()?;
|
|
322
|
+
Ok(buffer)
|
|
323
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
use crate::cli::commands::settings::{SettingDefinition, SettingType};
|
|
2
|
+
use crate::config::Config;
|
|
3
|
+
use crate::display::terminal::{init_tui, restore_tui, Tui};
|
|
4
|
+
use anyhow::Result;
|
|
5
|
+
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
|
8
|
+
style::{Color, Modifier, Style},
|
|
9
|
+
text::{Line, Span},
|
|
10
|
+
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
|
|
11
|
+
};
|
|
12
|
+
use std::time::Duration;
|
|
13
|
+
|
|
14
|
+
pub async fn run_settings_tui(config: &Config, settings: Vec<SettingDefinition>) -> Result<()> {
|
|
15
|
+
let mut terminal = init_tui()?;
|
|
16
|
+
let mut app = SettingsApp::new(config.clone(), settings);
|
|
17
|
+
let res = run_app(&mut terminal, &mut app).await;
|
|
18
|
+
restore_tui()?;
|
|
19
|
+
res
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[derive(PartialEq)]
|
|
23
|
+
enum EditMode {
|
|
24
|
+
View,
|
|
25
|
+
Input,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
struct SettingsApp {
|
|
29
|
+
config: Config,
|
|
30
|
+
settings: Vec<SettingDefinition>,
|
|
31
|
+
state: ListState,
|
|
32
|
+
should_quit: bool,
|
|
33
|
+
mode: EditMode,
|
|
34
|
+
input_buffer: String,
|
|
35
|
+
status_message: Option<(String, Color)>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl SettingsApp {
|
|
39
|
+
fn new(config: Config, settings: Vec<SettingDefinition>) -> Self {
|
|
40
|
+
let mut state = ListState::default();
|
|
41
|
+
if !settings.is_empty() {
|
|
42
|
+
state.select(Some(0));
|
|
43
|
+
}
|
|
44
|
+
Self {
|
|
45
|
+
config,
|
|
46
|
+
settings,
|
|
47
|
+
state,
|
|
48
|
+
should_quit: false,
|
|
49
|
+
mode: EditMode::View,
|
|
50
|
+
input_buffer: String::new(),
|
|
51
|
+
status_message: None,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn next(&mut self) {
|
|
56
|
+
let i = match self.state.selected() {
|
|
57
|
+
Some(i) => {
|
|
58
|
+
if i >= self.settings.len() - 1 {
|
|
59
|
+
0
|
|
60
|
+
} else {
|
|
61
|
+
i + 1
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
None => 0,
|
|
65
|
+
};
|
|
66
|
+
self.state.select(Some(i));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn previous(&mut self) {
|
|
70
|
+
let i = match self.state.selected() {
|
|
71
|
+
Some(i) => {
|
|
72
|
+
if i == 0 {
|
|
73
|
+
self.settings.len() - 1
|
|
74
|
+
} else {
|
|
75
|
+
i - 1
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
None => 0,
|
|
79
|
+
};
|
|
80
|
+
self.state.select(Some(i));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async fn handle_selection(&mut self) -> Result<()> {
|
|
84
|
+
if let Some(index) = self.state.selected() {
|
|
85
|
+
let setting = self.settings[index].clone();
|
|
86
|
+
|
|
87
|
+
match setting.setting_type {
|
|
88
|
+
SettingType::Boolean => {
|
|
89
|
+
let current_bool = setting.current_value == "true";
|
|
90
|
+
let new_value = (!current_bool).to_string();
|
|
91
|
+
self.update_setting(&setting.key, &new_value).await?;
|
|
92
|
+
}
|
|
93
|
+
SettingType::String | SettingType::Number => {
|
|
94
|
+
self.mode = EditMode::Input;
|
|
95
|
+
self.input_buffer = setting.current_value.clone();
|
|
96
|
+
}
|
|
97
|
+
_ => {
|
|
98
|
+
self.set_status("Editing this type is not supported in TUI", Color::Red);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async fn update_setting(&mut self, key: &str, value: &str) -> Result<()> {
|
|
106
|
+
let mut updated_config = self.config.clone();
|
|
107
|
+
if let Err(e) = updated_config.set_value(key, value) {
|
|
108
|
+
self.set_status(&format!("Error: {}", e), Color::Red);
|
|
109
|
+
return Ok(());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if let Err(e) = updated_config.validate() {
|
|
113
|
+
self.set_status(&format!("Validation Error: {}", e), Color::Red);
|
|
114
|
+
return Ok(());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if let Err(e) = updated_config.save(None).await {
|
|
118
|
+
self.set_status(&format!("Save Error: {}", e), Color::Red);
|
|
119
|
+
return Ok(());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
self.config = updated_config;
|
|
123
|
+
self.settings = crate::cli::commands::settings::get_all_settings(&self.config);
|
|
124
|
+
self.set_status("Setting updated successfully", Color::Green);
|
|
125
|
+
Ok(())
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn set_status(&mut self, msg: &str, color: Color) {
|
|
129
|
+
self.status_message = Some((msg.to_string(), color));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async fn submit_input(&mut self) -> Result<()> {
|
|
133
|
+
if let Some(index) = self.state.selected() {
|
|
134
|
+
let setting = self.settings[index].clone();
|
|
135
|
+
|
|
136
|
+
// Validate number if needed
|
|
137
|
+
if let SettingType::Number = setting.setting_type
|
|
138
|
+
&& self.input_buffer.parse::<f64>().is_err() {
|
|
139
|
+
self.set_status("Invalid number format", Color::Red);
|
|
140
|
+
return Ok(());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let value = self.input_buffer.clone();
|
|
144
|
+
self.update_setting(&setting.key, &value).await?;
|
|
145
|
+
}
|
|
146
|
+
self.mode = EditMode::View;
|
|
147
|
+
Ok(())
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async fn run_app(terminal: &mut Tui, app: &mut SettingsApp) -> Result<()> {
|
|
152
|
+
loop {
|
|
153
|
+
terminal.draw(|f| ui(f, app))?;
|
|
154
|
+
|
|
155
|
+
if event::poll(Duration::from_millis(100))?
|
|
156
|
+
&& let Event::Key(key) = event::read()?
|
|
157
|
+
&& key.kind == KeyEventKind::Press {
|
|
158
|
+
match app.mode {
|
|
159
|
+
EditMode::View => match key.code {
|
|
160
|
+
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
|
161
|
+
KeyCode::Down | KeyCode::Char('j') => app.next(),
|
|
162
|
+
KeyCode::Up | KeyCode::Char('k') => app.previous(),
|
|
163
|
+
KeyCode::Enter | KeyCode::Char(' ') => app.handle_selection().await?,
|
|
164
|
+
_ => {}
|
|
165
|
+
},
|
|
166
|
+
EditMode::Input => match key.code {
|
|
167
|
+
KeyCode::Enter => app.submit_input().await?,
|
|
168
|
+
KeyCode::Esc => {
|
|
169
|
+
app.mode = EditMode::View;
|
|
170
|
+
app.set_status("Cancelled", Color::Yellow);
|
|
171
|
+
}
|
|
172
|
+
KeyCode::Backspace => {
|
|
173
|
+
app.input_buffer.pop();
|
|
174
|
+
}
|
|
175
|
+
KeyCode::Char(c) => {
|
|
176
|
+
app.input_buffer.push(c);
|
|
177
|
+
}
|
|
178
|
+
_ => {}
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if app.should_quit {
|
|
183
|
+
return Ok(());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn ui(frame: &mut ratatui::Frame, app: &mut SettingsApp) {
|
|
189
|
+
let vertical = Layout::default()
|
|
190
|
+
.direction(Direction::Vertical)
|
|
191
|
+
.constraints([
|
|
192
|
+
Constraint::Length(3), // Header
|
|
193
|
+
Constraint::Min(0), // List
|
|
194
|
+
Constraint::Length(3), // Description
|
|
195
|
+
Constraint::Length(1), // Status/Footer
|
|
196
|
+
])
|
|
197
|
+
.split(frame.area());
|
|
198
|
+
|
|
199
|
+
// Header
|
|
200
|
+
let header = Paragraph::new("Grok CLI Settings (TUI)")
|
|
201
|
+
.style(
|
|
202
|
+
Style::default()
|
|
203
|
+
.fg(Color::Cyan)
|
|
204
|
+
.add_modifier(Modifier::BOLD),
|
|
205
|
+
)
|
|
206
|
+
.block(Block::default().borders(Borders::ALL));
|
|
207
|
+
frame.render_widget(header, vertical[0]);
|
|
208
|
+
|
|
209
|
+
// List
|
|
210
|
+
let items: Vec<ListItem> = app
|
|
211
|
+
.settings
|
|
212
|
+
.iter()
|
|
213
|
+
.map(|setting| {
|
|
214
|
+
let value_style = if setting.current_value == "true" {
|
|
215
|
+
Style::default().fg(Color::Green)
|
|
216
|
+
} else if setting.current_value == "false" {
|
|
217
|
+
Style::default().fg(Color::Red)
|
|
218
|
+
} else {
|
|
219
|
+
Style::default().fg(Color::Yellow)
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
let content = Line::from(vec![
|
|
223
|
+
Span::styled(
|
|
224
|
+
format!("{:<30}", setting.label),
|
|
225
|
+
Style::default().add_modifier(Modifier::BOLD),
|
|
226
|
+
),
|
|
227
|
+
Span::raw(" │ "),
|
|
228
|
+
Span::styled(setting.current_value.to_string(), value_style),
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
ListItem::new(content)
|
|
232
|
+
})
|
|
233
|
+
.collect();
|
|
234
|
+
|
|
235
|
+
let list = List::new(items)
|
|
236
|
+
.block(Block::default().borders(Borders::ALL).title("Settings"))
|
|
237
|
+
.highlight_style(
|
|
238
|
+
Style::default()
|
|
239
|
+
.bg(Color::DarkGray)
|
|
240
|
+
.add_modifier(Modifier::BOLD),
|
|
241
|
+
)
|
|
242
|
+
.highlight_symbol("> ");
|
|
243
|
+
|
|
244
|
+
frame.render_stateful_widget(list, vertical[1], &mut app.state);
|
|
245
|
+
|
|
246
|
+
// Description
|
|
247
|
+
let description = if let Some(index) = app.state.selected() {
|
|
248
|
+
&app.settings[index].description
|
|
249
|
+
} else {
|
|
250
|
+
"Select a setting to edit"
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
let desc_widget = Paragraph::new(description.to_string())
|
|
254
|
+
.style(Style::default().fg(Color::Gray))
|
|
255
|
+
.block(Block::default().borders(Borders::ALL).title("Description"));
|
|
256
|
+
frame.render_widget(desc_widget, vertical[2]);
|
|
257
|
+
|
|
258
|
+
// Status/Help
|
|
259
|
+
let status_text = if let Some((msg, color)) = &app.status_message {
|
|
260
|
+
Span::styled(msg, Style::default().fg(*color))
|
|
261
|
+
} else {
|
|
262
|
+
Span::styled(
|
|
263
|
+
"Press 'q' to quit, Enter to edit/toggle",
|
|
264
|
+
Style::default().fg(Color::DarkGray),
|
|
265
|
+
)
|
|
266
|
+
};
|
|
267
|
+
frame.render_widget(Paragraph::new(status_text), vertical[3]);
|
|
268
|
+
|
|
269
|
+
// Input Popup
|
|
270
|
+
if app.mode == EditMode::Input {
|
|
271
|
+
let block = Block::default()
|
|
272
|
+
.title("Edit Value")
|
|
273
|
+
.borders(Borders::ALL)
|
|
274
|
+
.style(Style::default().bg(Color::Blue));
|
|
275
|
+
|
|
276
|
+
let area = centered_rect(60, 20, frame.area());
|
|
277
|
+
frame.render_widget(Clear, area); // Clear background
|
|
278
|
+
|
|
279
|
+
let input = Paragraph::new(app.input_buffer.clone())
|
|
280
|
+
.style(Style::default().fg(Color::White))
|
|
281
|
+
.block(block);
|
|
282
|
+
|
|
283
|
+
frame.render_widget(input, area);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// Helper function to center a rect
|
|
288
|
+
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
|
289
|
+
let popup_layout = Layout::default()
|
|
290
|
+
.direction(Direction::Vertical)
|
|
291
|
+
.constraints([
|
|
292
|
+
Constraint::Percentage((100 - percent_y) / 2),
|
|
293
|
+
Constraint::Percentage(percent_y),
|
|
294
|
+
Constraint::Percentage((100 - percent_y) / 2),
|
|
295
|
+
])
|
|
296
|
+
.split(r);
|
|
297
|
+
|
|
298
|
+
Layout::default()
|
|
299
|
+
.direction(Direction::Horizontal)
|
|
300
|
+
.constraints([
|
|
301
|
+
Constraint::Percentage((100 - percent_x) / 2),
|
|
302
|
+
Constraint::Percentage(percent_x),
|
|
303
|
+
Constraint::Percentage((100 - percent_x) / 2),
|
|
304
|
+
])
|
|
305
|
+
.split(popup_layout[1])[1]
|
|
306
|
+
}
|