Skip to content

Commit 60da1dd

Browse files
feat: add bottom_buffer_fraction config for prompt positioning
- adds bottom_buffer_fraction to prompt config (default 1.0, disabled) - implements ensure_bottom_buffer() to scroll terminal when needed - integrates into both R mainloop and standalone modes - clamps values to 0.0, 1.0 range via builder pattern - adds 4 tests for config parsing and defaults - updates JSON schema and snapshots fraction values: 0.0=pinned top, 0.5=upper half, 1.0=disabled (default)
1 parent 2b6d0e7 commit 60da1dd

7 files changed

Lines changed: 143 additions & 1 deletion

File tree

artifacts/arf.schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"prompt": {
109109
"$ref": "#/$defs/PromptConfig",
110110
"default": {
111+
"bottom_buffer_fraction": 1.0,
111112
"continuation": "+ ",
112113
"format": "{status}R {version}> ",
113114
"indicators": {
@@ -1705,6 +1706,12 @@
17051706
"description": "Prompt configuration.",
17061707
"type": "object",
17071708
"properties": {
1709+
"bottom_buffer_fraction": {
1710+
"description": "Fraction of terminal height to reserve as bottom buffer.\n\nThis keeps the prompt away from the bottom of the terminal by scrolling\nup when the cursor would go below this fraction of the terminal height.\n\n- 0.0: Prompt stays at top of terminal\n- 0.5: Prompt stays in upper half (reserve bottom 50%)\n- 1.0: No buffer, prompt at bottom (default, current behavior)\n\nValues outside [0.0, 1.0] are clamped to this range.",
1711+
"type": "number",
1712+
"format": "float",
1713+
"default": 1.0
1714+
},
17081715
"continuation": {
17091716
"description": "Continuation prompt for multiline input.",
17101717
"type": "string",

crates/arf-console/src/config/mod.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,4 +986,40 @@ auto_match = false
986986
// Restore permissions so tempdir cleanup succeeds
987987
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
988988
}
989+
990+
#[test]
991+
fn test_default_bottom_buffer_fraction() {
992+
let config = Config::default();
993+
assert_eq!(config.prompt.bottom_buffer_fraction, 1.0);
994+
}
995+
996+
#[test]
997+
fn test_parse_bottom_buffer_fraction() {
998+
let toml_str = r#"
999+
[prompt]
1000+
bottom_buffer_fraction = 0.5
1001+
"#;
1002+
let config: Config = toml::from_str(toml_str).unwrap();
1003+
assert_eq!(config.prompt.bottom_buffer_fraction, 0.5);
1004+
}
1005+
1006+
#[test]
1007+
fn test_parse_bottom_buffer_fraction_top() {
1008+
let toml_str = r#"
1009+
[prompt]
1010+
bottom_buffer_fraction = 0.0
1011+
"#;
1012+
let config: Config = toml::from_str(toml_str).unwrap();
1013+
assert_eq!(config.prompt.bottom_buffer_fraction, 0.0);
1014+
}
1015+
1016+
#[test]
1017+
fn test_parse_bottom_buffer_fraction_disabled() {
1018+
let toml_str = r#"
1019+
[prompt]
1020+
bottom_buffer_fraction = 1.0
1021+
"#;
1022+
let config: Config = toml::from_str(toml_str).unwrap();
1023+
assert_eq!(config.prompt.bottom_buffer_fraction, 1.0);
1024+
}
9891025
}

crates/arf-console/src/config/prompt.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ pub struct PromptConfig {
2222
pub status: StatusConfig,
2323
/// Vi mode indicator configuration.
2424
pub vi: ViConfig,
25+
/// Fraction of terminal height to reserve as bottom buffer.
26+
///
27+
/// This keeps the prompt away from the bottom of the terminal by scrolling
28+
/// up when the cursor would go below this fraction of the terminal height.
29+
///
30+
/// - 0.0: Prompt stays at top of terminal
31+
/// - 0.5: Prompt stays in upper half (reserve bottom 50%)
32+
/// - 1.0: No buffer, prompt at bottom (default, current behavior)
33+
///
34+
/// Values outside [0.0, 1.0] are clamped to this range.
35+
#[serde(default = "default_bottom_buffer_fraction")]
36+
pub bottom_buffer_fraction: f32,
37+
}
38+
39+
fn default_bottom_buffer_fraction() -> f32 {
40+
1.0
2541
}
2642

2743
impl Default for PromptConfig {
@@ -34,6 +50,7 @@ impl Default for PromptConfig {
3450
indicators: Indicators::default(),
3551
status: StatusConfig::default(),
3652
vi: ViConfig::default(),
53+
bottom_buffer_fraction: default_bottom_buffer_fraction(),
3754
}
3855
}
3956
}

crates/arf-console/src/config/snapshots/arf__config__tests__schema_tests__config_schema.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ expression: schema
112112
"prompt": {
113113
"$ref": "#/$defs/PromptConfig",
114114
"default": {
115+
"bottom_buffer_fraction": 1.0,
115116
"continuation": "+ ",
116117
"format": "{status}R {version}> ",
117118
"indicators": {
@@ -1709,6 +1710,12 @@ expression: schema
17091710
"description": "Prompt configuration.",
17101711
"type": "object",
17111712
"properties": {
1713+
"bottom_buffer_fraction": {
1714+
"description": "Fraction of terminal height to reserve as bottom buffer.\n\nThis keeps the prompt away from the bottom of the terminal by scrolling\nup when the cursor would go below this fraction of the terminal height.\n\n- 0.0: Prompt stays at top of terminal\n- 0.5: Prompt stays in upper half (reserve bottom 50%)\n- 1.0: No buffer, prompt at bottom (default, current behavior)\n\nValues outside [0.0, 1.0] are clamped to this range.",
1715+
"type": "number",
1716+
"format": "float",
1717+
"default": 1.0
1718+
},
17121719
"continuation": {
17131720
"description": "Continuation prompt for multiline input.",
17141721
"type": "string",

crates/arf-console/src/config/snapshots/arf__config__tests__schema_tests__default_config.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ format = "{status}R {version}> "
3030
continuation = "+ "
3131
shell_format = "[{shell}] $ "
3232
mode_indicator = "prefix"
33+
bottom_buffer_fraction = 1.0
3334

3435
[prompt.indicators]
3536
reprex = "[reprex] "

crates/arf-console/src/repl/mod.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::highlighter::{CombinedHighlighter, MetaCommandHighlighter};
2020
use crate::history::FuzzyHistory;
2121
use anyhow::Result;
2222
use crossterm::{
23-
ExecutableCommand,
23+
ExecutableCommand, cursor,
2424
style::Stylize,
2525
terminal::{self, ClearType},
2626
};
@@ -32,6 +32,7 @@ use reedline::{
3232
};
3333
use std::cell::RefCell;
3434
use std::io;
35+
use std::io::Write;
3536
use std::sync::atomic::{AtomicU16, Ordering};
3637

3738
use crate::editor::keybindings::{
@@ -102,6 +103,48 @@ fn sync_r_width() {
102103
}
103104
}
104105

106+
/// Ensure the prompt maintains a buffer from the bottom of the terminal.
107+
///
108+
/// When `fraction` is less than 1.0, this function scrolls the terminal up
109+
/// to ensure the prompt stays above the specified fraction of terminal height.
110+
/// For example, with fraction 0.5, the prompt will stay in the upper half.
111+
///
112+
/// - fraction = 0.0: Prompt stays at top of terminal
113+
/// - fraction = 0.5: Prompt stays in upper half (reserve bottom 50%)
114+
/// - fraction = 1.0: No buffer, prompt at bottom (disabled)
115+
fn ensure_bottom_buffer(fraction: f32) {
116+
// Disabled when fraction >= 1.0 or <= 0.0
117+
if fraction >= 1.0 || fraction <= 0.0 {
118+
return;
119+
}
120+
121+
let Ok((_, cursor_row)) = cursor::position() else {
122+
return;
123+
};
124+
let Ok((_, term_rows)) = terminal::size() else {
125+
return;
126+
};
127+
128+
// Calculate the target row based on fraction
129+
// fraction 0.0 -> row 2 (leave small margin at top), fraction 0.5 -> middle, etc.
130+
let min_top_margin = 2u16;
131+
let target_row = (term_rows as f32 * fraction.clamp(0.0, 1.0)).max(min_top_margin as f32) as u16;
132+
133+
// If cursor is at or below the target row, we need to scroll up and move cursor
134+
if cursor_row >= target_row {
135+
let lines_to_scroll = cursor_row.saturating_sub(target_row) + 1;
136+
137+
// Scroll terminal up to create space, then move cursor to target row
138+
let mut stdout = io::stdout();
139+
for _ in 0..lines_to_scroll {
140+
let _ = stdout.execute(terminal::ScrollUp(1));
141+
}
142+
// Move cursor to the target row (column 0)
143+
let _ = stdout.execute(cursor::MoveTo(0, target_row));
144+
let _ = stdout.flush();
145+
}
146+
}
147+
105148
/// Prefix for arf messages to distinguish them from R output.
106149
/// Uses R comment syntax so messages don't interfere with R code.
107150
pub(crate) const ARF_PREFIX: &str = "# [arf]";
@@ -408,6 +451,7 @@ impl Repl {
408451
self.config.prompt.vi.clone(),
409452
self.config.colors.prompt.vi.clone(),
410453
)
454+
.bottom_buffer_fraction(self.config.prompt.bottom_buffer_fraction)
411455
.build();
412456

413457
// Get history paths for :history commands
@@ -598,6 +642,9 @@ impl Repl {
598642
let mut dir_stack: Vec<std::path::PathBuf> = Vec::new();
599643

600644
loop {
645+
// Apply bottom buffer to keep prompt away from terminal bottom
646+
ensure_bottom_buffer(self.config.prompt.bottom_buffer_fraction);
647+
601648
match line_editor.read_line(&prompt) {
602649
Ok(Signal::Success(line)) => {
603650
let trimmed = line.trim();
@@ -879,6 +926,12 @@ fn read_console_callback(r_prompt: &str) -> Option<String> {
879926
// keeping graphics windows (plot(), help browser) responsive.
880927
arf_libr::process_r_events();
881928

929+
// Apply bottom buffer to keep prompt away from terminal bottom
930+
// Only for command prompts, not continuation or menu prompts
931+
if is_r_command_prompt(r_prompt) {
932+
ensure_bottom_buffer(state.prompt_config.bottom_buffer_fraction());
933+
}
934+
882935
// Track whether we're in a non-standard prompt mode (menu selection, etc.)
883936
let is_menu_prompt = !is_r_command_prompt(r_prompt) && !r_prompt.starts_with('+');
884937

crates/arf-console/src/repl/state.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ pub struct PromptRuntimeConfig {
170170
vi_config: ViConfig,
171171
/// Vi mode colors for prompt indicator.
172172
vi_colors: ViColorConfig,
173+
/// Bottom buffer fraction to keep prompt away from terminal bottom.
174+
/// 0.0 = top of terminal, 0.5 = middle, 1.0 = no buffer (default).
175+
bottom_buffer_fraction: f32,
173176
}
174177

175178
impl PromptRuntimeConfig {
@@ -414,6 +417,16 @@ impl PromptRuntimeConfig {
414417
self.autoformat_enabled
415418
}
416419

420+
/// Get the bottom buffer fraction for keeping prompt away from terminal bottom.
421+
///
422+
/// Returns a value between 0.0 and 1.0:
423+
/// - 0.0: Prompt at top of terminal
424+
/// - 0.5: Prompt in upper half (reserve bottom 50%)
425+
/// - 1.0: No buffer, prompt at bottom (disabled/default)
426+
pub fn bottom_buffer_fraction(&self) -> f32 {
427+
self.bottom_buffer_fraction
428+
}
429+
417430
pub fn toggle_autoformat(&mut self) {
418431
self.autoformat_enabled = !self.autoformat_enabled;
419432
}
@@ -519,6 +532,7 @@ pub struct PromptRuntimeConfigBuilder {
519532
spinner_config: SpinnerConfig,
520533
vi_config: ViConfig,
521534
vi_colors: ViColorConfig,
535+
bottom_buffer_fraction: f32,
522536
}
523537

524538
impl PromptRuntimeConfigBuilder {
@@ -549,6 +563,7 @@ impl PromptRuntimeConfigBuilder {
549563
spinner_config: SpinnerConfig::default(),
550564
vi_config: ViConfig::default(),
551565
vi_colors: ViColorConfig::default(),
566+
bottom_buffer_fraction: 1.0, // Disabled by default
552567
}
553568
}
554569

@@ -616,6 +631,11 @@ impl PromptRuntimeConfigBuilder {
616631
self
617632
}
618633

634+
pub fn bottom_buffer_fraction(mut self, fraction: f32) -> Self {
635+
self.bottom_buffer_fraction = fraction.clamp(0.0, 1.0);
636+
self
637+
}
638+
619639
pub fn build(self) -> PromptRuntimeConfig {
620640
// Initialize spinner in arf-libr
621641
arf_libr::set_spinner_frames(&self.spinner_config.frames);
@@ -647,6 +667,7 @@ impl PromptRuntimeConfigBuilder {
647667
spinner_config: self.spinner_config,
648668
vi_config: self.vi_config,
649669
vi_colors: self.vi_colors,
670+
bottom_buffer_fraction: self.bottom_buffer_fraction,
650671
}
651672
}
652673
}

0 commit comments

Comments
 (0)