diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-02-24 13:10:40 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-24 13:10:40 +0100 |
| commit | 8cd6286c3ffc10d14fdafd5005fa89b0f7fdcd44 (patch) | |
| tree | c6341f5e3e251ae17db0c3427ddaf1e54d4fa91c /default-plugins/sequence/src/ui | |
| parent | Allow closing replaced panes when opening panes in place (#4760) (diff) | |
| download | sl0ppij-8cd6286c3ffc10d14fdafd5005fa89b0f7fdcd44.tar.gz sl0ppij-8cd6286c3ffc10d14fdafd5005fa89b0f7fdcd44.tar.xz sl0ppij-8cd6286c3ffc10d14fdafd5005fa89b0f7fdcd44.zip | |
remove sequence plugin (#4761)
Diffstat (limited to 'default-plugins/sequence/src/ui')
| -rw-r--r-- | default-plugins/sequence/src/ui/components.rs | 567 | ||||
| -rw-r--r-- | default-plugins/sequence/src/ui/layout_calculations.rs | 28 | ||||
| -rw-r--r-- | default-plugins/sequence/src/ui/mod.rs | 3 | ||||
| -rw-r--r-- | default-plugins/sequence/src/ui/truncation.rs | 373 |
4 files changed, 0 insertions, 971 deletions
diff --git a/default-plugins/sequence/src/ui/components.rs b/default-plugins/sequence/src/ui/components.rs deleted file mode 100644 index bc319567..00000000 --- a/default-plugins/sequence/src/ui/components.rs +++ /dev/null @@ -1,567 +0,0 @@ -use crate::path_formatting::format_cwd; -use crate::state::{CommandStatus, State}; -use crate::ui::truncation::{calculate_available_cmd_width, truncate_middle}; -use std::path::PathBuf; -use zellij_tile::prelude::*; - -const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"]; - -const INTRO_PART1: &str = "Paste a series of commands to run in sequence"; -const INTRO_PART2: &str = "or <e> to edit in your editor"; -const INTRO_FULL: &str = - "Paste a series of commands to run in sequence or <e> to edit in your editor"; - -const HELP_RUNNING_WITH_SELECTION: &str = "<Ctrl c> - interrupt, <Enter> - run from selected"; -const HELP_RUNNING_NO_SELECTION: &str = "<Ctrl c> - interrupt, <↓↑> - navigate"; -const HELP_STOPPED_WITH_SELECTION: &str = - "<Ctrl w> - close all, <Enter> - run from selected, <e> - edit"; -const HELP_STOPPED_NO_SELECTION: &str = - "<Ctrl w> - close all, <↓↑> - navigate, <Enter> - run from first"; - -const HELP_PENDING: &str = "<e> - edit, <Enter> - run"; - -fn select_help_text(sequence: &State) -> &'static str { - let is_running = sequence - .execution - .all_commands - .iter() - .any(|command| matches!(command.get_status(), CommandStatus::Running(_))); - let all_pending = sequence - .execution - .all_commands - .iter() - .all(|command| matches!(command.get_status(), CommandStatus::Pending)); - let has_selection = sequence.selection.current_selected_command_index.is_some(); - - if all_pending && !sequence.execution.is_running { - HELP_PENDING - } else if is_running { - if has_selection { - HELP_RUNNING_WITH_SELECTION - } else { - HELP_RUNNING_NO_SELECTION - } - } else { - if has_selection { - HELP_STOPPED_WITH_SELECTION - } else { - HELP_STOPPED_NO_SELECTION - } - } -} - -fn style_help_text(text: &str) -> Text { - Text::new(text) - .color_substring(3, "<Ctrl c>") - .color_substring(3, "<Ctrl w>") - .color_substring(3, "<↓↑>") - .color_substring(3, "<Esc>") - .color_substring(3, "<Enter>") - .color_substring(3, "<e>") -} - -pub fn render_help_lines( - sequence: &State, - max_width: Option<usize>, -) -> (Text, usize, Option<(Text, usize)>) { - let help_text = select_help_text(sequence); - - if let Some(width) = max_width { - if let Some((first_line, second_line)) = split_help_line(help_text, width) { - let first_len = first_line.chars().count(); - let first_styled = style_help_text(&first_line).unbold_all(); - - let second_line_width = second_line.chars().count(); - let (second_styled, second_len) = if second_line_width > width { - let (truncated, len) = truncate_help_line(&second_line, width); - (style_help_text(&truncated).unbold_all(), len) - } else { - ( - style_help_text(&second_line).unbold_all(), - second_line_width, - ) - }; - - return (first_styled, first_len, Some((second_styled, second_len))); - } - - let (truncated_text, help_len) = truncate_help_line(help_text, width); - ( - style_help_text(&truncated_text).unbold_all(), - help_len, - None, - ) - } else { - let help_len = help_text.chars().count(); - (style_help_text(help_text).unbold_all(), help_len, None) - } -} - -/// Returns `(text, x, y)` tuples for the main screen intro hint. -/// The text is centered horizontally; if it does not fit on one line, two lines -/// are returned separated by one blank line (1-line padding). -pub fn render_intro_hint(rows: usize, cols: usize) -> Vec<(Text, usize, usize)> { - let full_len = INTRO_FULL.chars().count(); - - if full_len <= cols { - let x = cols.saturating_sub(full_len) / 2; - let y = rows / 2; - vec![(Text::new(INTRO_FULL).color_substring(3, "<e>"), x, y)] - } else { - let p1_len = INTRO_PART1.chars().count(); - let p2_len = INTRO_PART2.chars().count(); - let x1 = cols.saturating_sub(p1_len) / 2; - let x2 = cols.saturating_sub(p2_len) / 2; - // 3 display rows: line1, padding, line2 — center the group vertically - let start_y = rows.saturating_sub(3) / 2; - vec![ - ( - Text::new(INTRO_PART1).color_substring(3, "<e>"), - x1, - start_y, - ), - ( - Text::new(INTRO_PART2).color_substring(3, "<e>"), - x2, - start_y + 2, - ), - ] - } -} - -fn split_help_line(help_text: &str, max_width: usize) -> Option<(String, String)> { - let text_width = help_text.chars().count(); - - if text_width <= max_width { - return None; - } - - let keybindings = parse_help_keybindings(help_text); - - if keybindings.is_empty() { - return None; - } - - for split_point in 1..keybindings.len() { - let first_part: Vec<String> = keybindings - .iter() - .take(split_point) - .map(|(key, _, action)| format!("{} - {}", key, action)) - .collect(); - let first_line = first_part.join(", "); - - let second_part: Vec<String> = keybindings - .iter() - .skip(split_point) - .map(|(key, _, action)| format!("{} - {}", key, action)) - .collect(); - let second_line = second_part.join(", "); - - if first_line.chars().count() <= max_width && second_line.chars().count() <= max_width { - return Some((first_line, second_line)); - } - } - - None -} - -fn folder_cell(cwd: &Option<PathBuf>) -> (Text, String) { - let cwd_display = if let Some(cwd) = cwd { - format_cwd(cwd) - } else { - "~".to_string() - }; - let folder_display = format!("{} >", cwd_display); - let text = Text::new(&folder_display).color_range(0, 0..cwd_display.len()); - (text, folder_display) -} - -fn command_cell(text: &str) -> Text { - let text = if text.is_empty() { " " } else { text }; - Text::new(text) -} - -fn chain_cell(chain_type: &crate::state::ChainType) -> Text { - let text = chain_type_to_str(chain_type); - if !text.is_empty() { - Text::new(text).color_range(1, 0..text.len()) - } else { - Text::new(text) - } -} - -pub fn get_spinner_frame(frame: usize) -> &'static str { - SPINNER_FRAMES[frame % SPINNER_FRAMES.len()] -} - -pub fn format_status_text(status: &CommandStatus, spinner_frame: usize) -> String { - match status { - CommandStatus::Running(_) => { - let spinner = get_spinner_frame(spinner_frame); - format!("[RUNNING] {}", spinner) - }, - CommandStatus::Interrupted(_) => "[INTERRUPTED]".to_string(), - CommandStatus::Exited(Some(code), _) => format!("[EXIT CODE: {}]", code), - CommandStatus::Exited(None, _) => "[EXITED]".to_string(), - CommandStatus::Pending => " ".to_string(), - } -} - -fn apply_status_color(text: Text, status: &CommandStatus, status_str: &str) -> Text { - match status { - CommandStatus::Running(_) => text.color_range(3, 0..status_str.len()), - CommandStatus::Interrupted(_) => text.error_color_range(0..status_str.len()), - CommandStatus::Exited(Some(0), _) => { - let number_start = 12; - let number_end = status_str.len() - 1; - text.success_color_range(number_start..number_end) - }, - CommandStatus::Exited(Some(_), _) => { - let number_start = 12; - let number_end = status_str.len() - 1; - text.error_color_range(number_start..number_end) - }, - _ => text, - } -} - -fn status_cell(status: &CommandStatus, spinner_frame: usize) -> Text { - let status_str = format_status_text(status, spinner_frame); - let text = Text::new(&status_str); - apply_status_color(text, status, &status_str) -} - -fn apply_row_styles(cells: Vec<Text>, is_selected: bool) -> Vec<Text> { - let mut styled_cells = cells; - - if is_selected { - styled_cells = styled_cells.into_iter().map(|c| c.selected()).collect(); - } - - styled_cells -} - -fn overflow_cell(indicator: &str, is_selected: bool) -> Text { - let mut text = Text::new(indicator).dim_all(); - if is_selected { - text = text.selected(); - } - text -} - -fn command_sequence_row( - state: &State, - cwd: &Option<PathBuf>, - truncated_cmd_text: &str, - chain_type: &crate::state::ChainType, - status: &crate::state::CommandStatus, - is_selected: bool, - overflow_indicator: Option<String>, -) -> (Vec<Text>, usize) { - let (folder_text, folder_display) = folder_cell(cwd); - - let mut cells = vec![ - folder_text, - command_cell(truncated_cmd_text), - chain_cell(chain_type), - status_cell(status, state.layout.spinner_frame), - ]; - - cells = apply_row_styles(cells, is_selected); - - let truncated_cmd_text = if truncated_cmd_text.is_empty() { - " " - } else { - truncated_cmd_text - }; - let mut row_length = folder_display.chars().count() - + truncated_cmd_text.chars().count() - + chain_type_to_str(chain_type).chars().count() - + format_status_text(status, state.layout.spinner_frame) - .chars() - .count() - + 3; - - if let Some(indicator) = overflow_indicator { - let indicator_len = indicator.chars().count(); - cells.push(overflow_cell(&indicator, is_selected)); - row_length += indicator_len + 1; - } - - (cells, row_length) -} - -pub fn build_table_header(has_overflow: bool) -> Table { - let header_cols = if has_overflow { - vec![" ", " ", " ", " ", " "] - } else { - vec![" ", " ", " ", " "] - }; - Table::new().add_row(header_cols) -} - -fn calculate_overflow_indicator( - visible_idx: usize, - visible_count: usize, - hidden_above: usize, - hidden_below: usize, -) -> Option<String> { - if visible_idx == 0 && hidden_above > 0 { - Some(format!("[+{}]", hidden_above)) - } else if visible_idx == visible_count.saturating_sub(1) && hidden_below > 0 { - Some(format!("[+{}]", hidden_below)) - } else if hidden_above > 0 || hidden_below > 0 { - Some(format!(" ")) - } else { - None - } -} - -pub fn calculate_row_layout_info( - index: usize, - offset: usize, - visible_count: usize, - hidden_above: usize, - hidden_below: usize, - cols: usize, - folder_width: usize, - max_chain_width: usize, - max_status_width: usize, -) -> Option<(Option<String>, usize)> { - if index < offset || index >= offset + visible_count { - return None; - } - - let visible_idx = index.saturating_sub(offset); - let overflow_indicator = - calculate_overflow_indicator(visible_idx, visible_count, hidden_above, hidden_below); - - let available_cmd_width = calculate_available_cmd_width( - cols, - folder_width, - overflow_indicator.as_ref(), - max_chain_width, - max_status_width, - ); - - Some((overflow_indicator, available_cmd_width)) -} - -pub fn add_command_row( - table: Table, - state: &State, - index: usize, - offset: usize, - visible_count: usize, - hidden_above: usize, - hidden_below: usize, -) -> Table { - let command = match state.execution.all_commands.get(index) { - Some(cmd) => cmd, - None => return table, - }; - - let cmd_text = command.get_text(); - let cmd_text = cmd_text.trim(); - let chain_type = &command.get_chain_type(); - let status = &command.get_status(); - let command_cwd = command.get_cwd().or_else(|| state.cwd.clone()); - - let cols = state.own_columns.unwrap_or(80); - let longest_cwd_display = state.execution.longest_cwd_display(&state.cwd); - let folder_display = format!("{} >", longest_cwd_display); - let folder_width = folder_display.chars().count(); - - let (max_chain_width, max_status_width) = - calculate_max_widths(&state.execution.all_commands, state.layout.spinner_frame); - - let Some((overflow_indicator, available_cmd_width)) = calculate_row_layout_info( - index, - offset, - visible_count, - hidden_above, - hidden_below, - cols, - folder_width, - max_chain_width, - max_status_width, - ) else { - return table; - }; - - let truncated_cmd_text = truncate_middle(cmd_text, available_cmd_width, None).0; - - let (row, _) = command_sequence_row( - state, - &command_cwd, - &truncated_cmd_text, - chain_type, - status, - state.selection.current_selected_command_index == Some(index), - overflow_indicator, - ); - - table.add_styled_row(row) -} - -pub fn calculate_max_widths( - commands: &[crate::state::CommandEntry], - spinner_frame: usize, -) -> (usize, usize) { - let max_chain_width = commands - .iter() - .map(|cmd| chain_type_to_str(&cmd.get_chain_type()).chars().count()) - .max() - .unwrap_or(0); - - let max_status_width = commands - .iter() - .map(|cmd| { - format_status_text(&cmd.get_status(), spinner_frame) - .chars() - .count() - }) - .max() - .unwrap_or(0); - - (max_chain_width, max_status_width) -} - -pub fn calculate_longest_line( - longest_cwd_display: &str, - longest_command: usize, - max_chain_width: usize, - max_status_width: usize, -) -> usize { - let folder_display = format!("{} >", longest_cwd_display); - let cell_padding = 3; - folder_display.chars().count() - + longest_command - + max_chain_width - + max_status_width - + cell_padding -} - -pub fn chain_type_to_str(chain_type: &crate::state::ChainType) -> &'static str { - match chain_type { - crate::state::ChainType::And => "AND", - crate::state::ChainType::Or => "OR", - crate::state::ChainType::Then => "THEN", - crate::state::ChainType::None => " ", - } -} - -pub fn truncate_help_line(help_text: &str, max_width: usize) -> (String, usize) { - let text_width = help_text.chars().count(); - - if text_width <= max_width { - return (help_text.to_string(), text_width); - } - - let keybindings = parse_help_keybindings(help_text); - - if let Some(result) = try_shortened_help(&keybindings, max_width) { - return result; - } - - if let Some(result) = try_keys_spaced(&keybindings, max_width) { - return result; - } - - if let Some(result) = try_keys_tight(&keybindings, max_width) { - return result; - } - - truncate_with_ellipsis( - &keybindings - .iter() - .map(|(k, _, _)| *k) - .collect::<Vec<_>>() - .join("/"), - max_width, - ) -} - -fn parse_help_keybindings(help_text: &str) -> Vec<(&str, &str, &str)> { - help_text - .split(", ") - .filter_map(|part| { - part.find(" - ").map(|dash_pos| { - let key = &part[..dash_pos]; - let action = &part[dash_pos + 3..]; - let first_word = action.split_whitespace().next().unwrap_or(action); - (key, first_word, action) - }) - }) - .collect() -} - -fn try_shortened_help( - keybindings: &[(&str, &str, &str)], - max_width: usize, -) -> Option<(String, usize)> { - let shortened: Vec<String> = keybindings - .iter() - .map(|(key, first_word, _)| format!("{} - {}", key, first_word)) - .collect(); - let shortened_text = shortened.join(", "); - let shortened_width = shortened_text.chars().count(); - - if shortened_width <= max_width { - Some((shortened_text, shortened_width)) - } else { - None - } -} - -fn try_keys_spaced( - keybindings: &[(&str, &str, &str)], - max_width: usize, -) -> Option<(String, usize)> { - let keys_spaced_text = keybindings - .iter() - .map(|(key, _, _)| *key) - .collect::<Vec<_>>() - .join(" / "); - let keys_spaced_width = keys_spaced_text.chars().count(); - - if keys_spaced_width <= max_width { - Some((keys_spaced_text, keys_spaced_width)) - } else { - None - } -} - -fn try_keys_tight(keybindings: &[(&str, &str, &str)], max_width: usize) -> Option<(String, usize)> { - let keys_tight_text = keybindings - .iter() - .map(|(key, _, _)| *key) - .collect::<Vec<_>>() - .join("/"); - let keys_tight_width = keys_tight_text.chars().count(); - - if keys_tight_width <= max_width { - Some((keys_tight_text, keys_tight_width)) - } else { - None - } -} - -fn truncate_with_ellipsis(text: &str, max_width: usize) -> (String, usize) { - let available = max_width.saturating_sub(3); - let mut truncated = String::new(); - let mut current_width = 0; - - for ch in text.chars() { - if current_width + 1 <= available { - truncated.push(ch); - current_width += 1; - } else { - break; - } - } - - truncated.push_str("..."); - let truncated_width = truncated.chars().count(); - (truncated, truncated_width) -} diff --git a/default-plugins/sequence/src/ui/layout_calculations.rs b/default-plugins/sequence/src/ui/layout_calculations.rs deleted file mode 100644 index 911dbc62..00000000 --- a/default-plugins/sequence/src/ui/layout_calculations.rs +++ /dev/null @@ -1,28 +0,0 @@ -pub fn calculate_viewport( - total_commands: usize, - max_visible: usize, - selected_index: Option<usize>, - running_index: usize, -) -> (usize, usize, usize, usize) { - if total_commands <= max_visible { - return (0, total_commands, 0, 0); - } - - let focus_index = selected_index.unwrap_or(running_index); - let half_visible = max_visible / 2; - - let offset = if focus_index < half_visible { - 0 - } else if focus_index >= total_commands.saturating_sub(half_visible) { - total_commands.saturating_sub(max_visible) - } else { - focus_index.saturating_sub(half_visible) - }; - - let offset = offset.min(total_commands.saturating_sub(max_visible)); - let visible_count = max_visible.min(total_commands.saturating_sub(offset)); - let hidden_above = offset; - let hidden_below = total_commands.saturating_sub(offset + visible_count); - - (offset, visible_count, hidden_above, hidden_below) -} diff --git a/default-plugins/sequence/src/ui/mod.rs b/default-plugins/sequence/src/ui/mod.rs deleted file mode 100644 index 7255fa35..00000000 --- a/default-plugins/sequence/src/ui/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod components; -pub mod layout_calculations; -pub mod truncation; diff --git a/default-plugins/sequence/src/ui/truncation.rs b/default-plugins/sequence/src/ui/truncation.rs deleted file mode 100644 index 0c4073a3..00000000 --- a/default-plugins/sequence/src/ui/truncation.rs +++ /dev/null @@ -1,373 +0,0 @@ -use unicode_width::UnicodeWidthStr; - -const BASE_SEPARATOR_WIDTH: usize = 2; - -pub fn calculate_available_cmd_width( - cols: usize, - folder_width: usize, - overflow_indicator: Option<&String>, - chain_width: usize, - status_width: usize, -) -> usize { - let mut separator_width = BASE_SEPARATOR_WIDTH; - if overflow_indicator.is_some() { - separator_width += 1; - } - if status_width > 0 { - separator_width += 1; - } - - let overflow_width = overflow_indicator.map(|s| s.chars().count()).unwrap_or(0); - - cols.saturating_sub(folder_width) - .saturating_sub(chain_width) - .saturating_sub(status_width) - .saturating_sub(separator_width) - .saturating_sub(overflow_width) - .saturating_sub(2) - .max(1) -} - -pub fn truncate_middle( - text: &str, - max_width: usize, - cursor_position: Option<usize>, -) -> (String, Option<usize>) { - let text_width = text.width(); - - if text_width <= max_width { - return (text.to_string(), cursor_position); - } - - if max_width < 5 { - return truncate_minimal(text, max_width, cursor_position); - } - - if let Some(cursor_char_idx) = cursor_position { - return truncate_with_cursor(text, max_width, cursor_char_idx); - } - - truncate_no_cursor(text, max_width) -} - -fn truncate_minimal( - text: &str, - max_width: usize, - cursor_position: Option<usize>, -) -> (String, Option<usize>) { - let mut result = String::new(); - let mut current_width = 0; - let mut new_cursor_pos = None; - let mut char_pos = 0; - - for ch in text.chars() { - let ch_width = ch.to_string().width(); - if current_width + ch_width <= max_width { - result.push(ch); - if let Some(cursor_char) = cursor_position { - if char_pos == cursor_char { - new_cursor_pos = Some(char_pos); - } - } - char_pos += 1; - current_width += ch_width; - } else { - break; - } - } - (result, new_cursor_pos) -} - -fn truncate_no_cursor(text: &str, max_width: usize) -> (String, Option<usize>) { - let available_for_text = max_width.saturating_sub(3); - let left_width = available_for_text / 2; - let right_width = available_for_text - left_width; - - let mut left_part = String::new(); - let mut current_width = 0; - for ch in text.chars() { - let ch_width = ch.to_string().width(); - if current_width + ch_width <= left_width { - left_part.push(ch); - current_width += ch_width; - } else { - break; - } - } - - let chars: Vec<char> = text.chars().collect(); - let mut right_part = String::new(); - let mut current_width = 0; - for ch in chars.iter().rev() { - let ch_width = ch.to_string().width(); - if current_width + ch_width <= right_width { - right_part.insert(0, *ch); - current_width += ch_width; - } else { - break; - } - } - - (format!("{}...{}", left_part, right_part), None) -} - -fn truncate_with_cursor( - text: &str, - max_width: usize, - cursor_char_idx: usize, -) -> (String, Option<usize>) { - let chars: Vec<char> = text.chars().collect(); - let char_widths: Vec<usize> = chars.iter().map(|ch| ch.to_string().width()).collect(); - - let width_before_cursor: usize = char_widths[..cursor_char_idx].iter().sum(); - let width_after_cursor: usize = char_widths[cursor_char_idx..].iter().sum(); - let available_one_ellipsis = max_width.saturating_sub(3); - - let (start_idx, end_idx) = if width_before_cursor <= available_one_ellipsis { - calculate_end_truncation(&chars, &char_widths, available_one_ellipsis) - } else if width_after_cursor <= available_one_ellipsis { - calculate_start_truncation(&chars, &char_widths, available_one_ellipsis) - } else { - calculate_middle_truncation(&char_widths, max_width, cursor_char_idx) - }; - - let (start_idx, end_idx) = - adjust_small_truncations(start_idx, end_idx, &char_widths, cursor_char_idx, &chars); - let (start_idx, end_idx) = - trim_excess(&char_widths, start_idx, end_idx, max_width, cursor_char_idx); - - build_truncated_result(&chars, start_idx, end_idx, cursor_char_idx) -} - -fn calculate_end_truncation( - chars: &[char], - char_widths: &[usize], - available: usize, -) -> (usize, usize) { - let mut end_idx = 0; - let mut width = 0; - while end_idx < chars.len() && width + char_widths[end_idx] <= available { - width += char_widths[end_idx]; - end_idx += 1; - } - (0, end_idx) -} - -fn calculate_start_truncation( - chars: &[char], - char_widths: &[usize], - available: usize, -) -> (usize, usize) { - let mut start_idx = chars.len(); - let mut width = 0; - while start_idx > 0 && width + char_widths[start_idx - 1] <= available { - start_idx -= 1; - width += char_widths[start_idx]; - } - (start_idx, chars.len()) -} - -fn calculate_middle_truncation( - char_widths: &[usize], - max_width: usize, - cursor_char_idx: usize, -) -> (usize, usize) { - let available_both_ellipsis = max_width.saturating_sub(6); - let target_before = available_both_ellipsis / 2; - let target_after = available_both_ellipsis - target_before; - - let mut start_idx = cursor_char_idx; - let mut width_before = 0; - while start_idx > 0 && width_before + char_widths[start_idx - 1] <= target_before { - start_idx -= 1; - width_before += char_widths[start_idx]; - } - - let mut end_idx = cursor_char_idx; - let mut width_after = 0; - while end_idx < char_widths.len() && width_after + char_widths[end_idx] <= target_after { - width_after += char_widths[end_idx]; - end_idx += 1; - } - - let leftover_before = target_before.saturating_sub(width_before); - let leftover_after = target_after.saturating_sub(width_after); - - if leftover_before > 0 { - let mut extra = leftover_before; - while end_idx < char_widths.len() && char_widths[end_idx] <= extra { - extra -= char_widths[end_idx]; - end_idx += 1; - } - } - if leftover_after > 0 { - let mut extra = leftover_after; - while start_idx > 0 && char_widths[start_idx - 1] <= extra { - start_idx -= 1; - extra -= char_widths[start_idx]; - } - } - - (start_idx, end_idx) -} - -fn adjust_small_truncations( - start_idx: usize, - end_idx: usize, - char_widths: &[usize], - cursor_char_idx: usize, - chars: &[char], -) -> (usize, usize) { - let width_truncated_start: usize = char_widths[..start_idx].iter().sum(); - let width_truncated_end: usize = char_widths[end_idx..].iter().sum(); - - let mut start_idx = start_idx; - let mut end_idx = end_idx; - - if width_truncated_start > 0 && width_truncated_start < 3 && end_idx < chars.len() { - let gained = width_truncated_start; - start_idx = 0; - let mut removed = 0; - while end_idx > cursor_char_idx + 1 && removed < gained { - end_idx -= 1; - removed += char_widths[end_idx]; - } - } else if width_truncated_end > 0 && width_truncated_end < 3 && start_idx > 0 { - let gained = width_truncated_end; - end_idx = chars.len(); - let mut removed = 0; - while start_idx < cursor_char_idx && removed < gained { - removed += char_widths[start_idx]; - start_idx += 1; - } - } - - (start_idx, end_idx) -} - -fn trim_excess( - char_widths: &[usize], - start_idx: usize, - end_idx: usize, - max_width: usize, - cursor_char_idx: usize, -) -> (usize, usize) { - let truncate_start = start_idx > 0; - let truncate_end = end_idx < char_widths.len(); - let ellipsis_width = match (truncate_start, truncate_end) { - (true, true) => 6, - (true, false) | (false, true) => 3, - (false, false) => 0, - }; - - let visible_width: usize = char_widths[start_idx..end_idx].iter().sum(); - let mut start_idx = start_idx; - let mut end_idx = end_idx; - - if visible_width + ellipsis_width > max_width { - let mut excess = visible_width + ellipsis_width - max_width; - if truncate_end { - while excess > 0 && end_idx > cursor_char_idx + 1 { - end_idx -= 1; - excess = excess.saturating_sub(char_widths[end_idx]); - } - } - if excess > 0 && truncate_start { - while excess > 0 && start_idx < cursor_char_idx { - excess = excess.saturating_sub(char_widths[start_idx]); - start_idx += 1; - } - } - } - - (start_idx, end_idx) -} - -fn build_truncated_result( - chars: &[char], - start_idx: usize, - end_idx: usize, - cursor_char_idx: usize, -) -> (String, Option<usize>) { - let mut result = String::new(); - let mut new_cursor_char_pos = 0; - let mut current_char_pos = 0; - - if start_idx > 0 { - result.push_str("..."); - current_char_pos = 3; - } - - for i in start_idx..end_idx { - if i == cursor_char_idx { - new_cursor_char_pos = current_char_pos; - } - result.push(chars[i]); - current_char_pos += 1; - } - - if cursor_char_idx >= end_idx { - new_cursor_char_pos = current_char_pos; - } - - if end_idx < chars.len() { - result.push_str("..."); - } - - (result, Some(new_cursor_char_pos)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_truncate_middle_no_truncation_needed() { - let text = "hello world"; - let (result, cursor_pos) = truncate_middle(text, 20, Some(6)); - assert_eq!(result, "hello world"); - assert_eq!(cursor_pos, Some(6)); - } - - #[test] - fn test_truncate_middle_no_cursor() { - let text = "this is a very long string that needs truncation"; - let (result, cursor_pos) = truncate_middle(text, 20, None); - assert!(result.contains("...")); - assert_eq!(cursor_pos, None); - } - - #[test] - fn test_truncate_middle_cursor_at_start() { - let text = "this is a very long string that needs truncation"; - let (result, cursor_pos) = truncate_middle(text, 20, Some(0)); - assert!(result.starts_with("this")); - assert!(result.ends_with("...")); - assert_eq!(cursor_pos, Some(0)); - } - - #[test] - fn test_truncate_middle_cursor_at_end() { - let text = "this is a very long string that needs truncation"; - let cursor_char = text.chars().count(); - let (result, _cursor_pos) = truncate_middle(text, 20, Some(cursor_char)); - assert!(result.starts_with("...")); - assert!(result.ends_with("truncation")); - } - - #[test] - fn test_truncate_middle_cursor_in_middle() { - let text = "this is a very long string that needs truncation"; - let cursor_char = "this is a very ".chars().count(); - let (result, cursor_pos) = truncate_middle(text, 20, Some(cursor_char)); - assert!(result.contains("very")); - assert!(cursor_pos.is_some()); - } - - #[test] - fn test_truncate_middle_wide_chars() { - let text = "こんにちは世界"; - let (result, _) = truncate_middle(text, 10, None); - assert!(result.width() <= 10); - } -} |
