Skip to content

Commit afe1b38

Browse files
committed
change cursor shape depending on the mode; cargo fmt
1 parent 85d21af commit afe1b38

6 files changed

Lines changed: 65 additions & 38 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ sofos
8989
- `/resume` - Resume previous session
9090
- `/clear` - Clear conversation history
9191
- `/think [on|off]` - Toggle extended thinking (shows status if no arg)
92-
- `/s` - Safe mode (read-only, prompt: **`λ:`**)
93-
- `/n` - Normal mode (all tools, prompt: **`λ>`**)
92+
- `/s` - Safe mode (read-only, prompt: **`λ:`**, blinking underscore (`_`) cursor)
93+
- `/n` - Normal mode (all tools, prompt: **`λ>`**, default cursor)
9494
- `/exit`, `/quit`, `/q`, `Ctrl+D` - Exit with cost summary
9595
- `ESC` - Interrupt AI response
9696

src/prompt.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::ui::{set_normal_mode_cursor_style, set_safe_mode_cursor_style};
12
use colored::Colorize;
23
use reedline::{Prompt, PromptEditMode, PromptHistorySearch};
34

@@ -12,6 +13,12 @@ impl ReplPrompt {
1213
}
1314

1415
pub fn set_safe_mode(&mut self, safe_mode: bool) {
16+
if safe_mode {
17+
set_safe_mode_cursor_style().ok();
18+
} else {
19+
set_normal_mode_cursor_style().ok();
20+
}
21+
1522
self.safe_mode = safe_mode;
1623
}
1724
}

