use libedit_sys::*;
use std::ffi::{c_void, CStr, CString};
use std::os::raw::{c_char, c_uchar};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::Path;
use crate::completion::{longest_common_prefix, CandidateStyler, Completer, LineContext};
use crate::error::{Error, Result};
use crate::hint::Hinter;
use crate::history::{path_to_cstring, History};
use crate::shim;
use crate::suggestion::Suggester;
const MAX_USER_ACTIONS: usize = 8;
#[allow(clippy::unnecessary_cast)]
const PROMPT_ESC_DELIM: c_char = 0x01;
type ActionHandler = Box<dyn FnMut(&ActionContext) -> Action>;
struct Context {
prompt_wide: Vec<wchar_t>,
completer: Option<Box<dyn Completer>>,
hinter: Option<Box<dyn Hinter>>,
candidate_styler: Option<Box<dyn CandidateStyler>>,
list_buf: String,
out: *mut FILE,
in_fd: i32,
hint_wide: Vec<wchar_t>,
suggester: Option<Box<dyn Suggester>>,
suggest_prefix: String,
suggest_suffix: String,
prev_suggest_total: usize,
prompt_cols: usize,
suggest_buf: String,
actions: Vec<ActionHandler>,
auto_add_history: bool,
ignore_space: bool,
history_ptr: *mut libedit_sys::History,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Editor {
Emacs,
Vi,
}
pub struct EditLine {
inner: *mut libedit_sys::EditLine,
context: *mut Context,
streams: [*mut FILE; 3],
}
impl EditLine {
pub fn new(app_name: impl AsRef<str>) -> Result<Self> {
let name = CString::new(app_name.as_ref())?;
let stdin = unsafe { open_std_stream(0, c"r") };
let stdout = unsafe { open_std_stream(1, c"w") };
let stderr = unsafe { open_std_stream(2, c"w") };
let streams = [stdin, stdout, stderr];
if stdin.is_null() || stdout.is_null() || stderr.is_null() {
unsafe { close_streams(&streams) };
return Err(Error::Null);
}
let inner = unsafe { el_init(name.as_ptr(), stdin, stdout, stderr) };
if inner.is_null() {
unsafe { close_streams(&streams) };
return Err(Error::Null);
}
let in_fd = unsafe { libc::fileno(stdin as *mut libc::FILE) };
let context = Box::into_raw(Box::new(Context {
prompt_wide: vec![0], completer: None,
hinter: None,
candidate_styler: None,
list_buf: String::new(),
out: stdout,
in_fd,
hint_wide: vec![0], suggester: None,
suggest_prefix: String::new(),
suggest_suffix: String::new(),
prev_suggest_total: 0,
prompt_cols: 0,
suggest_buf: String::new(),
actions: Vec::new(),
auto_add_history: false,
ignore_space: false,
history_ptr: std::ptr::null_mut(),
}));
unsafe {
shim::el_set_clientdata(inner, context as *mut c_void);
shim::el_set_prompt_esc_fn(inner, prompt_trampoline, PROMPT_ESC_DELIM);
shim::el_set_rprompt_esc_fn(inner, rprompt_trampoline, PROMPT_ESC_DELIM);
}
Ok(EditLine {
inner,
context,
streams,
})
}
pub fn new_with_locale(app_name: impl AsRef<str>) -> Result<Self> {
use std::sync::Once;
static LOCALE_INIT: Once = Once::new();
LOCALE_INIT.call_once(|| unsafe {
libc::setlocale(libc::LC_CTYPE, c"".as_ptr());
});
Self::new(app_name)
}
pub fn readline(&mut self, prompt: impl AsRef<str>) -> Result<Option<String>> {
Ok(self
.readline_bytes(prompt)?
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned()))
}
pub fn readline_bytes(&mut self, prompt: impl AsRef<str>) -> Result<Option<Vec<u8>>> {
let prompt_cols = display_width(prompt.as_ref());
unsafe {
(*self.context).prompt_wide = prompt_to_wide(prompt.as_ref());
(*self.context).prompt_cols = prompt_cols;
(*self.context).prev_suggest_total = 0;
}
let mut count: i32 = 0;
let mut err: i32 = 0;
let line_ptr = unsafe { shim::el_gets_err(self.inner, &mut count, &mut err) };
if line_ptr.is_null() || count < 0 {
if err == libc::EINTR {
return Err(Error::Interrupted);
}
return Ok(None); }
if count == 0 {
return Ok(Some(Vec::new()));
}
let cstr = unsafe { CStr::from_ptr(line_ptr) };
let mut bytes = cstr.to_bytes().to_vec();
if bytes.last() == Some(&b'\n') {
bytes.pop();
}
self.maybe_auto_add_history(&bytes);
Ok(Some(bytes))
}
fn maybe_auto_add_history(&self, bytes: &[u8]) {
let ctx = unsafe { &*self.context };
if !ctx.auto_add_history || ctx.history_ptr.is_null() || bytes.is_empty() {
return;
}
if ctx.ignore_space && bytes.first() == Some(&b' ') {
return;
}
if let Ok(cstr) = CString::new(bytes) {
unsafe { shim::history_enter(ctx.history_ptr, &cstr) };
}
}
pub fn set_history(&mut self, history: &mut History) -> Result<()> {
let ret = unsafe { shim::el_set_hist(self.inner, history.as_mut_ptr()) };
if ret != 0 {
return Err(Error::operation(EL_HIST as i32, ret));
}
unsafe {
(*self.context).history_ptr = history.as_mut_ptr();
}
Ok(())
}
pub fn set_auto_add_history(&mut self, enabled: bool) {
unsafe {
(*self.context).auto_add_history = enabled;
}
}
pub fn set_history_ignore_space(&mut self, enabled: bool) {
unsafe {
(*self.context).ignore_space = enabled;
}
}
pub fn set_completer<C: Completer + 'static>(&mut self, completer: C) -> Result<()> {
unsafe {
(*self.context).completer = Some(Box::new(completer));
}
let name = c"ed-complete";
let help = c"rust completion handler";
let key = c"\t";
let rc = unsafe { shim::el_addfn_bind(self.inner, name, help, key, completion_trampoline) };
if rc != 0 {
return Err(Error::operation(EL_ADDFN as i32, rc));
}
Ok(())
}
pub fn set_hinter<H: Hinter + 'static>(&mut self, hinter: H) {
unsafe {
(*self.context).hinter = Some(Box::new(hinter));
}
}
pub fn set_suggester<S: Suggester + 'static>(&mut self, suggester: S) -> Result<()> {
unsafe {
(*self.context).suggester = Some(Box::new(suggester));
}
self.install_suggestion_bindings()
}
pub fn set_suggestion_style(&mut self, prefix: impl Into<String>, suffix: impl Into<String>) {
unsafe {
(*self.context).suggest_prefix = prefix.into();
(*self.context).suggest_suffix = suffix.into();
}
}
pub fn clear_suggester(&mut self) {
unsafe {
(*self.context).suggester = None;
(*self.context).prev_suggest_total = 0;
}
}
pub fn add_action<F>(&mut self, name: &str, handler: F) -> Result<String>
where
F: FnMut(&ActionContext) -> Action + 'static,
{
let slot = unsafe { (*self.context).actions.len() };
if slot >= MAX_USER_ACTIONS {
return Err(Error::operation(EL_ADDFN as i32, -1));
}
unsafe {
(*self.context).actions.push(Box::new(handler));
}
let internal_name = format!("led-user-{slot}");
let c_name = CString::new(internal_name.as_str())?;
let c_help = CString::new(format!("user action: {name}"))?;
let trampoline = ACTION_TRAMPOLINES[slot];
let rc = unsafe { shim::el_addfn(self.inner, &c_name, &c_help, trampoline) };
if rc != 0 {
unsafe {
(*self.context).actions.pop();
}
return Err(Error::operation(EL_ADDFN as i32, rc));
}
Ok(internal_name)
}
pub fn bind_key(&mut self, key: &str, fn_name: &str) -> Result<()> {
let key_c = CString::new(key)?;
let fn_c = CString::new(fn_name)?;
let rc = unsafe { shim::el_bind(self.inner, &key_c, &fn_c) };
if rc != 0 {
return Err(Error::operation(EL_BIND as i32, rc));
}
Ok(())
}
fn install_suggestion_bindings(&mut self) -> Result<()> {
let rc = unsafe { shim::el_set_getcfn(self.inner, getcfn_trampoline) };
if rc != 0 {
return Err(Error::operation(EL_GETCFN as i32, rc));
}
let apply = c"led-suggest-apply";
let rc = unsafe {
shim::el_addfn(
self.inner,
apply,
c"accept inline suggestion",
suggest_apply_trampoline,
)
};
if rc != 0 {
return Err(Error::operation(EL_ADDFN as i32, rc));
}
let rc = unsafe { shim::el_bind(self.inner, c"^F", apply) };
if rc != 0 {
return Err(Error::operation(EL_BIND as i32, rc));
}
Ok(())
}
pub fn set_candidate_styler<S: CandidateStyler + 'static>(&mut self, styler: S) {
unsafe {
(*self.context).candidate_styler = Some(Box::new(styler));
}
}
pub fn set_signal_handling(&mut self, enabled: bool) -> Result<()> {
self.set_int(EL_SIGNAL as i32, enabled as i32)
}
pub fn beep(&mut self) {
unsafe { el_beep(self.inner) };
}
pub fn terminal_size(&self) -> (usize, usize) {
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 && ws.ws_row > 0 {
(ws.ws_col as usize, ws.ws_row as usize)
} else {
(80, 24)
}
}
}
pub fn set_editor(&mut self, editor: Editor) -> Result<()> {
let name = match editor {
Editor::Emacs => c"emacs",
Editor::Vi => c"vi",
};
let rc = unsafe { shim::el_set_editor(self.inner, name.as_ptr()) };
if rc != 0 {
return Err(Error::operation(EL_EDITOR as i32, rc));
}
Ok(())
}
pub fn source_config(&mut self, path: Option<&Path>) -> Result<()> {
let rc = match path {
Some(p) => {
let c_path = path_to_cstring(p)?;
unsafe { el_source(self.inner, c_path.as_ptr()) }
}
None => unsafe { el_source(self.inner, std::ptr::null()) },
};
if rc != 0 {
return Err(Error::operation(0, rc));
}
Ok(())
}
pub fn set_edit_mode(&mut self, enabled: bool) -> Result<()> {
self.set_int(EL_EDITMODE as i32, enabled as i32)
}
pub fn edit_mode(&self) -> Result<bool> {
Ok(self.get_int(EL_EDITMODE as i32)? != 0)
}
pub fn set_int(&mut self, op: i32, val: i32) -> Result<()> {
let ret = unsafe { shim::el_set_int(self.inner, op, val) };
if ret != 0 {
return Err(Error::operation(op, ret));
}
Ok(())
}
pub fn get_int(&self, op: i32) -> Result<i32> {
unsafe { shim::el_get_int(self.inner, op) }.ok_or_else(|| Error::operation(op, -1))
}
pub fn output(&mut self) -> Writer<'_> {
Writer {
stream: self.streams[1],
_marker: std::marker::PhantomData,
}
}
pub fn error_output(&mut self) -> Writer<'_> {
Writer {
stream: self.streams[2],
_marker: std::marker::PhantomData,
}
}
pub unsafe fn as_ptr(&self) -> *mut libedit_sys::EditLine {
self.inner
}
}
pub struct Writer<'a> {
stream: *mut FILE,
_marker: std::marker::PhantomData<&'a mut EditLine>,
}
impl std::fmt::Debug for Writer<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Writer")
.field("stream", &self.stream)
.finish()
}
}
impl std::io::Write for Writer<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
Ok(fwrite_bytes(self.stream, buf))
}
fn flush(&mut self) -> std::io::Result<()> {
if !self.stream.is_null() {
unsafe { libc::fflush(self.stream as *mut libc::FILE) };
}
Ok(())
}
}
fn fwrite_bytes(stream: *mut FILE, bytes: &[u8]) -> usize {
if stream.is_null() {
let n = unsafe { libc::write(2, bytes.as_ptr() as *const c_void, bytes.len()) };
return n.max(0) as usize;
}
unsafe {
libc::fwrite(
bytes.as_ptr() as *const c_void,
1,
bytes.len(),
stream as *mut libc::FILE,
)
}
}
impl Drop for EditLine {
fn drop(&mut self) {
unsafe { el_end(self.inner) };
drop(unsafe { Box::from_raw(self.context) });
unsafe { close_streams(&self.streams) };
}
}
extern "C" fn prompt_trampoline(el: *mut libedit_sys::EditLine) -> *mut c_char {
static EMPTY: wchar_t = 0;
let ctx = match context_from(el) {
Some(c) => c,
None => return &EMPTY as *const wchar_t as *mut c_char,
};
ctx.prompt_wide.as_ptr() as *mut c_char
}
extern "C" fn rprompt_trampoline(el: *mut libedit_sys::EditLine) -> *mut c_char {
static EMPTY: wchar_t = 0;
let empty = &EMPTY as *const wchar_t as *mut c_char;
let result = catch_unwind(AssertUnwindSafe(|| {
let Some(ctx) = context_from(el) else {
return empty;
};
let Some(hinter) = ctx.hinter.as_mut() else {
return empty;
};
let info = unsafe { el_line(el) };
if info.is_null() {
return empty;
}
let (line, cursor) = unsafe { line_and_cursor(&*info) };
let lc = LineContext::new(line, cursor);
match hinter.hint(&lc) {
Some(h) => {
ctx.hint_wide = prompt_to_wide(h.text());
ctx.hint_wide.as_ptr() as *mut c_char
}
None => {
ctx.hint_wide = vec![0];
empty
}
}
}));
result.unwrap_or(empty)
}
extern "C" fn completion_trampoline(el: *mut libedit_sys::EditLine, _c: i32) -> c_uchar {
let result = catch_unwind(AssertUnwindSafe(|| complete_impl(el)));
result.unwrap_or(CC_ERROR as c_uchar)
}
fn complete_impl(el: *mut libedit_sys::EditLine) -> c_uchar {
let Some(ctx) = context_from(el) else {
return CC_ERROR as c_uchar;
};
clear_ghost(ctx);
let Some(completer) = ctx.completer.as_mut() else {
return CC_ERROR as c_uchar;
};
let info = unsafe { el_line(el) };
if info.is_null() {
return CC_ERROR as c_uchar;
}
let info = unsafe { &*info };
let (line, cursor) = unsafe { line_and_cursor(info) };
let lc = LineContext::new(line, cursor);
let word = lc.word().to_string();
let completion = completer.complete(&lc);
if completion.is_empty() {
return CC_REFRESH_BEEP as c_uchar;
}
let candidates = &completion.candidates;
let suffix = if let Some(insertion) = &completion.insertion {
if insertion.is_empty() && candidates.len() > 1 {
String::new()
} else {
insertion.clone()
}
} else {
let lcp = if candidates.len() == 1 {
candidates[0].clone()
} else {
longest_common_prefix(candidates)
};
if lcp.len() > word.len() && lcp.starts_with(&word) {
let mut s = lcp[word.len()..].to_string();
if candidates.len() == 1 {
s.push(' ');
}
s
} else {
String::new()
}
};
if !suffix.is_empty() {
if let Ok(cstr) = CString::new(suffix.as_str()) {
unsafe { el_insertstr(el, cstr.as_ptr()) };
}
return CC_REFRESH as c_uchar;
}
if candidates.len() > 1 {
let Context {
list_buf,
candidate_styler,
out,
..
} = ctx;
list_buf.clear();
list_buf.push('\n');
for (i, cand) in candidates.iter().enumerate() {
if i > 0 {
list_buf.push_str(" ");
}
match candidate_styler {
Some(styler) => styler.style(cand, list_buf),
None => list_buf.push_str(cand),
}
}
list_buf.push('\n');
write_candidates(*out, list_buf);
return CC_REDISPLAY as c_uchar;
}
CC_NORM as c_uchar
}
unsafe extern "C" fn getcfn_trampoline(el: *mut libedit_sys::EditLine, out: *mut wchar_t) -> i32 {
let result = catch_unwind(AssertUnwindSafe(|| draw_suggestion_ghost(el)));
let _ = result;
let in_fd = context_from(el).map_or(0, |ctx| ctx.in_fd);
let mut lead: u8 = 0;
let n = unsafe { libc::read(in_fd, &mut lead as *mut u8 as *mut c_void, 1) };
if n <= 0 {
return if n == 0 { 0 } else { -1 }; }
let total = utf8_char_len(lead);
if total == 1 {
unsafe { *out = lead as wchar_t };
return 1;
}
let mut buf = [0u8; 4];
buf[0] = lead;
for slot in buf.iter_mut().take(total).skip(1) {
let mut b: u8 = 0;
let n = unsafe { libc::read(in_fd, &mut b as *mut u8 as *mut c_void, 1) };
if n <= 0 {
return if n == 0 { 0 } else { -1 };
}
*slot = b;
}
let cp = match std::str::from_utf8(&buf[..total]) {
Ok(s) => s.chars().next().map(|c| c as u32).unwrap_or(0xFFFD),
Err(_) => 0xFFFD,
};
unsafe { *out = cp as wchar_t };
1
}
fn draw_suggestion_ghost(el: *mut libedit_sys::EditLine) {
let Some(ctx) = context_from(el) else { return };
if ctx.suggester.is_none() || ctx.out.is_null() {
return;
}
let info = unsafe { el_line(el) };
if info.is_null() {
return;
}
let (line, cursor) = unsafe { line_and_cursor(&*info) };
let at_end = cursor >= line.len();
let suggestion = if at_end && !line.is_empty() {
ctx.suggester.as_mut().and_then(|s| {
let lc = LineContext::new(line.clone(), cursor);
s.suggest(&lc)
})
} else {
None
};
let sug_text = match &suggestion {
Some(s) if !s.is_empty() => s.text(),
_ => {
if ctx.prev_suggest_total > 0 {
clear_ghost(ctx);
}
return;
}
};
let stream = ctx.out;
ctx.suggest_buf.clear();
ctx.suggest_buf.push_str(&ctx.suggest_prefix);
ctx.suggest_buf.push_str(sug_text);
ctx.suggest_buf.push_str(&ctx.suggest_suffix);
let new_total = line.len() + sug_text.len();
if new_total < ctx.prev_suggest_total {
for _ in 0..(ctx.prev_suggest_total - new_total) {
ctx.suggest_buf.push(' ');
}
}
ctx.prev_suggest_total = new_total;
let term_cols = terminal_width(el).max(1);
let caret_pos = ctx.prompt_cols + display_width(&line);
let row = caret_pos / term_cols;
let to_column = caret_pos - (row * term_cols);
use std::fmt::Write;
let _ = write!(ctx.suggest_buf, "\x1b[{}G", to_column + 1);
fwrite_bytes(stream, ctx.suggest_buf.as_bytes());
unsafe { libc::fflush(stream as *mut libc::FILE) };
}
extern "C" fn suggest_apply_trampoline(el: *mut libedit_sys::EditLine, _ch: i32) -> c_uchar {
let result = catch_unwind(AssertUnwindSafe(|| suggest_apply_impl(el)));
result.unwrap_or(CC_ERROR as c_uchar)
}
fn suggest_apply_impl(el: *mut libedit_sys::EditLine) -> c_uchar {
let Some(ctx) = context_from(el) else {
return forward_char(el);
};
let Some(suggester) = ctx.suggester.as_mut() else {
return forward_char(el);
};
let info = unsafe { el_line(el) };
if info.is_null() {
return forward_char(el);
}
let (line, cursor) = unsafe { line_and_cursor(&*info) };
let at_end = cursor >= line.len();
let lc = LineContext::new(line, cursor);
let suggestion = suggester.suggest(&lc);
match suggestion {
Some(s) if at_end && !s.is_empty() => {
ctx.prev_suggest_total = 0;
if let Ok(cstr) = CString::new(s.text()) {
unsafe { el_insertstr(el, cstr.as_ptr()) };
}
CC_REDISPLAY as c_uchar
}
_ => forward_char(el),
}
}
fn forward_char(_el: *mut libedit_sys::EditLine) -> c_uchar {
CC_NORM as c_uchar
}
fn clear_ghost(ctx: &mut Context) {
if ctx.prev_suggest_total == 0 || ctx.out.is_null() {
return;
}
fwrite_bytes(ctx.out, b"\x1b[K");
unsafe { libc::fflush(ctx.out as *mut libc::FILE) };
ctx.prev_suggest_total = 0;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Redisplay,
Refresh,
Norm,
Beep,
}
impl Action {
fn to_cc(self) -> c_uchar {
match self {
Action::Redisplay => CC_REDISPLAY as c_uchar,
Action::Refresh => CC_REFRESH as c_uchar,
Action::Norm => CC_NORM as c_uchar,
Action::Beep => CC_REFRESH_BEEP as c_uchar,
}
}
}
pub struct ActionContext {
line: String,
cursor: usize,
stream: *mut FILE,
}
impl ActionContext {
pub fn line(&self) -> &str {
&self.line
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn word(&self) -> &str {
let before = &self.line[..self.cursor];
let start = before
.rfind(|c: char| c.is_ascii_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
&before[start..]
}
pub fn output(&self) -> Writer<'_> {
Writer {
stream: self.stream,
_marker: std::marker::PhantomData,
}
}
}
fn dispatch_user_action(el: *mut libedit_sys::EditLine, slot: usize) -> c_uchar {
let Some(ctx) = context_from(el) else {
return CC_ERROR as c_uchar;
};
clear_ghost(ctx);
let info = unsafe { el_line(el) };
let (line, cursor) = if info.is_null() {
(String::new(), 0)
} else {
unsafe { line_and_cursor(&*info) }
};
let action_ctx = ActionContext {
line,
cursor,
stream: ctx.out,
};
let action = if let Some(handler) = ctx.actions.get_mut(slot) {
handler(&action_ctx)
} else {
Action::Beep
};
action.to_cc()
}
macro_rules! action_trampolines {
($($idx:literal),*) => {
const ACTION_TRAMPOLINES: [shim::ElFn; MAX_USER_ACTIONS] = [
$({
extern "C" fn trampoline(
el: *mut libedit_sys::EditLine, _ch: i32,
) -> c_uchar {
let result = catch_unwind(AssertUnwindSafe(||
dispatch_user_action(el, $idx)
));
result.unwrap_or(CC_ERROR as c_uchar)
}
trampoline
},)*
];
};
}
action_trampolines!(0, 1, 2, 3, 4, 5, 6, 7);
fn terminal_width(_el: *mut libedit_sys::EditLine) -> usize {
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
ws.ws_col as usize
} else {
80
}
}
}
fn display_width(s: &str) -> usize {
let bytes = s.as_bytes();
let mut width = 0usize;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == 0x1b {
i += 1;
if i < bytes.len() && bytes[i] == b'[' {
i += 1;
}
while i < bytes.len() && !(0x40..=0x7e).contains(&bytes[i]) {
i += 1;
}
if i < bytes.len() {
i += 1;
}
} else if bytes[i] == 0x01 {
i += 1;
} else {
let ch_len = utf8_char_len(bytes[i]);
i += ch_len.max(1);
width += 1;
}
}
width
}
fn context_from<'a>(el: *mut libedit_sys::EditLine) -> Option<&'a mut Context> {
let mut data: usize = 0;
let rc = unsafe { shim::el_get_ptr(el, EL_CLIENTDATA as i32, &mut data) };
if rc != 0 || data == 0 {
return None;
}
Some(unsafe { &mut *(data as *mut Context) })
}
unsafe fn line_and_cursor(info: &LineInfo) -> (String, usize) {
let buffer = info.buffer;
let lastchar = info.lastchar;
let cursor = info.cursor;
if buffer.is_null() || lastchar.is_null() {
return (String::new(), 0);
}
let len = unsafe { lastchar.offset_from(buffer) }.max(0) as usize;
let cursor_off = if cursor.is_null() {
len
} else {
(unsafe { cursor.offset_from(buffer) }.max(0) as usize).min(len)
};
let bytes = unsafe { std::slice::from_raw_parts(buffer.cast::<u8>(), len) };
let line = String::from_utf8_lossy(bytes).into_owned();
(line, cursor_off)
}
fn write_candidates(stream: *mut FILE, listing: &str) {
fwrite_bytes(stream, listing.as_bytes());
if !stream.is_null() {
unsafe { libc::fflush(stream as *mut libc::FILE) };
}
}
fn prompt_to_wide(s: &str) -> Vec<wchar_t> {
let bytes = s.as_bytes();
let mut v: Vec<wchar_t> = Vec::with_capacity(s.len() + 5);
if !bytes.contains(&0x1b) {
v.extend(s.chars().map(|c| c as wchar_t));
v.push(0);
return v;
}
#[allow(clippy::unnecessary_cast)]
let delim = PROMPT_ESC_DELIM as wchar_t;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == 0x1b {
let start = i;
i += 1;
if i < bytes.len() && bytes[i] == b'[' {
i += 1;
}
while i < bytes.len() && !(0x40..=0x7e).contains(&bytes[i]) {
i += 1;
}
if i < bytes.len() {
i += 1; }
v.push(delim);
v.extend(bytes[start..i].iter().map(|&b| b as wchar_t));
v.push(delim);
} else {
let ch_len = utf8_char_len(bytes[i]);
let end = (i + ch_len).min(bytes.len());
if let Ok(s) = std::str::from_utf8(&bytes[i..end]) {
for c in s.chars() {
v.push(c as wchar_t);
}
}
i = end;
}
}
v.push(0);
v
}
fn utf8_char_len(b: u8) -> usize {
match b {
0x00..=0x7f => 1,
0xc0..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf7 => 4,
_ => 1, }
}
unsafe fn open_std_stream(fd: i32, mode: &CStr) -> *mut FILE {
let dup_fd = unsafe { libc::dup(fd) };
if dup_fd < 0 {
return std::ptr::null_mut();
}
let stream = unsafe { libc::fdopen(dup_fd, mode.as_ptr()) };
if stream.is_null() {
unsafe { libc::close(dup_fd) };
return std::ptr::null_mut();
}
stream as *mut FILE
}
unsafe fn close_streams(streams: &[*mut FILE; 3]) {
for &s in streams {
if !s.is_null() {
unsafe { libc::fclose(s as *mut libc::FILE) };
}
}
}
#[cfg(test)]
mod tests {
use super::{display_width, prompt_to_wide, PROMPT_ESC_DELIM};
use libedit_sys::wchar_t;
#[allow(clippy::unnecessary_cast)]
fn wide_to_string(v: &[wchar_t]) -> String {
v.iter()
.take_while(|&&w| w != 0)
.map(|&w| char::from_u32(w as u32).unwrap_or('\u{FFFD}'))
.collect()
}
#[allow(clippy::unnecessary_cast)]
fn d() -> char {
PROMPT_ESC_DELIM as u8 as char
}
#[test]
fn plain_text_is_unchanged() {
let wide = prompt_to_wide("prompt> ");
assert_eq!(wide_to_string(&wide), "prompt> ");
}
#[test]
fn display_width_counts_visible_chars() {
assert_eq!(display_width("hello"), 5);
assert_eq!(display_width(""), 0);
}
#[test]
fn display_width_ignores_ansi_escapes() {
assert_eq!(display_width("\x1b[2mghost\x1b[0m"), 5);
assert_eq!(display_width("\x1b[1;36mx\x1b[0m"), 1);
}
#[test]
fn display_width_ignores_prompt_delim() {
#[allow(clippy::unnecessary_cast)]
let delim = PROMPT_ESC_DELIM as u8 as char;
let s = format!("{delim}\x1b[32m{delim}> ");
assert_eq!(display_width(&s), 2);
}
#[test]
fn display_width_counts_multibyte_as_one() {
assert_eq!(display_width("café"), 4);
}
#[test]
fn wraps_a_single_sgr_sequence() {
let input = "\x1b[32m> ";
let expected = format!("{d}\x1b[32m{d}> ", d = d());
assert_eq!(wide_to_string(&prompt_to_wide(input)), expected);
}
#[test]
fn wraps_leading_and_trailing_sequences() {
let input = "\x1b[1;32m> \x1b[0m";
let expected = format!("{d}\x1b[1;32m{d}> {d}\x1b[0m{d}", d = d());
assert_eq!(wide_to_string(&prompt_to_wide(input)), expected);
}
#[test]
fn preserves_multibyte_text() {
let input = "caf\u{00e9} \x1b[0m";
let expected = format!("caf\u{00e9} {d}\x1b[0m{d}", d = d());
assert_eq!(wide_to_string(&prompt_to_wide(input)), expected);
}
#[test]
fn handles_escape_at_end_without_terminator() {
let input = "x\x1b";
let expected = format!("x{d}\x1b{d}", d = d());
assert_eq!(wide_to_string(&prompt_to_wide(input)), expected);
}
}