mod banner;
mod meta_command;
mod prompt;
mod reprex;
mod shell;
pub(crate) mod state;
use crate::completion::completer::CombinedCompleter;
use crate::completion::menu::{FunctionAwareMenu, StateSyncHistoryMenu};
use crate::completion::shell::ShellCompleter;
use crate::config::{
AutoSuggestions, Config, ConfigStatus, EditorMode, ModeIndicatorPosition, RSourceStatus,
history_dir,
};
use crate::editor::hinter::RLanguageHinter;
use crate::editor::mode::new_editor_state_ref;
use crate::editor::prompt::PromptFormatter;
use crate::highlighter::{CombinedHighlighter, MetaCommandHighlighter};
use crate::history::FuzzyHistory;
use anyhow::Result;
use crossterm::{
ExecutableCommand,
style::Stylize,
terminal::{self, ClearType},
};
use nu_ansi_term::{Color, Style};
use reedline::{
DefaultHinter, Emacs, HistorySessionId, IdeMenu, ListMenu, MenuBuilder, Reedline, ReedlineMenu,
Signal, SqliteBackedHistory, Vi, default_emacs_keybindings, default_vi_insert_keybindings,
default_vi_normal_keybindings,
};
use std::cell::RefCell;
use std::io::{self, Write};
use std::sync::atomic::{AtomicU16, Ordering};
use crate::editor::keybindings::{
add_auto_match_keybindings, add_common_keybindings, add_key_map_keybindings,
wrap_edit_mode_with_conditional_rules,
};
use crate::editor::validator::RValidator;
use banner::format_banner;
use meta_command::{MetaCommandResult, process_meta_command};
use prompt::RPrompt;
use reprex::{clear_input_lines, strip_reprex_output};
use shell::{execute_shell_command, restart_process};
use state::{PromptRuntimeConfig, ReplState};
thread_local! {
static REPL_STATE: RefCell<Option<ReplState>> = const { RefCell::new(None) };
}
static LAST_TERMINAL_WIDTH: AtomicU16 = AtomicU16::new(0);
const MIN_R_WIDTH: u16 = 20;
const MAX_R_WIDTH: u16 = 10000;
fn sync_r_width() {
let prev = LAST_TERMINAL_WIDTH.load(Ordering::Relaxed);
let (cols, _) = match terminal::size() {
Ok(size) => size,
Err(e) => {
if prev != 0 {
log::debug!(
"Failed to read terminal size (transient); keeping previous width: {:?}",
e
);
return;
}
log::debug!(
"Failed to read terminal size; falling back to default width: {:?}",
e
);
(80, 24)
}
};
let clamped = cols.clamp(MIN_R_WIDTH, MAX_R_WIDTH);
if prev != clamped {
let code = format!("options(width = {})", clamped);
match arf_harp::eval_string_with_visibility(&code) {
Ok(_) => {
LAST_TERMINAL_WIDTH.store(clamped, Ordering::Relaxed);
}
Err(e) => log::debug!("Failed to set R width option: {:?}", e),
}
}
}
pub(crate) const ARF_PREFIX: &str = "# [arf]";
macro_rules! arf_println {
($($arg:tt)*) => {
println!("{} {}", $crate::repl::ARF_PREFIX, format_args!($($arg)*))
};
}
macro_rules! arf_eprintln {
($($arg:tt)*) => {
eprintln!("{} {}", $crate::repl::ARF_PREFIX, format_args!($($arg)*))
};
}
pub(crate) use arf_eprintln;
pub(crate) use arf_println;
pub struct Repl {
config: Config,
config_path: Option<std::path::PathBuf>,
config_status: ConfigStatus,
r_source_status: RSourceStatus,
r_initialized: bool,
prompt_formatter: PromptFormatter,
session_id: Option<HistorySessionId>,
}
impl Repl {
pub fn new(
config: Config,
config_path: Option<std::path::PathBuf>,
config_status: ConfigStatus,
r_source_status: RSourceStatus,
session_id: Option<HistorySessionId>,
) -> Result<Self> {
let r_initialized = arf_libr::r_library().is_ok();
let prompt_formatter = PromptFormatter::new();
if config.startup.mode.reprex {
arf_libr::set_reprex_mode(true, &config.mode.reprex.comment);
}
Ok(Repl {
config,
config_path,
config_status,
r_source_status,
r_initialized,
prompt_formatter,
session_id,
})
}
fn history_session_id_raw(&self) -> Option<i64> {
self.session_id.map(i64::from)
}
fn r_history_path(&self) -> Option<std::path::PathBuf> {
if self.config.history.disabled {
return None;
}
let dir = self.config.history.dir.clone().or_else(history_dir);
dir.map(|d| d.join("r.db"))
}
fn shell_history_path(&self) -> Option<std::path::PathBuf> {
if self.config.history.disabled {
return None;
}
let dir = self.config.history.dir.clone().or_else(history_dir);
dir.map(|d| d.join("shell.db"))
}
fn create_r_hinter(&self) -> Option<Box<RLanguageHinter>> {
match self.config.editor.auto_suggestions {
AutoSuggestions::None => None,
AutoSuggestions::All => Some(Box::new(
RLanguageHinter::new().with_style(Style::new().italic().fg(Color::DarkGray)),
)),
AutoSuggestions::Cwd => Some(Box::new(
RLanguageHinter::new()
.with_style(Style::new().italic().fg(Color::DarkGray))
.with_cwd_aware(true),
)),
}
}
pub fn run(&mut self) -> Result<()> {
if self.config.startup.show_banner {
let banner = format_banner(&self.config, self.r_initialized);
if !self.r_initialized {
for line in banner.lines() {
if line.contains("R is not initialized") {
println!(
"# {}",
"R is not initialized. Commands will not be evaluated.".yellow()
);
} else {
println!("{}", line);
}
}
} else {
print!("{}", banner);
}
}
if self.r_initialized {
self.run_with_r_mainloop()?;
} else {
self.run_standalone()?;
}
Ok(())
}
fn run_with_r_mainloop(&self) -> Result<()> {
let line_editor = Reedline::create().use_bracketed_paste(true);
let (mut line_editor, r_history_ok) =
setup_history(line_editor, self.r_history_path(), self.session_id);
let editor_state = new_editor_state_ref();
line_editor = match self.config.editor.mode {
EditorMode::Vi => {
let mut insert_keybindings = default_vi_insert_keybindings();
add_common_keybindings(&mut insert_keybindings);
if self.config.editor.auto_match {
add_auto_match_keybindings(&mut insert_keybindings);
}
add_key_map_keybindings(&mut insert_keybindings, &self.config.editor.key_map);
let vi = Vi::new(insert_keybindings, default_vi_normal_keybindings());
line_editor.with_edit_mode(wrap_edit_mode_with_conditional_rules(
vi,
editor_state.clone(),
self.config.editor.auto_match,
self.config.experimental.completion_min_chars,
))
}
EditorMode::Emacs => {
let mut keybindings = default_emacs_keybindings();
add_common_keybindings(&mut keybindings);
if self.config.editor.auto_match {
add_auto_match_keybindings(&mut keybindings);
}
add_key_map_keybindings(&mut keybindings, &self.config.editor.key_map);
let emacs = Emacs::new(keybindings);
line_editor.with_edit_mode(wrap_edit_mode_with_conditional_rules(
emacs,
editor_state.clone(),
self.config.editor.auto_match,
self.config.experimental.completion_min_chars,
))
}
};
if self.config.completion.enabled {
let completer = Box::new(CombinedCompleter::with_settings_full(
self.config.completion.timeout_ms,
self.config.completion.debounce_ms,
self.config.completion.auto_paren_limit,
self.r_source_status.rig_enabled(),
self.config.experimental.r_completion.fuzzy,
self.config
.experimental
.r_completion
.package_functions
.clone(),
));
line_editor = line_editor.with_completer(completer);
let ide_menu = IdeMenu::default()
.with_name("completion_menu")
.with_max_completion_height(self.config.completion.max_height);
let completion_menu =
Box::new(FunctionAwareMenu::new(ide_menu).with_editor_state(editor_state.clone()));
line_editor = line_editor.with_menu(ReedlineMenu::EngineCompleter(completion_menu));
}
let (_, rows) = terminal::size().unwrap_or((80, 24));
let terminal_based_size = rows.saturating_sub(5) as usize;
let config_max_height = self.config.history.menu_max_height as usize;
let history_page_size = terminal_based_size.min(config_max_height).max(3);
let list_menu = ListMenu::default()
.with_name("history_menu")
.with_only_buffer_difference(false)
.with_page_size(history_page_size);
let history_menu =
Box::new(StateSyncHistoryMenu::new(list_menu).with_editor_state(editor_state.clone()));
line_editor = line_editor.with_menu(ReedlineMenu::HistoryMenu(history_menu));
line_editor = line_editor.with_validator(Box::new(
RValidator::new().with_editor_state(editor_state.clone()),
));
let highlighter = CombinedHighlighter::new(
self.config.colors.clone(),
self.config.editor.highlight_matching_bracket,
)
.with_editor_state(editor_state.clone());
line_editor = line_editor.with_highlighter(Box::new(highlighter));
if let Some(hinter) = self.create_r_hinter() {
line_editor = line_editor.with_hinter(hinter);
}
let auto_width = self.config.r.auto_width;
line_editor = line_editor
.with_break_signal(crate::ipc::break_signal())
.with_poll_interval(std::time::Duration::from_millis(33))
.with_idle_callback(Box::new(move || {
arf_libr::process_r_events();
if auto_width {
sync_r_width();
}
crate::ipc::poll_ipc_requests();
}));
let (shell_line_editor, shell_history_ok) = self.create_shell_line_editor();
if !r_history_ok && !shell_history_ok {
crate::ipc::clear_history_session_id();
}
let prompt_config = PromptRuntimeConfig::builder(
self.prompt_formatter.clone(),
self.config.prompt.format.clone(),
self.config.prompt.continuation.clone(),
self.config.prompt.shell_format.clone(),
)
.mode_indicator_position(self.config.prompt.mode_indicator)
.reprex(
self.config.startup.mode.reprex,
self.config.mode.reprex.comment.clone(),
)
.indicators(self.config.prompt.indicators.clone())
.autoformat(self.config.startup.mode.autoformat)
.main_color(self.config.colors.prompt.main)
.continuation_color(self.config.colors.prompt.continuation)
.shell_color(self.config.colors.prompt.shell)
.mode_indicator_color(self.config.colors.prompt.indicator)
.status(
self.config.prompt.status.clone(),
self.config.colors.prompt.status.clone(),
)
.duration(
self.config.experimental.prompt_duration.clone(),
self.config.colors.prompt.duration,
)
.spinner(self.config.experimental.prompt_spinner.clone())
.vi(
self.config.prompt.vi.clone(),
self.config.colors.prompt.vi.clone(),
)
.build();
let r_history_path = self.r_history_path();
let shell_history_path = self.shell_history_path();
REPL_STATE.with(|state| {
*state.borrow_mut() = Some(ReplState {
line_editor,
shell_line_editor,
prompt_config,
should_exit: false,
config_path: self.config_path.clone(),
config_status: self.config_status,
r_history_path,
shell_history_path,
r_source_status: self.r_source_status.clone(),
forget_config: self.config.experimental.history_forget.clone(),
sponge_queue: state::SpongeQueue::new(),
dir_stack: Vec::new(),
history_session_id: if r_history_ok || shell_history_ok {
self.history_session_id_raw()
} else {
None
},
});
});
let error_handler_code = arf_libr::global_error_handler_code();
match arf_harp::eval_string_with_visibility(error_handler_code) {
Ok(_) => {
log::info!("Global error handler initialized");
arf_libr::mark_global_error_handler_initialized();
}
Err(e) => {
log::warn!("Failed to initialize global error handler: {:?}", e);
}
}
#[cfg(unix)]
{
let askpass_handler_code = arf_libr::askpass_handler_code();
match arf_harp::eval_string_with_visibility(askpass_handler_code) {
Ok(_) => {
log::info!("Askpass handler initialized");
}
Err(e) => {
log::warn!("Failed to initialize askpass handler: {:?}", e);
}
}
}
if self.config.r.auto_width {
sync_r_width();
}
arf_libr::set_read_console_callback(read_console_callback);
#[cfg(windows)]
if arf_libr::is_r_interrupt_flag_available() {
if let Err(e) = ctrlc::set_handler(|| {
arf_libr::set_r_interrupt_pending();
}) {
log::warn!("Could not set Ctrl+C handler: {e}");
}
} else {
log::warn!(
"R interrupt flag not available; skipping Ctrl+C handler installation. \
Default console handler will terminate the process on Ctrl+C."
);
}
unsafe {
arf_libr::run_r_mainloop();
}
REPL_STATE.with(|state| {
if let Some(ref mut repl_state) = *state.borrow_mut()
&& repl_state.forget_config.enabled
&& !repl_state.sponge_queue.is_empty()
{
for id_to_delete in repl_state.sponge_queue.drain_failed_ids() {
let _ = repl_state.line_editor.history_mut().delete(id_to_delete);
}
let _ = repl_state.line_editor.sync_history();
}
});
REPL_STATE.with(|state| {
*state.borrow_mut() = None;
});
println!("\nGoodbye!");
Ok(())
}
fn run_standalone(&self) -> Result<()> {
let line_editor = Reedline::create().use_bracketed_paste(true);
let (mut line_editor, history_ok) =
setup_history(line_editor, self.r_history_path(), self.session_id);
if !history_ok {
crate::ipc::clear_history_session_id();
}
let history_session_id = if history_ok {
self.history_session_id_raw()
} else {
None
};
let editor_state = new_editor_state_ref();
line_editor = match self.config.editor.mode {
EditorMode::Vi => {
let mut insert_keybindings = default_vi_insert_keybindings();
add_common_keybindings(&mut insert_keybindings);
if self.config.editor.auto_match {
add_auto_match_keybindings(&mut insert_keybindings);
}
add_key_map_keybindings(&mut insert_keybindings, &self.config.editor.key_map);
let vi = Vi::new(insert_keybindings, default_vi_normal_keybindings());
line_editor.with_edit_mode(wrap_edit_mode_with_conditional_rules(
vi,
editor_state.clone(),
self.config.editor.auto_match,
self.config.experimental.completion_min_chars,
))
}
EditorMode::Emacs => {
let mut keybindings = default_emacs_keybindings();
add_common_keybindings(&mut keybindings);
if self.config.editor.auto_match {
add_auto_match_keybindings(&mut keybindings);
}
add_key_map_keybindings(&mut keybindings, &self.config.editor.key_map);
let emacs = Emacs::new(keybindings);
line_editor.with_edit_mode(wrap_edit_mode_with_conditional_rules(
emacs,
editor_state.clone(),
self.config.editor.auto_match,
self.config.experimental.completion_min_chars,
))
}
};
if let Some(hinter) = self.create_r_hinter() {
line_editor = line_editor.with_hinter(hinter);
}
let mode_position = self.config.prompt.mode_indicator;
let mode_indicator =
if self.config.startup.mode.reprex && mode_position != ModeIndicatorPosition::None {
Some(self.config.prompt.indicators.reprex.clone())
} else {
None
};
let prompt = RPrompt::new(
self.prompt_formatter.format(&self.config.prompt.format),
self.prompt_formatter
.format(&self.config.prompt.continuation),
)
.with_mode_indicator(mode_indicator, mode_position)
.with_colors(
self.config.colors.prompt.main,
self.config.colors.prompt.continuation,
self.config.colors.prompt.indicator,
);
let mut prompt_config =
PromptRuntimeConfig::builder(self.prompt_formatter.clone(), "R > ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.main_color(self.config.colors.prompt.main)
.continuation_color(self.config.colors.prompt.continuation)
.shell_color(self.config.colors.prompt.shell)
.mode_indicator_color(self.config.colors.prompt.indicator)
.status(
self.config.prompt.status.clone(),
self.config.colors.prompt.status.clone(),
)
.duration(
self.config.experimental.prompt_duration.clone(),
self.config.colors.prompt.duration,
)
.spinner(self.config.experimental.prompt_spinner.clone())
.vi(
self.config.prompt.vi.clone(),
self.config.colors.prompt.vi.clone(),
)
.build();
let r_history_path = self.r_history_path();
let shell_history_path = self.shell_history_path();
let mut dir_stack: Vec<std::path::PathBuf> = Vec::new();
loop {
match line_editor.read_line(&prompt) {
Ok(Signal::Success(line)) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(result) = process_meta_command(
&line,
&mut prompt_config,
&r_history_path,
&shell_history_path,
&self.r_source_status,
&mut dir_stack,
history_session_id,
) {
prompt_config.clear_command_duration();
let ctx = SessionInfoContext {
prompt_config: &prompt_config,
config_path: &self.config_path,
config_status: self.config_status,
r_history_path: &r_history_path,
shell_history_path: &shell_history_path,
r_source_status: &self.r_source_status,
};
match handle_meta_command_result(result, &ctx) {
MetaAction::Continue => continue,
MetaAction::Exit => {
println!("\nGoodbye!");
return Ok(());
}
}
}
println!("{}", format!("[R not initialized] {}", line).dark_grey());
}
Ok(Signal::CtrlC) => {
let _ = io::stdout().execute(terminal::Clear(ClearType::FromCursorDown));
println!("^C");
continue;
}
Ok(Signal::CtrlD) => {
let _ = io::stdout().execute(terminal::Clear(ClearType::FromCursorDown));
println!("\nGoodbye!");
break;
}
Ok(_) => {
continue;
}
Err(err) => {
eprintln!("Error: {}", err);
break;
}
}
}
Ok(())
}
fn create_shell_line_editor(&self) -> (Reedline, bool) {
let shell_editor = Reedline::create().use_bracketed_paste(true);
let (mut shell_editor, history_ok) =
setup_history(shell_editor, self.shell_history_path(), self.session_id);
shell_editor = match self.config.editor.mode {
EditorMode::Vi => {
let mut insert_keybindings = default_vi_insert_keybindings();
add_common_keybindings(&mut insert_keybindings);
add_key_map_keybindings(&mut insert_keybindings, &self.config.editor.key_map);
shell_editor.with_edit_mode(Box::new(Vi::new(
insert_keybindings,
default_vi_normal_keybindings(),
)))
}
EditorMode::Emacs => {
let mut keybindings = default_emacs_keybindings();
add_common_keybindings(&mut keybindings);
add_key_map_keybindings(&mut keybindings, &self.config.editor.key_map);
shell_editor.with_edit_mode(Box::new(Emacs::new(keybindings)))
}
};
if self.config.completion.enabled {
let completer = Box::new(ShellCompleter::new(
self.config.experimental.shell_completion.command_names,
));
shell_editor = shell_editor.with_completer(completer);
let completion_menu = Box::new(
IdeMenu::default()
.with_name("completion_menu")
.with_max_completion_height(self.config.completion.max_height),
);
shell_editor = shell_editor.with_menu(ReedlineMenu::EngineCompleter(completion_menu));
}
let (_, rows) = terminal::size().unwrap_or((80, 24));
let terminal_based_size = rows.saturating_sub(5) as usize;
let config_max_height = self.config.history.menu_max_height as usize;
let history_page_size = terminal_based_size.min(config_max_height).max(3);
let history_menu = Box::new(
ListMenu::default()
.with_name("history_menu")
.with_only_buffer_difference(false)
.with_page_size(history_page_size),
);
shell_editor = shell_editor.with_menu(ReedlineMenu::HistoryMenu(history_menu));
shell_editor = shell_editor.with_highlighter(Box::new(MetaCommandHighlighter::new(
self.config.colors.meta.clone(),
)));
if !matches!(self.config.editor.auto_suggestions, AutoSuggestions::None) {
let hinter =
DefaultHinter::default().with_style(Style::new().italic().fg(Color::DarkGray));
shell_editor = shell_editor.with_hinter(Box::new(hinter));
}
shell_editor = shell_editor
.with_poll_interval(std::time::Duration::from_millis(33))
.with_idle_callback(Box::new(|| {
arf_libr::process_r_events();
}));
(shell_editor, history_ok)
}
}
fn read_console_callback(r_prompt: &str) -> Option<String> {
REPL_STATE.with(|state| {
let mut guard = match state.try_borrow_mut() {
Ok(guard) => guard,
Err(_) => {
return None;
}
};
let state = guard.as_mut()?;
if state.should_exit {
return None;
}
crate::ipc::set_r_at_prompt(is_r_command_prompt(r_prompt));
if is_r_command_prompt(r_prompt) && !state.prompt_config.is_shell_enabled() {
let had_error = if state.line_editor.has_last_command_context() {
let had_error = arf_libr::command_had_error();
let exit_status = if had_error { 1i64 } else { 0i64 };
let captured_id: std::cell::Cell<Option<reedline::HistoryItemId>> =
std::cell::Cell::new(None);
let _ = state.line_editor.update_last_command_context(&|mut item| {
item.exit_status = Some(exit_status);
captured_id.set(item.id);
item
});
if state.forget_config.enabled {
let effective_delay = if state.forget_config.on_exit_only {
usize::MAX
} else {
state.forget_config.delay
};
if let Some(id_to_delete) = state.sponge_queue.record_command(
had_error,
captured_id.get(),
effective_delay,
) {
let _ = state.line_editor.history_mut().delete(id_to_delete);
}
}
had_error
} else {
false
};
state.prompt_config.set_last_command_failed(had_error);
state.prompt_config.set_command_duration();
arf_libr::reset_command_error_state();
}
if is_r_command_prompt(r_prompt)
&& !state.prompt_config.is_shell_enabled()
&& let Some(op) = crate::ipc::take_pending_ipc_operation()
{
use crate::ipc::{
PendingIpcKind, accept_user_input, run_silent_eval, setup_visible_eval,
};
match op.kind {
PendingIpcKind::SilentEvaluate { reply } => {
run_silent_eval(&op.code, reply);
}
PendingIpcKind::VisibleEvaluate { reply, timeout } => {
setup_visible_eval(reply, timeout);
let prompt_str = "agent> ";
println!("{}{}", prompt_str.dark_cyan(), op.code);
if !op.code.is_empty() {
state.prompt_config.set_command_start();
state.prompt_config.start_spinner();
}
crate::ipc::set_r_at_prompt(false);
return Some(op.code);
}
PendingIpcKind::UserInput { reply } => {
accept_user_input(reply);
let prompt_str = "agent> ";
println!("{}{}", prompt_str.dark_cyan(), op.code);
if !op.code.is_empty() {
state.prompt_config.set_command_start();
state.prompt_config.start_spinner();
}
crate::ipc::set_r_at_prompt(false);
return Some(op.code);
}
}
}
loop {
let prompt = if r_prompt.starts_with('+') {
state.prompt_config.build_cont_prompt()
} else if is_r_command_prompt(r_prompt) {
state.prompt_config.build_main_prompt()
} else {
RPrompt::new(r_prompt.to_string(), r_prompt.to_string())
};
let editor = if state.prompt_config.is_shell_enabled() {
&mut state.shell_line_editor
} else {
&mut state.line_editor
};
arf_libr::process_r_events();
let is_menu_prompt = !is_r_command_prompt(r_prompt) && !r_prompt.starts_with('+');
match editor.read_line(&prompt) {
Ok(Signal::Success(line)) => {
if is_menu_prompt {
return Some(line);
}
if let Some(result) = process_meta_command(
&line,
&mut state.prompt_config,
&state.r_history_path,
&state.shell_history_path,
&state.r_source_status,
&mut state.dir_stack,
state.history_session_id,
) {
state.prompt_config.clear_command_duration();
let ctx = SessionInfoContext {
prompt_config: &state.prompt_config,
config_path: &state.config_path,
config_status: state.config_status,
r_history_path: &state.r_history_path,
shell_history_path: &state.shell_history_path,
r_source_status: &state.r_source_status,
};
match handle_meta_command_result(result, &ctx) {
MetaAction::Continue => continue,
MetaAction::Exit => {
state.should_exit = true;
return None;
}
}
}
if state.prompt_config.is_shell_enabled() {
let trimmed = line.trim();
if !trimmed.is_empty() {
if trimmed == "exit" || trimmed == "logout" {
state.prompt_config.set_shell(false);
arf_println!("Returned to R mode.");
continue;
}
if let Some(hint) = meta_command::dir_command_hint(trimmed) {
arf_println!("{}", hint);
}
execute_shell_command(trimmed);
}
continue;
}
let (original_line, line) = if state.prompt_config.is_reprex_enabled() {
(line.clone(), strip_reprex_output(&line))
} else {
(line.clone(), line)
};
let code = state.prompt_config.maybe_format_code(&line);
if state.prompt_config.is_reprex_enabled() && !code.is_empty() {
clear_input_lines(&original_line, &code);
}
if !code.is_empty() {
state.prompt_config.set_command_start();
state.prompt_config.start_spinner();
}
crate::ipc::set_r_at_prompt(false);
return Some(code);
}
Ok(Signal::CtrlC) => {
let _ = io::stdout().execute(terminal::Clear(ClearType::FromCursorDown));
println!("^C");
if state.prompt_config.is_shell_enabled() {
state.prompt_config.set_shell(false);
arf_println!("Returned to R mode.");
continue;
}
return Some(String::new());
}
Ok(Signal::CtrlD) => {
let _ = io::stdout().execute(terminal::Clear(ClearType::FromCursorDown));
if state.prompt_config.is_shell_enabled() {
state.prompt_config.set_shell(false);
arf_println!("Returned to R mode.");
continue;
}
state.should_exit = true;
return None;
}
Ok(Signal::ExternalBreak(buffer)) => {
if let Some(op) = crate::ipc::take_pending_ipc_operation() {
use crate::ipc::{
PendingIpcKind, accept_user_input, reject_operation_user_typing,
run_silent_eval, setup_visible_eval,
};
if !buffer.trim().is_empty() {
reject_operation_user_typing(op, &buffer);
continue;
}
let clear_and_show_agent_prompt = |code: &str| {
let mut out = io::stdout();
let _ = out.execute(crossterm::cursor::MoveToColumn(0));
let _ = out.execute(terminal::Clear(ClearType::CurrentLine));
println!("{}{}", "agent> ".dark_cyan(), code);
};
if let PendingIpcKind::SilentEvaluate { reply } = op.kind {
{
let mut out = io::stdout();
let _ = out.execute(crossterm::cursor::MoveToColumn(0));
let _ = out.execute(terminal::Clear(ClearType::CurrentLine));
print!("{}", "[evaluating...]".dark_cyan());
let _ = out.flush();
}
run_silent_eval(&op.code, reply);
{
let mut out = io::stdout();
let _ = out.execute(crossterm::cursor::MoveToColumn(0));
let _ = out.execute(terminal::Clear(ClearType::CurrentLine));
}
continue;
}
match op.kind {
PendingIpcKind::VisibleEvaluate { reply, timeout } => {
setup_visible_eval(reply, timeout);
}
PendingIpcKind::UserInput { reply } => {
accept_user_input(reply);
}
PendingIpcKind::SilentEvaluate { .. } => unreachable!(),
}
clear_and_show_agent_prompt(&op.code);
if !op.code.is_empty() {
let entry = reedline::HistoryItem::from_command_line(&op.code);
let _ = editor.history_mut().save(entry);
}
if !op.code.is_empty() {
state.prompt_config.set_command_start();
state.prompt_config.start_spinner();
}
crate::ipc::set_r_at_prompt(false);
return Some(op.code);
}
continue;
}
Ok(_) => continue,
Err(err) => {
eprintln!("Error: {}", err);
state.should_exit = true;
return None;
}
}
}
})
}
struct IpcAlternateGuard {
was_alternate: bool,
}
impl IpcAlternateGuard {
fn new() -> Self {
let was_alternate = crate::ipc::is_in_alternate_mode();
crate::ipc::set_in_alternate_mode(true);
Self { was_alternate }
}
}
impl Drop for IpcAlternateGuard {
fn drop(&mut self) {
crate::ipc::set_in_alternate_mode(self.was_alternate);
}
}
fn with_ipc_alternate_guard<R>(f: impl FnOnce() -> R) -> R {
let _guard = IpcAlternateGuard::new();
f()
}
fn run_pager_help_browser(query: &str) {
let help_result = with_ipc_alternate_guard(|| crate::pager::run_help_browser(query));
if let Err(e) = help_result {
arf_println!("Error in help browser: {}", e);
}
}
fn run_pager_history_browser(path: &std::path::Path, mode: crate::pager::HistoryDbMode) {
let browser_result = with_ipc_alternate_guard(|| crate::pager::run_history_browser(path, mode));
match browser_result {
Ok(crate::pager::HistoryBrowserResult::Copied(cmd)) => {
let display = crate::pager::text_utils::truncate_to_width(&cmd, 60);
arf_println!("Copied: {}", display);
}
Ok(crate::pager::HistoryBrowserResult::Cancelled) => {}
Err(e) => {
arf_println!("Error: {}", e);
}
}
}
enum MetaAction {
Continue,
Exit,
}
struct SessionInfoContext<'a> {
prompt_config: &'a PromptRuntimeConfig,
config_path: &'a Option<std::path::PathBuf>,
config_status: ConfigStatus,
r_history_path: &'a Option<std::path::PathBuf>,
shell_history_path: &'a Option<std::path::PathBuf>,
r_source_status: &'a RSourceStatus,
}
fn handle_meta_command_result(
result: MetaCommandResult,
ctx: &SessionInfoContext<'_>,
) -> MetaAction {
match result {
MetaCommandResult::Handled | MetaCommandResult::ShellExecuted => MetaAction::Continue,
MetaCommandResult::Exit => MetaAction::Exit,
MetaCommandResult::Unknown(cmd) => {
arf_println!(
"Unknown command: {}. Type :commands for available commands.",
cmd
);
MetaAction::Continue
}
MetaCommandResult::Restart(version) => {
restart_process(version.as_deref());
MetaAction::Continue
}
MetaCommandResult::ShowHelpBrowser(query) => {
run_pager_help_browser(&query);
MetaAction::Continue
}
MetaCommandResult::ShowSessionInfo => {
with_ipc_alternate_guard(|| {
crate::pager::display_session_info(
ctx.prompt_config,
ctx.config_path,
ctx.config_status,
ctx.r_history_path,
ctx.shell_history_path,
ctx.r_source_status,
);
});
MetaAction::Continue
}
MetaCommandResult::ShowChangelog => {
with_ipc_alternate_guard(crate::pager::display_changelog);
MetaAction::Continue
}
MetaCommandResult::ShowHistoryBrowser { path, mode } => {
run_pager_history_browser(&path, mode);
MetaAction::Continue
}
MetaCommandResult::ShowHistorySchema => {
if let Err(e) =
with_ipc_alternate_guard(crate::pager::history_schema::show_schema_pager)
{
arf_println!("Error: {}", e);
}
MetaAction::Continue
}
}
}
fn is_r_command_prompt(prompt: &str) -> bool {
if prompt.starts_with('+') {
return false;
}
match arf_harp::r_n_frame() {
Ok(n_frame) => n_frame == 0,
Err(_) => {
prompt.ends_with("> ")
}
}
}
fn setup_history(
line_editor: Reedline,
history_path: Option<std::path::PathBuf>,
session_id: Option<HistorySessionId>,
) -> (Reedline, bool) {
let Some(path) = history_path else {
return (line_editor, false);
};
match SqliteBackedHistory::with_file(path.clone(), session_id, Some(chrono::Utc::now())) {
Ok(history) => {
let fuzzy_history = FuzzyHistory::new(history);
let editor = line_editor
.with_history_session_id(session_id)
.with_history(Box::new(fuzzy_history));
(editor, true)
}
Err(e) => {
log::warn!("Failed to open history database {}: {}", path.display(), e);
(line_editor, false)
}
}
}
#[cfg(test)]
mod sponge_tests {
use super::state::SpongeQueue;
use reedline::HistoryItemId;
fn make_id(id: i64) -> HistoryItemId {
HistoryItemId::new(id)
}
fn run_commands(commands: &[(bool, i64)], delay: usize) -> Vec<HistoryItemId> {
let mut queue = SpongeQueue::new();
let mut deleted = Vec::new();
for &(is_failure, id) in commands {
let history_id = Some(make_id(id));
if let Some(id_to_delete) = queue.record_command(is_failure, history_id, delay) {
deleted.push(id_to_delete);
}
}
deleted
}
#[test]
fn test_sponge_delay_keeps_recent_failures() {
let commands = vec![
(true, 1), (false, 2), (false, 3), ];
let deleted = run_commands(&commands, 2);
assert_eq!(deleted.len(), 1);
assert_eq!(deleted[0], make_id(1));
}
#[test]
fn test_sponge_delay_zero_deletes_immediately() {
let commands = vec![
(true, 1), ];
let deleted = run_commands(&commands, 0);
assert_eq!(deleted.len(), 1);
assert_eq!(deleted[0], make_id(1));
}
#[test]
fn test_sponge_success_does_not_delete() {
let commands = vec![
(false, 1), (false, 2), (false, 3), ];
let deleted = run_commands(&commands, 2);
assert!(deleted.is_empty(), "No failures to delete");
}
#[test]
fn test_sponge_multiple_failures_fifo() {
let commands = vec![
(true, 1), (true, 2), (true, 3), (false, 4), ];
let deleted = run_commands(&commands, 2);
assert_eq!(deleted.len(), 2);
assert_eq!(deleted[0], make_id(1)); assert_eq!(deleted[1], make_id(2)); }
#[test]
fn test_sponge_interleaved_success_failure() {
let commands = vec![
(true, 1), (false, 2), (true, 3), (false, 4), (false, 5), ];
let deleted = run_commands(&commands, 3);
assert_eq!(deleted.len(), 1);
assert_eq!(deleted[0], make_id(1));
}
#[test]
fn test_sponge_large_delay_no_deletion() {
let commands = vec![(true, 1), (true, 2), (true, 3), (false, 4), (false, 5)];
let deleted = run_commands(&commands, 100);
assert!(deleted.is_empty(), "Delay is larger than command count");
}
#[test]
fn test_sponge_delay_one_keeps_one_command() {
let commands = vec![
(true, 1), (false, 2), (true, 3), (true, 4), ];
let deleted = run_commands(&commands, 1);
assert_eq!(deleted.len(), 2);
assert_eq!(deleted[0], make_id(1));
assert_eq!(deleted[1], make_id(3));
}
#[test]
fn test_sponge_realistic_scenario() {
let commands = vec![
(true, 100), (false, 101), (false, 102), ];
let deleted = run_commands(&commands, 2);
assert_eq!(deleted.len(), 1);
assert_eq!(deleted[0], make_id(100));
}
#[test]
fn test_sponge_drain_failed_ids() {
let mut queue = SpongeQueue::new();
queue.record_command(true, Some(make_id(1)), 100);
queue.record_command(false, Some(make_id(2)), 100);
queue.record_command(true, Some(make_id(3)), 100);
queue.record_command(false, Some(make_id(4)), 100);
let drained: Vec<_> = queue.drain_failed_ids().collect();
assert_eq!(drained.len(), 2);
assert_eq!(drained[0], make_id(1));
assert_eq!(drained[1], make_id(3));
assert!(queue.is_empty());
}
}