feat(config): add support for custom input prompt in user config (#648)

This commit is contained in:
Alex Flores 2025-07-18 10:16:51 -05:00 committed by GitHub
parent 28f58e0641
commit e809ccf686
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 132 additions and 3 deletions

View File

@ -56,6 +56,8 @@ ui_scale = 100
# Where to place the input bar in the UI (top or bottom)
input_bar_position = "top"
# The input prompt string (defaults to ">" if not specified)
input_prompt = ">"
# What orientation should tv be (landscape or portrait)
orientation = "landscape"
# The theme to use for the UI

View File

@ -379,6 +379,8 @@ pub struct UiSpec {
pub input_bar_position: Option<InputPosition>,
#[serde(default)]
pub input_header: Option<Template>,
#[serde(default)]
pub input_prompt: Option<String>,
// Feature-specific configurations
#[serde(default)]
pub preview_panel: Option<ui::PreviewPanelConfig>,
@ -400,6 +402,7 @@ impl From<&crate::config::UiConfig> for UiSpec {
orientation: Some(config.orientation),
input_bar_position: Some(config.input_bar_position),
input_header: config.input_header.clone(),
input_prompt: Some(config.input_prompt.clone()),
preview_panel: Some(config.preview_panel.clone()),
status_bar: Some(config.status_bar.clone()),
help_panel: Some(config.help_panel.clone()),

View File

@ -141,6 +141,16 @@ pub struct Cli {
#[arg(long = "input-header", value_name = "STRING", verbatim_doc_comment)]
pub input_header: Option<String>,
/// Input prompt string
///
/// When a channel is specified: This overrides the prompt defined in the channel prototype.
/// When no channel is specified: Sets the input prompt for the ad-hoc channel.
///
/// The given value is used as the prompt string shown before the input field.
/// Defaults to ">" when omitted.
#[arg(long = "input-prompt", value_name = "STRING", verbatim_doc_comment)]
pub input_prompt: Option<String>,
/// Preview header template
///
/// When a channel is specified: This overrides the header defined in the channel prototype.

View File

@ -80,6 +80,7 @@ pub struct PostProcessedCli {
// Input configuration
pub input: Option<String>,
pub input_header: Option<String>,
pub input_prompt: Option<String>,
// UI and layout configuration
pub layout: Option<Orientation>,
@ -150,6 +151,7 @@ impl Default for PostProcessedCli {
// Input configuration
input: None,
input_header: None,
input_prompt: None,
// UI and layout configuration
layout: None,
@ -340,6 +342,7 @@ pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
// Input configuration
input: cli.input,
input_header: cli.input_header,
input_prompt: cli.input_prompt,
// UI and layout configuration
layout,

View File

@ -266,6 +266,11 @@ impl Config {
self.ui.input_header = Some(value.clone());
}
// Apply input_prompt
if let Some(value) = &ui_spec.input_prompt {
self.ui.input_prompt.clone_from(value);
}
// Handle preview_panel with field merging
if let Some(preview_panel) = &ui_spec.preview_panel {
self.ui.preview_panel.size = preview_panel.size;
@ -481,6 +486,30 @@ mod tests {
);
}
const USER_CONFIG_INPUT_PROMPT: &str = r#"
[ui]
input_prompt = ""
"#;
#[test]
fn test_config_input_prompt_from_user_cfg() {
// write user config to a file
let dir = tempdir().unwrap();
let config_dir = dir.path();
let config_file = config_dir.join(CONFIG_FILE_NAME);
let mut file = File::create(&config_file).unwrap();
file.write_all(USER_CONFIG_INPUT_PROMPT.as_bytes()).unwrap();
let config_env = ConfigEnv {
_data_dir: get_data_dir(),
config_dir: config_dir.to_path_buf(),
};
let config = Config::new(&config_env, None).unwrap();
// Verify that input_prompt was loaded from user config
assert_eq!(config.ui.input_prompt, "");
}
#[test]
fn test_setting_user_shell_integration_triggers_overrides_default() {
let user_config = r#"

View File

@ -108,6 +108,8 @@ pub struct UiConfig {
pub orientation: Orientation,
pub theme: String,
pub input_header: Option<Template>,
#[serde(default = "default_input_prompt")]
pub input_prompt: String,
pub features: Features,
// Feature-specific configurations
@ -121,6 +123,11 @@ pub struct UiConfig {
pub theme_overrides: ThemeOverrides,
}
const DEFAULT_INPUT_PROMPT: &str = ">";
fn default_input_prompt() -> String {
String::from(DEFAULT_INPUT_PROMPT)
}
impl Default for UiConfig {
fn default() -> Self {
Self {
@ -130,6 +137,7 @@ impl Default for UiConfig {
orientation: Orientation::Landscape,
theme: String::from(DEFAULT_THEME),
input_header: None,
input_prompt: String::from(DEFAULT_INPUT_PROMPT),
features: Features::default(),
status_bar: StatusBarConfig::default(),
preview_panel: PreviewPanelConfig::default(),

View File

@ -197,6 +197,8 @@ pub fn draw(ctx: Box<Ctx>, f: &mut Frame<'_>, area: Rect) -> Result<Layout> {
)?;
// input box
let input_prompt = ctx.config.ui.input_prompt.clone();
draw_input_box(
f,
layout.input,
@ -209,6 +211,7 @@ pub fn draw(ctx: Box<Ctx>, f: &mut Frame<'_>, area: Rect) -> Result<Layout> {
&ctx.tv_state.spinner,
&ctx.colorscheme,
&ctx.config.ui.input_header,
&input_prompt,
&ctx.config.ui.input_bar_position,
)?;

View File

@ -202,6 +202,9 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
config.ui.input_header = Some(t);
}
}
if let Some(input_prompt) = &args.input_prompt {
config.ui.input_prompt.clone_from(input_prompt);
}
if let Some(preview_header) = &args.preview_header {
if let Ok(t) = Template::parse(preview_header) {
config.ui.preview_panel.header = Some(t);
@ -372,6 +375,7 @@ fn apply_ui_overrides(
orientation: None,
input_bar_position: None,
input_header: None,
input_prompt: None,
preview_panel: None,
status_bar: None,
help_panel: None,
@ -386,6 +390,12 @@ fn apply_ui_overrides(
}
}
// Apply input prompt override
if let Some(input_prompt_str) = &args.input_prompt {
ui_spec.input_prompt = Some(input_prompt_str.clone());
ui_changes_needed = true;
}
// Apply layout/orientation override
if let Some(layout) = args.layout {
ui_spec.orientation = Some(layout);
@ -737,6 +747,7 @@ mod tests {
orientation: Some(Orientation::Portrait),
input_bar_position: None,
input_header: Some(Template::parse("Original Header").unwrap()),
input_prompt: None,
preview_panel: Some(television::config::ui::PreviewPanelConfig {
size: 50,
header: Some(

View File

@ -29,6 +29,7 @@ pub fn draw_input_box(
spinner: &Spinner,
colorscheme: &Colorscheme,
input_header: &Option<Template>,
input_prompt: &str,
input_bar_position: &InputPosition,
) -> Result<()> {
let header = input_header
@ -64,8 +65,10 @@ pub fn draw_input_box(
let inner_input_chunks = RatatuiLayout::default()
.direction(Direction::Horizontal)
.constraints([
// prompt symbol
Constraint::Length(2),
// prompt symbol + space
Constraint::Length(
u16::try_from(input_prompt.chars().count() + 1).unwrap_or(2),
),
// input field
Constraint::Fill(1),
// result count
@ -81,7 +84,7 @@ pub fn draw_input_box(
let arrow_block = Block::default();
let arrow = Paragraph::new(Span::styled(
"> ",
format!("{} ", input_prompt),
Style::default().fg(colorscheme.input.input_fg).bold(),
))
.block(arrow_block);

View File

@ -89,6 +89,63 @@ fn test_input_header_in_adhoc_mode() {
PtyTester::assert_exit_ok(&mut child, DEFAULT_DELAY);
}
/// Tests that --input-prompt customizes the prompt symbol in Channel Mode.
#[test]
fn test_input_prompt_in_channel_mode() {
let mut tester = PtyTester::new();
// This overrides the channel's default input prompt with custom symbol
let mut cmd = tv_local_config_and_cable_with_args(&["files"]);
cmd.args(["--input-prompt", " "]);
let mut child = tester.spawn_command_tui(cmd);
// Verify the custom input prompt is displayed
tester.assert_tui_frame_contains(" ");
tester.assert_tui_frame_contains("CHANNEL files");
// Send Ctrl+C to exit
tester.send(&ctrl('c'));
PtyTester::assert_exit_ok(&mut child, DEFAULT_DELAY);
}
/// Tests that --input-prompt works in Ad-hoc Mode.
#[test]
fn test_input_prompt_in_adhoc_mode() {
let mut tester = PtyTester::new();
// This provides a custom input prompt for an ad-hoc channel
let mut cmd =
tv_local_config_and_cable_with_args(&["--source-command", "ls"]);
cmd.args(["--input-prompt", ""]);
let mut child = tester.spawn_command_tui(cmd);
// Verify the custom input prompt is displayed
tester.assert_tui_frame_contains("");
tester.assert_tui_frame_contains("CHANNEL custom");
// Send Ctrl+C to exit
tester.send(&ctrl('c'));
PtyTester::assert_exit_ok(&mut child, DEFAULT_DELAY);
}
/// Tests that the default input prompt "> " is used when no custom prompt is specified.
#[test]
fn test_default_input_prompt() {
let mut tester = PtyTester::new();
// This uses the default input prompt
let cmd = tv_local_config_and_cable_with_args(&["files"]);
let mut child = tester.spawn_command_tui(cmd);
// Verify the default input prompt is displayed
tester.assert_tui_frame_contains("> ");
tester.assert_tui_frame_contains("CHANNEL files");
// Send Ctrl+C to exit
tester.send(&ctrl('c'));
PtyTester::assert_exit_ok(&mut child, DEFAULT_DELAY);
}
/// Tests that --ui-scale adjusts the overall interface size.
#[test]
fn test_ui_scale() {