From ba68f268729fbd3c4b19fd03760a71e061450e4b Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Wed, 7 Oct 2026 14:06:22 +0900 Subject: [PATCH 1/2] Remove configurable selection cursor --- config.schema.json | 8 +- docs/src/configurations/config-file-format.md | 14 --- src/config.rs | 26 +---- src/ui/browser.rs | 71 ++++---------- src/ui/inspector.rs | 37 +------ src/ui/key_value.rs | 18 +--- src/ui/normalization.rs | 15 +-- src/ui/normalization_result.rs | 10 +- src/ui/render.rs | 64 ++----------- src/ui/search.rs | 77 ++------------- src/ui/sequence.rs | 96 +++++-------------- src/ui/settings.rs | 68 ------------- ...tests__browse_blocks_from_gap_minimum.snap | 22 ++--- ...render__tests__browse_blocks_standard.snap | 50 +++++----- ...__browse_planes_minimum_at_last_plane.snap | 22 ++--- ...render__tests__browse_planes_standard.snap | 34 +++---- ...s__browse_planes_wide_at_middle_plane.snap | 34 +++---- ...um_after_reversing_at_the_bottom_edge.snap | 22 ++--- ...browse_ranges_minimum_at_middle_range.snap | 22 ++--- ...render__tests__browse_ranges_standard.snap | 50 +++++----- ...sts__browse_ranges_wide_at_last_range.snap | 70 +++++++------- ...nder__tests__details_aliases_standard.snap | 40 ++++---- ...anonical_decomposition_minimum_at_end.snap | 16 ++-- ...nical_decomposition_minimum_at_middle.snap | 18 ++-- ...composition_standard_at_normalization.snap | 40 ++++---- ...ails_compatibility_decomposition_wide.snap | 46 ++++----- ...nder__tests__glyph_combining_standard.snap | 38 ++++---- ...render__tests__glyph_missing_standard.snap | 38 ++++---- .../sauva__ui__render__tests__glyph_wide.snap | 44 ++++----- ...idth_wraps_values_in_the_value_column.snap | 16 ++-- ...er__tests__normalization_long_minimum.snap | 10 +- ..._render__tests__normalization_minimum.snap | 10 +- ...rmalization_result_decomposed_minimum.snap | 8 +- ...__tests__normalization_result_minimum.snap | 8 +- ..._tests__normalization_result_standard.snap | 18 ++-- ...render__tests__normalization_standard.snap | 10 +- ...malization_unchanged_controls_minimum.snap | 10 +- ..._tests__search_alias_results_standard.snap | 2 +- ...der__tests__search_alias_results_wide.snap | 2 +- ...sts__search_combined_results_standard.snap | 46 ++++----- ...esults_minimum_with_scrolled_viewport.snap | 18 ++-- ...__tests__search_name_results_standard.snap | 46 ++++----- ...i__render__tests__search_special_wide.snap | 2 +- ...__ui__render__tests__sequence_minimum.snap | 12 +-- ...der__tests__sequence_scrolled_minimum.snap | 24 ++--- ..._sequence_selected_combining_standard.snap | 14 +-- ..._ui__render__tests__sequence_standard.snap | 12 +-- ...uva__ui__render__tests__sequence_wide.snap | 12 +-- ..._sequence_wrapped_coordinate_standard.snap | 44 ++++----- ...a__ui__render__tests__startup_minimum.snap | 18 ++-- ...__ui__render__tests__startup_standard.snap | 38 ++++---- src/ui/theme.rs | 2 +- ...e2e__inspector_after_browse_selection.snap | 74 +++++++------- ...sult_selected_across_terminal_resizes.snap | 4 +- ...ghts_changes_and_preserves_navigation.snap | 10 +- ...rminal_and_opens_single_point_results.snap | 10 +- ...without_moving_the_original_selection.snap | 22 ++--- ...ent_changes_to_separate_original_rows.snap | 10 +- ...s_keep_the_highlight_in_the_row_body.snap} | 16 ++-- tests/terminal_e2e.rs | 10 +- 60 files changed, 644 insertions(+), 1004 deletions(-) rename tests/snapshots/{terminal_e2e__sequence_clusters_keep_the_cursor_and_highlight_in_the_row_body.snap => terminal_e2e__sequence_clusters_keep_the_highlight_in_the_row_body.snap} (82%) diff --git a/config.schema.json b/config.schema.json index bff5389..0629ac2 100644 --- a/config.schema.json +++ b/config.schema.json @@ -124,7 +124,7 @@ "additionalProperties": false }, "differenceColors": { - "description": "Changed normalization spans, separate from the selection cursor. Underlining also identifies these spans.", + "description": "Changed normalization spans, separate from the selection colors. Underlining also identifies these spans.", "type": "object", "properties": { "fg": { @@ -272,12 +272,6 @@ } ], "default": "native" - }, - "selection_cursor": { - "description": "The cursor shown in the reserved leading cell of selected Inspector properties, list entries, and code point entries. It must be empty or occupy exactly one terminal cell.", - "type": "string", - "pattern": "^(?:|[^\\u0000-\\u001F\\u007F]+)$", - "default": "" } }, "additionalProperties": false diff --git a/docs/src/configurations/config-file-format.md b/docs/src/configurations/config-file-format.md index 7b2275c..69f7da1 100644 --- a/docs/src/configurations/config-file-format.md +++ b/docs/src/configurations/config-file-format.md @@ -33,7 +33,6 @@ fg = "#f5f7fa" bg = "#00000000" [ui] -selection_cursor = "" input_cursor = "native" ``` @@ -100,19 +99,6 @@ See [Glyph Preview](./glyph-preview.md) for examples and the complete font selec ## `ui` -### `selection_cursor` - -The marker shown before a selected Inspector property, list entry, or code point. - -- Type: string. -- Default: `""` (no visible marker). -- Must be empty or occupy exactly one terminal cell, without control characters. - -```toml -[ui] -selection_cursor = "▸" -``` - ### `input_cursor` The cursor shown in the search input. diff --git a/src/config.rs b/src/config.rs index aa95842..3f9ea80 100644 --- a/src/config.rs +++ b/src/config.rs @@ -475,14 +475,13 @@ mod tests { } #[test] - fn ui_config_is_loaded_and_unspecified_values_use_defaults() { + fn empty_ui_config_uses_defaults() { let directory = tempdir().unwrap(); let path = directory.path().join("config.toml"); fs::write( &path, r#" [ui] - selection_cursor = ">" "#, ) .unwrap(); @@ -490,8 +489,6 @@ mod tests { let configured = load_config(Some(&ConfigFile::Explicit(path))).unwrap(); let defaults = default_runtime_config(); - assert_eq!(configured.ui().selection_cursor, ">"); - assert_eq!(defaults.ui().selection_cursor, ""); assert_eq!( configured.ui().input_cursor, crate::ui::settings::InputCursor::Native @@ -556,27 +553,6 @@ mod tests { assert!(message.contains("ui.input_cursor")); } - #[test] - fn invalid_ui_config_includes_the_config_path_and_setting() { - let directory = tempdir().unwrap(); - let path = directory.path().join("config.toml"); - fs::write( - &path, - r#" - [ui] - selection_cursor = "界" - "#, - ) - .unwrap(); - - let error = load_config(Some(&ConfigFile::Explicit(path.clone()))).unwrap_err(); - let message = error.to_string(); - - assert!(matches!(error, ConfigError::Validate { .. })); - assert!(message.contains(&path.display().to_string())); - assert!(message.contains("ui.selection_cursor")); - } - #[test] fn unknown_top_level_field_is_rejected_with_the_path() { let directory = tempdir().unwrap(); diff --git a/src/ui/browser.rs b/src/ui/browser.rs index fba133e..756141b 100644 --- a/src/ui/browser.rs +++ b/src/ui/browser.rs @@ -14,20 +14,13 @@ use crate::{ layout::browser, scrollbar::{self, ViewportScrollbar}, selectable_list_line, selection_preview, - settings::UiSettings, theme::ColorTheme, workspace, }, unicode::{CodePoint, Plane, UnicodeDatabase, plane::PlaneRange}, }; -pub fn render( - frame: &mut Frame, - area: Rect, - state: &AppState, - color_theme: &ColorTheme, - ui: &UiSettings, -) { +pub fn render(frame: &mut Frame, area: Rect, state: &AppState, color_theme: &ColorTheme) { let browse = state .browse() .expect("the browser view always has browse state"); @@ -46,7 +39,6 @@ pub fn render( .visible_list_items() .expect("the plane level has a list viewport"), color_theme, - ui, ); } BrowseLevel::Range => { @@ -61,7 +53,6 @@ pub fn render( .visible_list_items() .expect("the range level has a list viewport"), color_theme, - ui, ); } BrowseLevel::Block => { @@ -76,7 +67,6 @@ pub fn render( .visible_list_items() .expect("the block level has a list viewport"), color_theme, - ui, ); } BrowseLevel::CodePointTable => { @@ -98,7 +88,6 @@ pub fn render( .visible_table_rows() .expect("the code point table level has a row viewport"), color_theme, - ui, ); } } @@ -110,7 +99,6 @@ fn render_block_navigator( browse: &BrowseState, visible_items: std::ops::Range, color_theme: &ColorTheme, - ui: &UiSettings, ) { let selected = browse .selected_block() @@ -121,10 +109,9 @@ fn render_block_navigator( .map(|index| { let item = UnicodeDatabase::block(index).expect("the viewport contains valid blocks"); let is_selected = item == selected; - let marker = ui.selection_marker(is_selected); selectable_list_line( Line::from(format!( - "{marker} {:06X}–{:06X} {}", + "{:06X}–{:06X} {}", item.start().value(), item.end().value(), item.name() @@ -174,7 +161,6 @@ fn render_plane_navigator( cursor: CodePoint, visible_items: std::ops::Range, color_theme: &ColorTheme, - ui: &UiSettings, ) { let selected = Plane::for_code_point(cursor); let content = workspace::render_primary_heading(frame, area, "Planes", None, color_theme); @@ -183,10 +169,9 @@ fn render_plane_navigator( .map(|number| { let plane = Plane::new(number as u8).expect("the viewport contains valid planes"); let is_selected = plane == selected; - let marker = ui.selection_marker(is_selected); selectable_list_line( Line::from(format!( - "{marker} Plane {:>2} {}", + "Plane {:>2} {}", plane.number(), plane.name().unwrap_or("Reserved") )), @@ -230,7 +215,6 @@ fn render_range_navigator( cursor: CodePoint, visible_items: std::ops::Range, color_theme: &ColorTheme, - ui: &UiSettings, ) { let selected = PlaneRange::for_code_point(cursor); let plane = format!("Plane {}", selected.plane().number()); @@ -241,9 +225,8 @@ fn render_range_navigator( .map(|index| { let range = selected.plane().range(index as u8); let is_selected = range == selected; - let marker = ui.selection_marker(is_selected); selectable_list_line( - Line::from(format!("{marker} {}–{}", range.start(), range.end())), + Line::from(format!("{}–{}", range.start(), range.end())), is_selected, content.width, color_theme.selection, @@ -298,7 +281,6 @@ fn render_code_point_table( browse: &BrowseState, visible_rows: std::ops::Range, color_theme: &ColorTheme, - ui: &UiSettings, ) { let cursor = browse.cursor(); let (page_start, page_end) = browse @@ -322,7 +304,7 @@ fn render_code_point_table( let code_point = CodePoint::new(row_start + column as u32) .expect("table pages contain valid code points"); let selected = code_point == cursor; - let cell = table_cell(code_point, selected, ui); + let cell = table_cell(code_point); spans.push(if selected { Span::styled(cell, color_theme.selection.style()) } else { @@ -355,7 +337,7 @@ fn table_column_header(selected_column: usize, color_theme: &ColorTheme) -> Line Line::from(spans) } -fn table_cell(code_point: CodePoint, selected: bool, ui: &UiSettings) -> String { +fn table_cell(code_point: CodePoint) -> String { let representation = UnicodeDatabase::display_representation(code_point); let width = Line::from(representation.as_str()).width(); let display = if matches!(width, 1 | 2) { @@ -364,9 +346,7 @@ fn table_cell(code_point: CodePoint, selected: bool, ui: &UiSettings) -> String "·" }; let display_width = Line::from(display).width(); - let marker = ui.selection_marker(selected); - - format!("{marker}{display}{}", " ".repeat(2 - display_width)) + format!(" {display}{}", " ".repeat(2 - display_width)) } #[cfg(test)] @@ -376,25 +356,20 @@ mod tests { use super::*; #[rstest] - #[case(0x0041, false, " A ")] - #[case(0x4e00, false, " 一")] - #[case(0x0301, true, ">◌́ ")] - #[case(0x000a, false, " · ")] - #[case(0x0378, false, " · ")] - #[case(0xd800, true, ">· ")] - #[case(0xe000, false, " · ")] - #[case(0xfdd0, false, " · ")] + #[case(0x0041, " A ")] + #[case(0x4e00, " 一")] + #[case(0x0301, " ◌́ ")] + #[case(0x000a, " · ")] + #[case(0x0378, " · ")] + #[case(0xd800, " · ")] + #[case(0xe000, " · ")] + #[case(0xfdd0, " · ")] fn table_cells_use_only_safe_one_or_two_column_representations( #[case] value: u32, - #[case] selected: bool, #[case] expected: &str, ) { let code_point = CodePoint::new(value).unwrap(); - let ui = UiSettings { - selection_cursor: ">".to_owned(), - ..Default::default() - }; - let cell = table_cell(code_point, selected, &ui); + let cell = table_cell(code_point); assert_eq!(cell, expected, "{code_point}"); assert_eq!(Line::from(cell).width(), 3, "{code_point}"); @@ -406,24 +381,12 @@ mod tests { let cursor = CodePoint::new(0x0041).unwrap(); let range = PlaneRange::for_code_point(cursor); - let ui = UiSettings::default(); let mut row = format!("{:06X} ", range.start().value()); for offset in 0..16 { let code_point = range.code_point(offset); - row.push_str(&table_cell(code_point, code_point == cursor, &ui)); + row.push_str(&table_cell(code_point)); } assert_eq!(Line::from(row).width(), 55); } - - #[test] - fn empty_selection_cursor_preserves_the_code_point_cell_width() { - let cursor = CodePoint::new(0x0041).unwrap(); - - assert_eq!(table_cell(cursor, true, &UiSettings::default()), " A "); - assert_eq!( - Line::from(table_cell(cursor, true, &UiSettings::default())).width(), - 3 - ); - } } diff --git a/src/ui/inspector.rs b/src/ui/inspector.rs index 567f2de..cccd9e1 100644 --- a/src/ui/inspector.rs +++ b/src/ui/inspector.rs @@ -11,13 +11,12 @@ use crate::inspector::InspectorField; use crate::ui::{ key_value, layout, scrollbar::{self, ViewportScrollbar}, - settings::UiSettings, theme::ColorTheme, workspace, }; use crate::unicode::CodePoint; -const LABEL_WIDTH: usize = 29; +const LABEL_WIDTH: usize = 27; #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ViewportMetrics { @@ -40,20 +39,13 @@ pub fn viewport_metrics(area: Rect, state: &AppState) -> ViewportMetrics { } } -pub fn render( - frame: &mut Frame, - area: Rect, - state: &AppState, - color_theme: &ColorTheme, - ui: &UiSettings, -) { +pub fn render(frame: &mut Frame, area: Rect, state: &AppState, color_theme: &ColorTheme) { let content = content_area(area); let document = InspectorDocument::with_color_theme( state.selected(), usize::from(content.width), state.inspector().selected_index(), color_theme, - ui, ); let range = state.inspector().visible_range(); let start = range.start.min(document.lines.len()); @@ -79,13 +71,7 @@ struct InspectorDocument { impl InspectorDocument { fn for_code_point(code_point: CodePoint, width: usize) -> Self { - Self::with_color_theme( - code_point, - width, - 0, - &ColorTheme::default(), - &UiSettings::default(), - ) + Self::with_color_theme(code_point, width, 0, &ColorTheme::default()) } fn with_color_theme( @@ -93,7 +79,6 @@ impl InspectorDocument { width: usize, selected_index: usize, color_theme: &ColorTheme, - ui: &UiSettings, ) -> Self { let mut builder = DocumentBuilder::new(width, color_theme); let mut field_ranges = Vec::new(); @@ -108,12 +93,7 @@ impl InspectorDocument { } let start = builder.lines.len(); let selected = index == selected_index; - builder.fields( - field.label(), - field.values().iter().cloned(), - selected, - ui.selection_marker(selected), - ); + builder.fields(field.label(), field.values().iter().cloned(), selected); field_ranges.push(start..builder.lines.len()); } @@ -155,19 +135,12 @@ impl DocumentBuilder { ))); } - fn fields( - &mut self, - label: &str, - values: impl IntoIterator, - selected: bool, - selection_marker: &str, - ) { + fn fields(&mut self, label: &str, values: impl IntoIterator, selected: bool) { self.lines.extend(key_value::property_lines( label, values, self.width, self.label_width, - &format!("{selection_marker} "), Style::new().fg(self.color_theme.muted), Style::new(), selected.then(|| self.color_theme.selection.style()), diff --git a/src/ui/key_value.rs b/src/ui/key_value.rs index de10ec8..50d4e86 100644 --- a/src/ui/key_value.rs +++ b/src/ui/key_value.rs @@ -43,7 +43,6 @@ impl Document { [entry.value.to_owned()], width, label_width, - "", Style::new().fg(color_theme.muted), Style::new(), None, @@ -84,13 +83,11 @@ pub fn required_height(content_width: u16, label_width: u16, entries: &[KeyValue Document::for_entries(content_width, label_width, entries, &ColorTheme::default()).height() } -#[allow(clippy::too_many_arguments)] pub fn property_lines( label: &str, values: impl IntoIterator, width: usize, label_width: usize, - label_prefix: &str, label_style: Style, value_style: Style, selected_style: Option