aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/sequence/src/ui
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-02-24 13:10:40 +0100
committerGitHub <noreply@github.com>2026-02-24 13:10:40 +0100
commit8cd6286c3ffc10d14fdafd5005fa89b0f7fdcd44 (patch)
treec6341f5e3e251ae17db0c3427ddaf1e54d4fa91c /default-plugins/sequence/src/ui
parentAllow closing replaced panes when opening panes in place (#4760) (diff)
downloadsl0ppij-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.rs567
-rw-r--r--default-plugins/sequence/src/ui/layout_calculations.rs28
-rw-r--r--default-plugins/sequence/src/ui/mod.rs3
-rw-r--r--default-plugins/sequence/src/ui/truncation.rs373
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);
- }
-}