src/repl.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::response_handler::ResponseHandler;
1212
use crate::session_state::SessionState;
1313
use crate::tools::image::{extract_image_references, ImageLoader, ImageReference};
1414
use crate::tools::ToolExecutor;
15-
use crate::ui::UI;
15+
use crate::ui::{set_safe_mode_cursor_style, UI};
1616
use colored::Colorize;
1717
use crossterm::event::{KeyCode, KeyModifiers};
1818
use reedline::{
@@ -98,6 +98,7 @@ impl Repl {
9898

9999
if config.safe_mode {
100100
conversation.add_user_message(SAFE_MODE_MESSAGE.to_string());
101+
set_safe_mode_cursor_style()?;
101102
}
102103

103104
let commands: Vec<String> = crate::commands::COMMANDS

src/tools/codesearch.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,16 @@ impl CodeSearchTool {
1414
let env_override = std::env::var_os("SOFOS_RG_PATH").map(PathBuf::from);
1515

1616
// Fallback search list for common macOS/Homebrew and Linux locations
17-
let fallback_paths = [
18-
"/opt/homebrew/bin/rg",
19-
"/usr/local/bin/rg",
20-
"/usr/bin/rg",
21-
];
17+
let fallback_paths = ["/opt/homebrew/bin/rg", "/usr/local/bin/rg", "/usr/bin/rg"];
2218

2319
let try_path = |p: &PathBuf| Command::new(p).arg("--version").output();
2420

2521
if let Some(p) = env_override {
2622
if try_path(&p).is_ok() {
27-
return Ok(Self { workspace, rg_path: p });
23+
return Ok(Self {
24+
workspace,
25+
rg_path: p,
26+
});
2827
}
2928
}
3029

src/tools/image.rs

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -442,19 +442,19 @@ mod tests {
442442
assert_eq!(text, "check out please");
443443

444444
// Test single-quoted path with spaces
445-
let (text, refs) = extract_image_references(
446-
"view '/home/user/my photos/vacation.jpg' now",
445+
let (text, refs) = extract_image_references("view '/home/user/my photos/vacation.jpg' now");
446+
assert_eq!(
447+
refs.len(),
448+
1,
449+
"Should detect single-quoted path with spaces"
447450
);
448-
assert_eq!(refs.len(), 1, "Should detect single-quoted path with spaces");
449451
assert!(
450452
matches!(&refs[0], ImageReference::LocalPath(p) if p == "/home/user/my photos/vacation.jpg")
451453
);
452454
assert_eq!(text, "view now");
453455

454456
// Test path with spaces at the end
455-
let (text, refs) = extract_image_references(
456-
"\"/Users/alex/test/image file.png\"",
457-
);
457+
let (text, refs) = extract_image_references("\"/Users/alex/test/image file.png\"");
458458
assert_eq!(refs.len(), 1, "Should detect quoted path at end");
459459
assert!(
460460
matches!(&refs[0], ImageReference::LocalPath(p) if p == "/Users/alex/test/image file.png")
@@ -465,10 +465,13 @@ mod tests {
465465
#[test]
466466
fn test_extract_mixed_quoted_and_unquoted() {
467467
// Mix of quoted path and unquoted path
468-
let (text, refs) = extract_image_references(
469-
"compare \"file with space.png\" and simple.jpg",
468+
let (text, refs) =
469+
extract_image_references("compare \"file with space.png\" and simple.jpg");
470+
assert_eq!(
471+
refs.len(),
472+
2,
473+
"Should detect both quoted and unquoted paths"
470474
);
471-
assert_eq!(refs.len(), 2, "Should detect both quoted and unquoted paths");
472475
assert!(matches!(&refs[0], ImageReference::LocalPath(p) if p == "file with space.png"));
473476
assert!(matches!(&refs[1], ImageReference::LocalPath(p) if p == "simple.jpg"));
474477
assert_eq!(text, "compare and");
@@ -477,9 +480,7 @@ mod tests {
477480
#[test]
478481
fn test_extract_quoted_non_image() {
479482
// Quoted text that's not an image should remain in text
480-
let (text, refs) = extract_image_references(
481-
"the title is \"Hello World\" and image.png",
482-
);
483+
let (text, refs) = extract_image_references("the title is \"Hello World\" and image.png");
483484
assert_eq!(refs.len(), 1, "Should only detect the actual image");
484485
assert!(matches!(&refs[0], ImageReference::LocalPath(p) if p == "image.png"));
485486
assert_eq!(text, "the title is \"Hello World\" and");
@@ -488,20 +489,21 @@ mod tests {
488489
#[test]
489490
fn test_extract_unclosed_quote() {
490491
// Unclosed quote should be treated as regular text
491-
let (text, refs) = extract_image_references(
492-
"this is \"unclosed quote and image.png",
493-
);
492+
let (text, refs) = extract_image_references("this is \"unclosed quote and image.png");
494493
// The unclosed quote should make everything after it part of the text
495-
assert_eq!(refs.len(), 0, "Unclosed quote should prevent image detection");
494+
assert_eq!(
495+
refs.len(),
496+
0,
497+
"Unclosed quote should prevent image detection"
498+
);
496499
assert!(text.contains("unclosed quote and image.png"));
497500
}
498501

499502
#[test]
500503
fn test_extract_web_url_with_spaces_quoted() {
501504
// Web URLs with spaces (rare but possible)
502-
let (text, refs) = extract_image_references(
503-
"see \"https://example.com/my image.png\" please",
504-
);
505+
let (text, refs) =
506+
extract_image_references("see \"https://example.com/my image.png\" please");
505507
assert_eq!(refs.len(), 1, "Should detect quoted URL with spaces");
506508
assert!(
507509
matches!(&refs[0], ImageReference::WebUrl(u) if u == "https://example.com/my image.png")
@@ -513,19 +515,21 @@ mod tests {
513515
fn test_user_reported_case() {
514516
// Exact case from user report: path with space but no quotes
515517
// This will NOT work without quotes - user needs to quote it
516-
let (_text, refs) = extract_image_references(
517-
"/Users/alex/test/sofos_allowed/test_r copy.png",
518-
);
518+
let (_text, refs) =
519+
extract_image_references("/Users/alex/test/sofos_allowed/test_r copy.png");
519520
// Without quotes, this gets split into two words
520521
// Only "copy.png" would be detected as an image
521522
assert_eq!(refs.len(), 1, "Only the second part is detected as image");
522523
assert!(matches!(&refs[0], ImageReference::LocalPath(p) if p == "copy.png"));
523524

524525
// With quotes, it should work
525-
let (text, refs) = extract_image_references(
526-
"\"/Users/alex/test/sofos_allowed/test_r copy.png\"",
526+
let (text, refs) =
527+
extract_image_references("\"/Users/alex/test/sofos_allowed/test_r copy.png\"");
528+
assert_eq!(
529+
refs.len(),
530+
1,
531+
"Quoted path should be detected as single image"
527532
);
528-
assert_eq!(refs.len(), 1, "Quoted path should be detected as single image");
529533
assert!(
530534
matches!(&refs[0], ImageReference::LocalPath(p) if p == "/Users/alex/test/sofos_allowed/test_r copy.png")
531535
);

src/ui.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
use crate::history::DisplayMessage;
2+
use crate::syntax::SyntaxHighlighter;
13
use colored::Colorize;
4+
use crossterm::cursor::SetCursorStyle;
25
use crossterm::event::{self, Event, KeyCode, KeyEvent};
3-
use std::io::{self, Write};
6+
use crossterm::execute;
7+
use std::io::{self, stdout, Write};
48
use std::sync::atomic::{AtomicBool, Ordering};
59
use std::sync::Arc;
610
use std::thread;
711
use std::time::Duration;
812

9-
use crate::history::DisplayMessage;
10-
use crate::syntax::SyntaxHighlighter;
11-
1213
/// Message severity levels for consistent UI feedback
1314
#[derive(Debug, Clone, Copy, PartialEq)]
1415
pub enum MessageSeverity {
@@ -516,3 +517,18 @@ impl UI {
516517
result.chars().rev().collect()
517518
}
518519
}
520+
521+
fn set_cursor_style(style: SetCursorStyle) -> io::Result<()> {
522+
let mut out = stdout();
523+
execute!(out, style)?;
524+
out.flush()?;
525+
Ok(())
526+
}
527+
528+
pub fn set_safe_mode_cursor_style() -> io::Result<()> {
529+
set_cursor_style(SetCursorStyle::BlinkingUnderScore)
530+
}
531+
532+
pub fn set_normal_mode_cursor_style() -> io::Result<()> {
533+
set_cursor_style(SetCursorStyle::DefaultUserShape)
534+
}

0 commit comments

Comments
 (0)