Signed-off-by: toyamagu2021 <tomoki-yamaguchi@c-fo.com>
This commit is contained in:
@@ -4,6 +4,7 @@ use cliclack::{self, intro, outro};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::project_tracker::ProjectTracker;
|
use crate::project_tracker::ProjectTracker;
|
||||||
|
use crate::utils::safe_truncate;
|
||||||
|
|
||||||
/// Format a DateTime for display
|
/// Format a DateTime for display
|
||||||
fn format_date(date: DateTime<chrono::Utc>) -> String {
|
fn format_date(date: DateTime<chrono::Utc>) -> String {
|
||||||
@@ -199,11 +200,7 @@ pub fn handle_projects_interactive() -> Result<()> {
|
|||||||
.last_instruction
|
.last_instruction
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(String::new(), |instr| {
|
.map_or(String::new(), |instr| {
|
||||||
let truncated = if instr.len() > 40 {
|
let truncated = safe_truncate(instr, 40);
|
||||||
format!("{}...", &instr[0..37])
|
|
||||||
} else {
|
|
||||||
instr.clone()
|
|
||||||
};
|
|
||||||
format!(" [{}]", truncated)
|
format!(" [{}]", truncated)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::session::message_to_markdown;
|
use crate::session::message_to_markdown;
|
||||||
|
use crate::utils::safe_truncate;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use cliclack::{confirm, multiselect, select};
|
use cliclack::{confirm, multiselect, select};
|
||||||
use goose::session::info::{get_valid_sorted_sessions, SessionInfo, SortOrder};
|
use goose::session::info::{get_valid_sorted_sessions, SessionInfo, SortOrder};
|
||||||
@@ -50,11 +51,7 @@ fn prompt_interactive_session_removal(sessions: &[SessionInfo]) -> Result<Vec<Se
|
|||||||
} else {
|
} else {
|
||||||
&s.metadata.description
|
&s.metadata.description
|
||||||
};
|
};
|
||||||
let truncated_desc = if desc.len() > TRUNCATED_DESC_LENGTH {
|
let truncated_desc = safe_truncate(desc, TRUNCATED_DESC_LENGTH);
|
||||||
format!("{}...", &desc[..TRUNCATED_DESC_LENGTH - 3])
|
|
||||||
} else {
|
|
||||||
desc.to_string()
|
|
||||||
};
|
|
||||||
let display_text = format!("{} - {} ({})", s.modified, truncated_desc, s.id);
|
let display_text = format!("{} - {} ({})", s.modified, truncated_desc, s.id);
|
||||||
(display_text, s.clone())
|
(display_text, s.clone())
|
||||||
})
|
})
|
||||||
@@ -320,11 +317,7 @@ pub fn prompt_interactive_session_selection() -> Result<session::Identifier> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Truncate description if too long
|
// Truncate description if too long
|
||||||
let truncated_desc = if desc.len() > 40 {
|
let truncated_desc = safe_truncate(desc, 40);
|
||||||
format!("{}...", &desc[..37])
|
|
||||||
} else {
|
|
||||||
desc.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let display_text = format!("{} - {} ({})", s.modified, truncated_desc, s.id);
|
let display_text = format!("{} - {} ({})", s.modified, truncated_desc, s.id);
|
||||||
(display_text, s.clone())
|
(display_text, s.clone())
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub mod project_tracker;
|
|||||||
pub mod recipes;
|
pub mod recipes;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
|
pub mod utils;
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
pub use session::Session;
|
pub use session::Session;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/// Utility functions for safe string handling and other common operations
|
||||||
|
/// Safely truncate a string at character boundaries, not byte boundaries
|
||||||
|
///
|
||||||
|
/// This function ensures that multi-byte UTF-8 characters (like Japanese, emoji, etc.)
|
||||||
|
/// are not split in the middle, which would cause a panic.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `s` - The string to truncate
|
||||||
|
/// * `max_chars` - Maximum number of characters to keep
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A truncated string with "..." appended if truncation occurred
|
||||||
|
pub fn safe_truncate(s: &str, max_chars: usize) -> String {
|
||||||
|
if s.chars().count() <= max_chars {
|
||||||
|
s.to_string()
|
||||||
|
} else {
|
||||||
|
let truncated: String = s.chars().take(max_chars.saturating_sub(3)).collect();
|
||||||
|
format!("{}...", truncated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_safe_truncate_ascii() {
|
||||||
|
assert_eq!(safe_truncate("hello world", 20), "hello world");
|
||||||
|
assert_eq!(safe_truncate("hello world", 8), "hello...");
|
||||||
|
assert_eq!(safe_truncate("hello", 5), "hello");
|
||||||
|
assert_eq!(safe_truncate("hello", 3), "...");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_safe_truncate_japanese() {
|
||||||
|
// Japanese characters: "こんにちは世界" (Hello World)
|
||||||
|
let japanese = "こんにちは世界";
|
||||||
|
assert_eq!(safe_truncate(japanese, 10), japanese);
|
||||||
|
assert_eq!(safe_truncate(japanese, 5), "こん...");
|
||||||
|
assert_eq!(safe_truncate(japanese, 7), japanese);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_safe_truncate_mixed() {
|
||||||
|
// Mixed ASCII and Japanese
|
||||||
|
let mixed = "Hello こんにちは";
|
||||||
|
assert_eq!(safe_truncate(mixed, 20), mixed);
|
||||||
|
assert_eq!(safe_truncate(mixed, 8), "Hello...");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user