@@ -153,12 +153,15 @@ fn format_relative_time(time: &chrono::DateTime<chrono::Utc>) -> String {
153153 }
154154}
155155
156- /// Truncate string to fit width.
156+ /// Truncate string to fit width, handling multi-byte UTF-8 safely .
157157fn truncate_string ( s : & str , width : usize ) -> String {
158- if s. len ( ) <= width {
158+ // Count actual character width, not byte length
159+ let char_count = s. chars ( ) . count ( ) ;
160+ if char_count <= width {
159161 s. to_string ( )
160162 } else if width > 3 {
161- format ! ( "{}..." , & s[ ..width - 3 ] )
163+ let truncated: String = s. chars ( ) . take ( width - 3 ) . collect ( ) ;
164+ format ! ( "{}..." , truncated)
162165 } else {
163166 s. chars ( ) . take ( width) . collect ( )
164167 }
@@ -176,4 +179,40 @@ mod tests {
176179 let hour_ago = now - chrono:: Duration :: hours ( 2 ) ;
177180 assert_eq ! ( format_relative_time( & hour_ago) , "2h ago" ) ;
178181 }
182+
183+ #[ test]
184+ fn test_truncate_string_ascii ( ) {
185+ // Short string, no truncation needed
186+ assert_eq ! ( truncate_string( "hello" , 10 ) , "hello" ) ;
187+
188+ // Exact fit
189+ assert_eq ! ( truncate_string( "hello" , 5 ) , "hello" ) ;
190+
191+ // Needs truncation
192+ assert_eq ! ( truncate_string( "hello world" , 8 ) , "hello..." ) ;
193+
194+ // Very short width
195+ assert_eq ! ( truncate_string( "hello" , 3 ) , "hel" ) ;
196+ assert_eq ! ( truncate_string( "hello" , 2 ) , "he" ) ;
197+ }
198+
199+ #[ test]
200+ fn test_truncate_string_utf8 ( ) {
201+ // UTF-8 multi-byte characters (Japanese)
202+ let japanese = "こんにちは世界" ; // 7 chars
203+ assert_eq ! ( truncate_string( japanese, 10 ) , japanese) ; // No truncation
204+ assert_eq ! ( truncate_string( japanese, 7 ) , japanese) ; // Exact fit
205+ assert_eq ! ( truncate_string( japanese, 6 ) , "こんに..." ) ; // Truncated (3 chars + ...)
206+
207+ // UTF-8 with emoji
208+ let emoji = "Hello 🌍🌎🌏" ; // 9 chars: H,e,l,l,o, ,🌍,🌎,🌏
209+ assert_eq ! ( truncate_string( emoji, 20 ) , emoji) ; // No truncation
210+ assert_eq ! ( truncate_string( emoji, 9 ) , emoji) ; // Exact fit (9 chars)
211+ assert_eq ! ( truncate_string( emoji, 8 ) , "Hello..." ) ; // Truncated (5 chars + ...)
212+
213+ // Mixed UTF-8 and ASCII
214+ let mixed = "路径/path/文件" ; // 11 chars
215+ assert_eq ! ( truncate_string( mixed, 20 ) , mixed) ; // No truncation
216+ assert_eq ! ( truncate_string( mixed, 8 ) , "路径/pa..." ) ; // Truncated
217+ }
179218}
0 commit comments