1use crate::client::ZinitHandle;
10use crate::rhai::ZinitEngine;
11use colored::Colorize;
12use rustyline::completion::{Completer, Pair};
13use rustyline::error::ReadlineError;
14use rustyline::highlight::Highlighter;
15use rustyline::hint::{Hinter, HistoryHinter};
16use rustyline::history::DefaultHistory;
17use rustyline::validate::Validator;
18use rustyline::{Config, Context, Editor, Helper};
19use std::borrow::Cow;
20
21const ZINIT_FUNCTIONS: &[(&str, &str, &str)] = &[
23 ("zinit_ping()", "map", "Check if zinit is responding"),
25 ("zinit_shutdown()", "void", "Shutdown zinit server"),
26 (
28 "new_service(name)",
29 "ServiceBuilder",
30 "Create a new service builder",
31 ),
32 (
33 "register(builder)",
34 "void",
35 "Register service without starting",
36 ),
37 (
38 "start(builder, secs)",
39 "void",
40 "Register, start, and wait for service",
41 ),
42 ("zinit_start(name)", "void", "Start a service"),
43 ("zinit_stop(name)", "void", "Stop a service"),
44 ("zinit_restart(name)", "void", "Restart a service"),
45 (
46 "zinit_delete(name)",
47 "void",
48 "Stop, kill if needed, and remove service",
49 ),
50 ("zinit_kill(name, signal)", "void", "Send signal to service"),
51 ("zinit_list()", "array", "List all service names"),
53 ("zinit_status(name)", "map", "Get service status"),
54 ("zinit_stats(name)", "map", "Get CPU/memory stats"),
55 (
56 "zinit_is_running(name)",
57 "bool",
58 "Check if service is running",
59 ),
60 ("zinit_start_all()", "void", "Start all services"),
62 ("zinit_stop_all()", "void", "Stop all services"),
63 ("zinit_logs()", "array", "Get all logs from ring buffer"),
65 (
66 "zinit_logs_filter(service)",
67 "array",
68 "Get logs for a service",
69 ),
70 ("print(msg)", "void", "Print message"),
72 ("sleep(secs)", "void", "Sleep for seconds"),
73 ("sleep_ms(ms)", "void", "Sleep for milliseconds"),
74 ("get_env(key)", "string|unit", "Get environment variable"),
75 ("set_env(key, value)", "void", "Set environment variable"),
76 ("file_exists(path)", "bool", "Check if path exists"),
77 ("is_dir(path)", "bool", "Check if path is directory"),
78 ("is_file(path)", "bool", "Check if path is file"),
79 ("check_tcp(addr)", "bool", "Check if TCP port is open"),
80 ("check_http(url)", "bool", "Check if HTTP endpoint is up"),
81 ("kill_port(port)", "int", "Kill process on TCP port"),
82];
83
84const BUILDER_METHODS: &[(&str, &str)] = &[
86 (".name(name)", "Service name (required)"),
87 (".exec(cmd)", "Command to run (required)"),
88 (".test(cmd)", "Health check command"),
89 (".oneshot(bool)", "Run once, don't restart"),
90 (".shutdown_timeout(secs)", "Seconds before SIGKILL"),
91 (".after(service)", "Start after this service"),
92 (".signal_stop(signal)", "Signal to send on stop"),
93 (".log(mode)", "\"ring\", \"stdout\", or \"none\""),
94 (".env(key, value)", "Set environment variable"),
95 (".dir(path)", "Working directory"),
96 (".test_cmd(cmd)", "Command-based health check"),
97 (".test_tcp(addr)", "TCP port health check"),
98 (".test_http(url)", "HTTP endpoint health check"),
99 (".tcp_kill()", "Kill processes on port before starting"),
100 (".register()", "Register the service"),
101 (".wait(secs)", "Wait for service to be running"),
102];
103
104const REPL_COMMANDS: &[(&str, &str)] = &[
106 ("/help", "Show this help"),
107 ("/functions", "List all zinit functions"),
108 ("/builder", "Show service builder methods"),
109 ("/load <file>", "Load and execute a .rhai script"),
110 ("/tui", "Enter full-screen interactive TUI mode"),
111 ("/clear", "Clear screen"),
112 ("/quit", "Exit REPL (or Ctrl+D)"),
113];
114
115const RHAI_KEYWORDS: &[&str] = &[
117 "let", "const", "if", "else", "while", "loop", "for", "in", "break", "continue", "return",
118 "throw", "try", "catch", "fn", "private", "import", "export", "as", "true", "false", "null",
119];
120
121#[derive(Helper)]
123struct ReplHelper {
124 hinter: HistoryHinter,
125}
126
127impl Completer for ReplHelper {
128 type Candidate = Pair;
129
130 fn complete(
131 &self,
132 line: &str,
133 pos: usize,
134 _ctx: &Context<'_>,
135 ) -> rustyline::Result<(usize, Vec<Pair>)> {
136 let mut completions = Vec::new();
137
138 let line_to_cursor = &line[..pos];
140 let word_start = line_to_cursor
141 .rfind(|c: char| !c.is_alphanumeric() && c != '_')
142 .map(|i| i + 1)
143 .unwrap_or(0);
144 let word = &line_to_cursor[word_start..];
145
146 if word.is_empty() {
147 return Ok((pos, completions));
148 }
149
150 if word.starts_with('/') {
152 for (cmd, desc) in REPL_COMMANDS {
153 let cmd_name = cmd.split_whitespace().next().unwrap_or(cmd);
155 if cmd_name.starts_with(word) {
156 completions.push(Pair {
157 display: format!("{} - {}", cmd, desc),
158 replacement: cmd_name.to_string(),
159 });
160 }
161 }
162 return Ok((word_start, completions));
163 }
164
165 if word.starts_with('.') || line_to_cursor.ends_with('.') {
167 let method_word = if word.starts_with('.') { word } else { "." };
168 for (method, desc) in BUILDER_METHODS {
169 if method.starts_with(method_word) {
170 let replacement = method.split('(').next().unwrap_or(method);
171 completions.push(Pair {
172 display: format!("{} - {}", method, desc),
173 replacement: replacement.to_string(),
174 });
175 }
176 }
177 let start = if word.starts_with('.') {
178 word_start
179 } else {
180 pos
181 };
182 return Ok((start, completions));
183 }
184
185 for (func, ret, desc) in ZINIT_FUNCTIONS {
187 let func_name = func.split('(').next().unwrap_or(func);
188 if func_name.starts_with(word) {
189 completions.push(Pair {
190 display: format!("{} -> {} - {}", func, ret, desc),
191 replacement: func_name.to_string(),
192 });
193 }
194 }
195
196 for kw in RHAI_KEYWORDS {
198 if kw.starts_with(word) && !completions.iter().any(|p| p.replacement == *kw) {
199 completions.push(Pair {
200 display: format!("{} (keyword)", kw),
201 replacement: kw.to_string(),
202 });
203 }
204 }
205
206 Ok((word_start, completions))
207 }
208}
209
210impl Hinter for ReplHelper {
211 type Hint = String;
212
213 fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
214 if let Some(hint) = self.hinter.hint(line, pos, ctx) {
216 return Some(hint);
217 }
218
219 let line_to_cursor = &line[..pos];
221
222 for (func, ret, desc) in ZINIT_FUNCTIONS {
224 let func_name = func.split('(').next().unwrap_or(func);
225 if line_to_cursor.ends_with(func_name) {
226 let signature = &func[func_name.len()..];
227 return Some(
228 format!("{} -> {} | {}", signature, ret, desc)
229 .dimmed()
230 .to_string(),
231 );
232 }
233 }
234
235 None
236 }
237}
238
239impl Highlighter for ReplHelper {
240 fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
241 let mut result = String::with_capacity(line.len() * 2);
242 let mut chars = line.chars().peekable();
243 let mut current_word = String::new();
244
245 while let Some(c) = chars.next() {
246 if c.is_alphanumeric() || c == '_' {
247 current_word.push(c);
248 } else {
249 if !current_word.is_empty() {
250 result.push_str(&highlight_word(¤t_word));
251 current_word.clear();
252 }
253 if c == '"' {
255 result.push_str(&format!("{}", "\"".green()));
256 let mut string_content = String::new();
257 while let Some(&next) = chars.peek() {
258 chars.next();
259 if next == '"' {
260 result.push_str(&format!("{}", string_content.green()));
261 result.push_str(&format!("{}", "\"".green()));
262 break;
263 } else if next == '\\' {
264 string_content.push(next);
265 if let Some(escaped) = chars.next() {
266 string_content.push(escaped);
267 }
268 } else {
269 string_content.push(next);
270 }
271 }
272 }
273 else if c == '/' && chars.peek() == Some(&'/') {
275 result.push_str(&format!(
276 "{}",
277 format!("//{}", chars.collect::<String>()).dimmed()
278 ));
279 break;
280 }
281 else if "=+-*/<>!&|".contains(c) {
283 result.push_str(&format!("{}", c.to_string().yellow()));
284 }
285 else if "()[]{}".contains(c) {
287 result.push_str(&format!("{}", c.to_string().cyan()));
288 } else {
289 result.push(c);
290 }
291 }
292 }
293
294 if !current_word.is_empty() {
295 result.push_str(&highlight_word(¤t_word));
296 }
297
298 Cow::Owned(result)
299 }
300
301 fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
302 &'s self,
303 prompt: &'p str,
304 _default: bool,
305 ) -> Cow<'b, str> {
306 Cow::Owned(format!("{}", prompt.cyan().bold()))
307 }
308
309 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
310 Cow::Owned(format!("{}", hint.dimmed()))
311 }
312
313 fn highlight_char(
314 &self,
315 _line: &str,
316 _pos: usize,
317 _forced: rustyline::highlight::CmdKind,
318 ) -> bool {
319 true
320 }
321}
322
323fn highlight_word(word: &str) -> String {
324 if RHAI_KEYWORDS.contains(&word) {
326 return format!("{}", word.magenta().bold());
327 }
328
329 if word.starts_with("zinit_") {
331 return format!("{}", word.blue().bold());
332 }
333
334 if [
336 "print",
337 "sleep",
338 "sleep_ms",
339 "get_env",
340 "set_env",
341 "file_exists",
342 "is_dir",
343 "is_file",
344 "check_tcp",
345 "check_http",
346 "kill_port",
347 ]
348 .contains(&word)
349 {
350 return format!("{}", word.blue());
351 }
352
353 if word.chars().all(|c| c.is_numeric() || c == '.') {
355 return format!("{}", word.yellow());
356 }
357
358 if word == "true" || word == "false" {
360 return format!("{}", word.yellow().bold());
361 }
362
363 word.to_string()
364}
365
366impl Validator for ReplHelper {}
367
368fn print_banner() {
370 println!(
371 "{}",
372 "╔═══════════════════════════════════════════════════════════╗".cyan()
373 );
374 println!(
375 "{}",
376 "║ Zinit Interactive Shell ║".cyan()
377 );
378 println!(
379 "{}",
380 "╠═══════════════════════════════════════════════════════════╣".cyan()
381 );
382 println!(
383 "{} {}",
384 "║".cyan(),
385 format!(
386 "{:<56} {}",
387 "Tab: completion | Up/Down: history | Ctrl+D: exit", "║"
388 )
389 .cyan()
390 );
391 println!(
392 "{} {}",
393 "║".cyan(),
394 format!(
395 "{:<56} {}",
396 "Type /help for commands, /functions for API", "║"
397 )
398 .cyan()
399 );
400 println!(
401 "{}",
402 "╚═══════════════════════════════════════════════════════════╝".cyan()
403 );
404 println!();
405}
406
407fn print_help() {
409 println!("{}", "REPL Commands:".yellow().bold());
410 for (cmd, desc) in REPL_COMMANDS {
411 println!(" {:15} {}", cmd.cyan(), desc);
412 }
413 println!();
414 println!("{}", "Tips:".yellow().bold());
415 println!(" - Press {} to autocomplete function names", "Tab".cyan());
416 println!(" - Use {} arrows for command history", "Up/Down".cyan());
417 println!(
418 " - Multi-line input: end lines with {} or {}",
419 "\\".cyan(),
420 "{".cyan()
421 );
422 println!(" - Scripts execute when you press Enter on complete statement",);
423}
424
425fn print_functions() {
427 println!("{}", "Zinit Functions:".yellow().bold());
428 println!();
429
430 println!(" {}", "Control:".green().bold());
431 for (func, ret, desc) in ZINIT_FUNCTIONS.iter().take(2) {
432 println!(
433 " {:40} {} {:15} {}",
434 func.blue(),
435 "->".dimmed(),
436 ret,
437 desc.dimmed()
438 );
439 }
440
441 println!();
442 println!(" {}", "Service Management:".green().bold());
443 for (func, ret, desc) in ZINIT_FUNCTIONS.iter().skip(2).take(7) {
444 println!(
445 " {:40} {} {:15} {}",
446 func.blue(),
447 "->".dimmed(),
448 ret,
449 desc.dimmed()
450 );
451 }
452
453 println!();
454 println!(" {}", "Query:".green().bold());
455 for (func, ret, desc) in ZINIT_FUNCTIONS.iter().skip(9).take(4) {
456 println!(
457 " {:40} {} {:15} {}",
458 func.blue(),
459 "->".dimmed(),
460 ret,
461 desc.dimmed()
462 );
463 }
464
465 println!();
466 println!(" {}", "Batch:".green().bold());
467 for (func, ret, desc) in ZINIT_FUNCTIONS.iter().skip(13).take(2) {
468 println!(
469 " {:40} {} {:15} {}",
470 func.blue(),
471 "->".dimmed(),
472 ret,
473 desc.dimmed()
474 );
475 }
476
477 println!();
478 println!(" {}", "Logging:".green().bold());
479 for (func, ret, desc) in ZINIT_FUNCTIONS.iter().skip(15).take(2) {
480 println!(
481 " {:40} {} {:15} {}",
482 func.blue(),
483 "->".dimmed(),
484 ret,
485 desc.dimmed()
486 );
487 }
488
489 println!();
490 println!(" {}", "Utilities:".green().bold());
491 for (func, ret, desc) in ZINIT_FUNCTIONS.iter().skip(17) {
492 println!(
493 " {:40} {} {:15} {}",
494 func.blue(),
495 "->".dimmed(),
496 ret,
497 desc.dimmed()
498 );
499 }
500}
501
502fn print_builder() {
504 println!("{}", "Service Builder Pattern:".yellow().bold());
505 println!();
506 println!(" let svc = {}()", "zinit_service_new".blue());
507 for (method, desc) in BUILDER_METHODS {
508 println!(" {:30} // {}", method.cyan(), desc.dimmed());
509 }
510 println!();
511 println!(" Example:");
512 println!(" let svc = {}()", "zinit_service_new".blue());
513 println!(" {}", ".name(\"myservice\")".cyan());
514 println!(" {}", ".exec(\"python -m http.server 8080\")".cyan());
515 println!(" {}", ".test_tcp(\"127.0.0.1:8080\")".cyan());
516 println!(" {}", ".register();".cyan());
517 println!(" {}", "svc.wait(30);".cyan());
518}
519
520fn load_script(file_path: &str, engine: &ZinitEngine) -> anyhow::Result<()> {
522 let expanded_path = if file_path.starts_with("~/") {
524 dirs::home_dir()
525 .map(|h| h.join(&file_path[2..]))
526 .unwrap_or_else(|| std::path::PathBuf::from(file_path))
527 } else {
528 std::path::PathBuf::from(file_path)
529 };
530
531 if !expanded_path.exists() {
532 anyhow::bail!("File not found: {}", expanded_path.display());
533 }
534
535 println!("{} {}", "Loading:".green(), expanded_path.display());
536
537 match engine.run_file(&expanded_path) {
538 Ok(result) => {
539 if !result.is_unit() {
540 println!("{} {:?}", "=>".green(), result);
541 }
542 Ok(())
543 }
544 Err(e) => {
545 anyhow::bail!("{}", e)
546 }
547 }
548}
549
550pub fn run_repl(handle: ZinitHandle) -> anyhow::Result<()> {
552 print_banner();
553
554 match handle.ping() {
556 Ok(resp) => {
557 println!(
558 "{} {} v{}",
559 "Connected to zinit server:".green(),
560 resp.message,
561 resp.version
562 );
563 }
564 Err(e) => {
565 println!(
566 "{}: {}",
567 "Warning: Could not connect to zinit server".yellow(),
568 e
569 );
570 println!("Make sure the server is running with: zinit server start");
571 }
572 }
573 println!();
574
575 let engine = ZinitEngine::with_handle(handle.clone());
577
578 let config = Config::builder()
579 .history_ignore_space(true)
580 .completion_type(rustyline::CompletionType::List)
581 .edit_mode(rustyline::EditMode::Emacs)
582 .build();
583
584 let helper = ReplHelper {
585 hinter: HistoryHinter::new(),
586 };
587
588 let mut rl: Editor<ReplHelper, DefaultHistory> = Editor::with_config(config)?;
589 rl.set_helper(Some(helper));
590
591 let history_path = dirs::home_dir()
593 .map(|h| h.join(".zinit_history"))
594 .unwrap_or_else(|| std::path::PathBuf::from(".zinit_history"));
595 let _ = rl.load_history(&history_path);
596
597 let mut multiline_buffer = String::new();
598
599 loop {
600 let prompt = if multiline_buffer.is_empty() {
601 "zinit> "
602 } else {
603 " ... "
604 };
605
606 match rl.readline(prompt) {
607 Ok(line) => {
608 let line = line.trim();
609
610 if multiline_buffer.is_empty() {
612 match line {
613 "/help" => {
614 print_help();
615 continue;
616 }
617 "/functions" => {
618 print_functions();
619 continue;
620 }
621 "/builder" => {
622 print_builder();
623 continue;
624 }
625 "/clear" => {
626 print!("\x1B[2J\x1B[1;1H");
627 print_banner();
628 continue;
629 }
630 "/tui" => {
631 match crate::tui::run_tui(handle.clone()) {
633 Ok(_) => {
634 print_banner();
636 }
637 Err(e) => {
638 println!("{}: {}", "TUI Error".red().bold(), e);
639 }
640 }
641 continue;
642 }
643 "/quit" | "/exit" | "/q" => {
644 println!("{}", "Goodbye!".cyan());
645 break;
646 }
647 "" => continue,
648 _ if line.starts_with("/load ") => {
649 let file_path = line.strip_prefix("/load ").unwrap().trim();
650 match load_script(file_path, &engine) {
651 Ok(_) => {}
652 Err(e) => println!("{}: {}", "Error".red().bold(), e),
653 }
654 println!();
655 continue;
656 }
657 _ if line.starts_with('/') => {
658 println!(
659 "{}: Unknown command '{}'. Type /help for commands.",
660 "Error".red().bold(),
661 line
662 );
663 continue;
664 }
665 _ => {}
666 }
667 }
668
669 multiline_buffer.push_str(line);
671 multiline_buffer.push('\n');
672
673 let trimmed = line.trim_end();
675 if trimmed.ends_with('\\') {
676 multiline_buffer = multiline_buffer
678 .trim_end()
679 .strip_suffix('\\')
680 .unwrap_or(&multiline_buffer)
681 .to_string();
682 multiline_buffer.push('\n');
683 continue;
684 }
685
686 let open_braces = multiline_buffer.matches('{').count();
688 let close_braces = multiline_buffer.matches('}').count();
689 let open_brackets = multiline_buffer.matches('[').count();
690 let close_brackets = multiline_buffer.matches(']').count();
691 let open_parens = multiline_buffer.matches('(').count();
692 let close_parens = multiline_buffer.matches(')').count();
693
694 if open_braces > close_braces
695 || open_brackets > close_brackets
696 || open_parens > close_parens
697 {
698 continue;
699 }
700
701 let script = std::mem::take(&mut multiline_buffer);
703 let script = script.trim();
704
705 if script.is_empty() {
706 continue;
707 }
708
709 let _ = rl.add_history_entry(script);
711
712 match engine.run(script) {
714 Ok(result) => {
715 if !result.is_unit() {
716 println!("{} {:?}", "=>".green(), result);
717 }
718 }
719 Err(e) => {
720 println!("{}: {}", "Error".red().bold(), e);
721 }
722 }
723 println!();
724 }
725 Err(ReadlineError::Interrupted) => {
726 if !multiline_buffer.is_empty() {
728 multiline_buffer.clear();
729 println!("{}", "^C (input cleared)".dimmed());
730 } else {
731 println!("{}", "^C (use /quit or Ctrl+D to exit)".dimmed());
732 }
733 }
734 Err(ReadlineError::Eof) => {
735 println!("{}", "Goodbye!".cyan());
737 break;
738 }
739 Err(err) => {
740 println!("{}: {:?}", "Error".red(), err);
741 break;
742 }
743 }
744 }
745
746 let _ = rl.save_history(&history_path);
748
749 Ok(())
750}