22//!
33//! This module reads the character before the cursor in the currently focused
44//! text field to determine appropriate capitalization for inserted text.
5+ //!
6+ //! Features:
7+ //! - Capitalizes after sentence-ending punctuation (. ! ?)
8+ //! - Lowercases after continuation punctuation (, ; : -)
9+ //! - Adds trailing space after sentence-ending punctuation in output
10+ //! - When context cannot be read (terminal apps), assumes consecutive sentences
511
612use log:: { debug, info} ;
713
@@ -201,45 +207,53 @@ fn find_relevant_punctuation(text: &str) -> Option<char> {
201207 None
202208}
203209
204- /// Result of context analysis including both capitalization and spacing .
210+ /// Result of context analysis.
205211#[ derive( Debug , Clone ) ]
206- pub struct ContextResult {
212+ struct ContextResult {
207213 /// Whether to capitalize, lowercase, or leave unchanged
208- pub hint : CapitalizationHint ,
209- /// Whether to prepend a space before the text
210- pub needs_leading_space : bool ,
214+ hint : CapitalizationHint ,
215+ /// Whether context was successfully read (false = fallback mode)
216+ context_readable : bool ,
211217}
212218
213- /// Analyze the context and return capitalization hint plus spacing info .
219+ /// Analyze the context and return capitalization hint plus context readability .
214220///
215221/// This function:
216222/// 1. Reads text before the cursor
217223/// 2. Scans back through whitespace (up to MAX_WHITESPACE_LOOKBACK chars)
218224/// 3. Determines capitalization based on the punctuation found
219- /// 4. Determines if a leading space is needed
225+ /// 4. Reports whether context was successfully read
220226///
221227/// ## Scenarios handled:
222- /// - `"Hello."` → Capitalize, add leading space
223- /// - `"Hello. "` → Capitalize, no leading space needed
224- /// - `"Hey,"` → Lowercase, add leading space
225- /// - `"Hey, "` → Lowercase, no leading space needed
226- /// - `"Hello"` → Unknown (mid-word), no change
227- /// - Empty/beginning → Capitalize (start of text)
228+ /// - `"Hello."` → Capitalize (context readable)
229+ /// - `"Hello. "` → Capitalize (context readable)
230+ /// - `"Hey,"` → Lowercase (context readable)
231+ /// - `"Hey, "` → Lowercase (context readable)
232+ /// - `"Hello"` → Unknown/mid-word (context readable)
233+ /// - Empty/beginning → Capitalize (context readable)
234+ /// - Terminal/API failure → Capitalize (context NOT readable - fallback mode)
228235fn analyze_context ( ) -> ContextResult {
229236 let text = match get_text_before_cursor ( ) {
230237 Some ( t) => t,
231238 None => {
232- // Empty field, can't read, or at beginning - capitalize by default
233- debug ! ( "No text before cursor, defaulting to Capitalize" ) ;
239+ // Can't read context (unsupported app like terminal, or API failure)
240+ // In fallback mode, assume consecutive sentences
241+ debug ! ( "No text before cursor (fallback mode), defaulting to Capitalize" ) ;
234242 return ContextResult {
235243 hint : CapitalizationHint :: Capitalize ,
236- needs_leading_space : false ,
244+ context_readable : false ,
237245 } ;
238246 }
239247 } ;
240248
241- // Check if there's already a space at the end
242- let has_trailing_space = text. ends_with ( ' ' ) || text. ends_with ( '\t' ) ;
249+ // If we got an empty string, treat as start of text (but context IS readable)
250+ if text. is_empty ( ) {
251+ debug ! ( "Empty text field, Capitalize (context readable)" ) ;
252+ return ContextResult {
253+ hint : CapitalizationHint :: Capitalize ,
254+ context_readable : true ,
255+ } ;
256+ }
243257
244258 // Find the relevant punctuation by looking back through whitespace
245259 let relevant_char = find_relevant_punctuation ( & text) ;
@@ -250,57 +264,41 @@ fn analyze_context() -> ContextResult {
250264 debug ! ( "No relevant punctuation found, defaulting to Capitalize" ) ;
251265 ContextResult {
252266 hint : CapitalizationHint :: Capitalize ,
253- needs_leading_space : false ,
267+ context_readable : true ,
254268 }
255269 }
256270 Some ( c) if CAPITALIZE_AFTER . contains ( & c) => {
257- debug ! (
258- "Found sentence-ending '{}', Capitalize, needs_space={}" ,
259- c, !has_trailing_space
260- ) ;
271+ debug ! ( "Found sentence-ending '{}', Capitalize" , c) ;
261272 ContextResult {
262273 hint : CapitalizationHint :: Capitalize ,
263- needs_leading_space : !has_trailing_space ,
274+ context_readable : true ,
264275 }
265276 }
266277 Some ( c) if c == '\n' || c == '\r' => {
267- // Newline - capitalize, no space needed (newline acts as separator)
268- debug ! ( "Found newline, Capitalize, no space needed" ) ;
278+ debug ! ( "Found newline, Capitalize" ) ;
269279 ContextResult {
270280 hint : CapitalizationHint :: Capitalize ,
271- needs_leading_space : false ,
281+ context_readable : true ,
272282 }
273283 }
274284 Some ( c) if LOWERCASE_AFTER . contains ( & c) => {
275- debug ! (
276- "Found continuation '{}', Lowercase, needs_space={}" ,
277- c, !has_trailing_space
278- ) ;
285+ debug ! ( "Found continuation '{}', Lowercase" , c) ;
279286 ContextResult {
280287 hint : CapitalizationHint :: Lowercase ,
281- needs_leading_space : !has_trailing_space ,
288+ context_readable : true ,
282289 }
283290 }
284291 Some ( c) => {
285292 // Some other character (letter, number, etc.)
286- // Don't change capitalization, but might need space
287- debug ! (
288- "Found other char '{}', Unknown, needs_space={}" ,
289- c, !has_trailing_space
290- ) ;
293+ debug ! ( "Found other char '{}', Unknown" , c) ;
291294 ContextResult {
292295 hint : CapitalizationHint :: Unknown ,
293- needs_leading_space : !has_trailing_space ,
296+ context_readable : true ,
294297 }
295298 }
296299 }
297300}
298301
299- /// Legacy function for getting just the hint (used by tests).
300- pub fn get_capitalization_hint ( ) -> CapitalizationHint {
301- analyze_context ( ) . hint
302- }
303-
304302/// Apply capitalization hint to the given text.
305303///
306304/// - If hint is `Capitalize`, ensures first alphabetic char is uppercase
@@ -345,31 +343,42 @@ pub fn apply_capitalization(text: &str, hint: CapitalizationHint) -> String {
345343/// Convenience function that reads context and applies capitalization in one step.
346344///
347345/// This is the main entry point for context-aware capitalization.
348- /// On non-macOS platforms or if context cannot be read, returns text unchanged .
346+ /// On non-macOS platforms or if context cannot be read, uses fallback behavior .
349347///
350348/// This function:
351349/// 1. Analyzes the text before the cursor
352350/// 2. Determines appropriate capitalization (capitalize/lowercase/unchanged)
353- /// 3. Adds a leading space if needed (no space after punctuation)
351+ /// 3. Adds a trailing space based on smart logic:
352+ /// - If output ends with sentence-ending punctuation (. ! ?), add space
353+ /// - If context was NOT readable (terminal apps, etc.), add space (assume consecutive sentences)
354354///
355355/// ## Examples:
356- /// - After `"Hello."` with input `"world"` → `" World"` (space + capitalize )
357- /// - After `"Hello. "` with input `"world "` → `"World "` (capitalize, space exists )
358- /// - After `"Hey,"` with input `"What" ` → `" what "` (space + lowercase )
359- /// - After `"Hey, "` with input `"What "` → `"what "` (lowercase, space exists )
356+ /// - After `"Hello."` with input `"world"` → `"World"` (capitalize, context readable )
357+ /// - After `"Hey, "` with input `"What "` → `"what "` (lowercase, context readable )
358+ /// - Input `"Hello world." ` → `"Hello world. "` (trailing space for punctuation )
359+ /// - In terminal with input `"hello "` → `"Hello "` (trailing space for fallback mode )
360360pub fn apply_context_aware_capitalization ( text : & str ) -> String {
361361 let context = analyze_context ( ) ;
362362 let capitalized = apply_capitalization ( text, context. hint ) ;
363363
364- let result = if context. needs_leading_space {
365- format ! ( " {}" , capitalized)
364+ // Determine if we should add a trailing space:
365+ // 1. Always add space after sentence-ending punctuation
366+ // 2. Add space in fallback mode (context not readable) - assume consecutive sentences
367+ let ends_with_sentence_punctuation = capitalized. ends_with ( '.' )
368+ || capitalized. ends_with ( '!' )
369+ || capitalized. ends_with ( '?' ) ;
370+
371+ let should_add_trailing_space = ends_with_sentence_punctuation || !context. context_readable ;
372+
373+ let result = if should_add_trailing_space {
374+ format ! ( "{} " , capitalized)
366375 } else {
367376 capitalized
368377 } ;
369378
370379 info ! (
371- "Context-aware capitalization: hint={:?}, needs_space ={}, input='{}', output='{}'" ,
372- context. hint, context. needs_leading_space , text, result
380+ "Context-aware capitalization: hint={:?}, context_readable ={}, input='{}', output='{}'" ,
381+ context. hint, context. context_readable , text, result
373382 ) ;
374383 result
375384}
@@ -485,9 +494,16 @@ mod tests {
485494 apply_capitalization( "42 - hello" , CapitalizationHint :: Capitalize ) ,
486495 "42 - Hello"
487496 ) ;
497+ // Lowercase hint only affects the first alphabetic char
498+ // Here 'n' in 'note' is already lowercase, so no change
488499 assert_eq ! (
489500 apply_capitalization( "(note: Hello)" , CapitalizationHint :: Lowercase ) ,
490- "(note: hello)"
501+ "(note: Hello)"
502+ ) ;
503+ // Test with first letter being uppercase
504+ assert_eq ! (
505+ apply_capitalization( "(Note: Hello)" , CapitalizationHint :: Lowercase ) ,
506+ "(note: Hello)"
491507 ) ;
492508 }
493509
0 commit comments