fix(repl): plain readline prompt (fix garbled interactive line editing)

The prompt passed to rustyline embedded ANSI escapes AND a newline (dim context
line + colored `neurosploit›`), so rustyline mis-measured the prompt width and
cursor position — typing/backspace/history/cursor got garbled in a real
terminal (fine when piped, which has no line editor).

Now: the dim context line is printed with println!() ABOVE the prompt, the
readline prompt is plain "neurosploit› " (correct width), and the magenta color
is applied via Highlighter::highlight_prompt (display-only, doesn't affect width).
This commit is contained in:
CyberSecurityUP
2026-07-01 23:00:15 -03:00
parent 2e25809a93
commit 78b638a956
+25 -5
View File
@@ -170,7 +170,22 @@ fn complete_path(prefix: &str) -> Vec<Pair> {
}
impl Hinter for NsHelper { type Hint = String; }
impl Highlighter for NsHelper {}
impl Highlighter for NsHelper {
// Color the prompt for display only. rustyline measures the ORIGINAL (plain)
// prompt for cursor width, so adding ANSI here does NOT break line editing —
// unlike embedding escapes in the prompt string passed to readline().
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
_default: bool,
) -> std::borrow::Cow<'b, str> {
if prompt.trim_start().starts_with("neurosploit") {
std::borrow::Cow::Owned(format!("\x1b[35m{prompt}\x1b[0m"))
} else {
std::borrow::Cow::Borrowed(prompt)
}
}
}
impl Validator for NsHelper {
fn validate(&self, ctx: &mut ValidationContext<'_>) -> rustyline::Result<ValidationResult> {
if ctx.input().ends_with('\\') {
@@ -341,7 +356,8 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
show(&s);
loop {
let Some(line) = reader.read(&context_prompt(&s)) else { println!("\n bye."); break };
println!("{}", context_prompt(&s)); // dim context line above the prompt
let Some(line) = reader.read(PROMPT) else { println!("\n bye."); break };
let line = line.trim();
if line.is_empty() {
continue;
@@ -1214,11 +1230,15 @@ fn context_prompt(s: &Session) -> String {
};
let tgt = s.target.clone().or_else(|| s.repo.clone()).unwrap_or_default();
let tgt = if tgt.is_empty() { String::new() } else { format!("{}", tgt.replace("https://", "").replace("http://", "")) };
format!(
"\x1b[2m{model} {auth} · {cwd} · {mode}{tgt}\x1b[0m\n\x1b[35mneurosploit\x1b[0m "
)
// Dim context line, printed ABOVE the prompt (not part of the readline prompt,
// so its ANSI/newline never corrupts rustyline's cursor math).
format!("\x1b[2m{model} {auth} · {cwd} · {mode}{tgt}\x1b[0m")
}
/// The actual readline prompt — plain text so rustyline measures its width
/// correctly; color is applied by the Highlighter, not embedded here.
const PROMPT: &str = "neurosploit ";
fn onoff(b: bool) -> &'static str { if b { "on" } else { "off" } }
fn trunc(s: &str, n: usize) -> String {
if s.chars().count() <= n { s.to_string() }