diff --git a/Cargo.lock b/Cargo.lock index 23856c1..c3a9af6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "ostiary" -version = "1.1.5" +version = "1.1.6" dependencies = [ "anyhow", "base64", @@ -1223,6 +1223,7 @@ dependencies = [ "tokio", "tokio-util", "toml", + "unicode-width 0.1.14", ] [[package]] @@ -1586,7 +1587,7 @@ dependencies = [ "thiserror 2.0.18", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -1637,7 +1638,7 @@ dependencies = [ "strum", "time", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -2431,9 +2432,15 @@ checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ "itertools", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.2", ] +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index ae23a95..62375da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ostiary" -version = "1.1.5" +version = "1.1.6" edition = "2024" [dependencies] @@ -21,3 +21,4 @@ libc = "0.2" ed25519-dalek = { version = "2", features = ["rand_core"] } rand = "0.8" base64 = "0.22" +unicode-width = "0.1" diff --git a/src/ui.rs b/src/ui.rs index 20e989c..bc73d1c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,6 +3,7 @@ use crate::config::Config; use crate::executor; use crate::app::{App, MessageLevel, Popup, UpdatingStatus}; use crate::text_input::TextInput; +use unicode_width::UnicodeWidthChar; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, @@ -102,8 +103,11 @@ fn render_menu(f: &mut Frame, app: &mut App, area: Rect) { let endpoint_title = app.config.endpoints.iter() .find(|ep| ep.url == app.config.active_endpoint) - .map(|ep| format!(" {} ", ep.name)) - .unwrap_or_else(|| " Меню ".to_string()); + .map(|ep| Line::from(vec![ + Span::raw(format!(" {} ", ep.name)), + Span::styled(" — [↹] Изменить ", Style::default().fg(Color::DarkGray)), + ])) + .unwrap_or_else(|| Line::from(" Меню ")); let list = List::new(list_items).block( Block::default() @@ -542,10 +546,11 @@ fn render_popup(f: &mut Frame, popup: &mut Popup, config: &Config) { // Real terminal cursor in the active field let af = *active_field; - let cx = (chunks[af].x + 1 + fields[af].cursor as u16) + let cx = (chunks[af].x + 1 + display_cols(&fields[af].buf, fields[af].cursor)) .min(chunks[af].x + chunks[af].width.saturating_sub(2)); f.set_cursor_position((cx, chunks[af].y + 1)); + let (title, hint) = if edit_index.is_some() { (" Редактировать эндпоинт ", " Enter — далее / сохранить ") } else { @@ -655,7 +660,8 @@ fn render_command( ), area, ); - let cursor_x = (area.x + 1 + input.cursor as u16).min(area.x + area.width.saturating_sub(2)); + let col = if *secret { input.cursor as u16 } else { display_cols(&input.buf, input.cursor) }; + let cursor_x = (area.x + 1 + col).min(area.x + area.width.saturating_sub(2)); f.set_cursor_position((cursor_x, area.y + 1)); } @@ -814,6 +820,15 @@ fn render_command( } } +/// Display-column width of `buf` up to char index `cursor`. +/// Accounts for wide characters (emoji, CJK) that occupy 2 terminal columns. +fn display_cols(buf: &str, cursor: usize) -> u16 { + buf.chars() + .take(cursor) + .map(|c| UnicodeWidthChar::width(c).unwrap_or(0)) + .sum::() as u16 +} + fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical)