feat(tui): add double‑Ctrl+C quick‑exit and update command help texts
- Introduce “Ctrl+C twice” shortcut for quitting the application and display corresponding help line. - Rename and clarify session‑related commands (`:session save`) and add short aliases (`:w[!]`, `:q[!]`, `:wq[!]`) with updated help entries. - Adjust quit help text to remove `:q, :quit` redundancy and replace with the new quick‑exit hint. - Update UI key hint to show only “Esc” for cancel actions. - Implement double‑Ctrl+C detection in `ChatApp` using `DOUBLE_CTRL_C_WINDOW`, track `last_ctrl_c`, reset on other keys, and show status messages prompting the second press. - Minor wording tweaks in help dialogs and README to reflect the new command syntax and quick‑exit behavior.
This commit is contained in:
@@ -90,7 +90,8 @@ OWLEN uses a modal, vim-inspired interface. Press `F1` (available from any mode)
|
||||
|
||||
- **Normal Mode**: Navigate with `h/j/k/l`, `w/b`, `gg/G`.
|
||||
- **Editing Mode**: Enter with `i` or `a`. Send messages with `Enter`.
|
||||
- **Command Mode**: Enter with `:`. Access commands like `:quit`, `:save`, `:theme`.
|
||||
- **Command Mode**: Enter with `:`. Access commands like `:quit`, `:w`, `:session save`, `:theme`.
|
||||
- **Quick Exit**: Press `Ctrl+C` twice in Normal mode to quit quickly (first press still cancels active generations).
|
||||
- **Tutorial Command**: Type `:tutorial` any time for a quick summary of the most important keybindings.
|
||||
- **MCP Slash Commands**: Owlen auto-registers zero-argument MCP tools as slash commands—type `/mcp__github__list_prs` (for example) to pull remote context directly into the chat log.
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ const FOCUS_CHORD_TIMEOUT: Duration = Duration::from_millis(1200);
|
||||
const RESIZE_DOUBLE_TAP_WINDOW: Duration = Duration::from_millis(450);
|
||||
const RESIZE_STEP: f32 = 0.05;
|
||||
const RESIZE_SNAP_VALUES: [f32; 3] = [0.5, 0.75, 0.25];
|
||||
const DOUBLE_CTRL_C_WINDOW: Duration = Duration::from_millis(1500);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SlashOutcome {
|
||||
@@ -251,18 +252,19 @@ pub struct ChatApp {
|
||||
last_resize_tap: Option<(PaneDirection, Instant)>, // For Alt+arrow double-tap detection
|
||||
resize_snap_index: usize, // Cycles through 25/50/75 snaps
|
||||
last_snap_direction: Option<PaneDirection>,
|
||||
file_tree: FileTreeState, // Workspace file tree state
|
||||
file_icons: FileIconResolver, // Icon resolver with Nerd/ASCII fallback
|
||||
file_panel_collapsed: bool, // Whether the file panel is collapsed
|
||||
file_panel_width: u16, // Cached file panel width
|
||||
saved_sessions: Vec<SessionMeta>, // Cached list of saved sessions
|
||||
selected_session_index: usize, // Index of selected session in browser
|
||||
help_tab_index: usize, // Currently selected help tab (0-(HELP_TAB_COUNT-1))
|
||||
theme: Theme, // Current theme
|
||||
last_ctrl_c: Option<Instant>, // Track timing for double Ctrl+C quit
|
||||
file_tree: FileTreeState, // Workspace file tree state
|
||||
file_icons: FileIconResolver, // Icon resolver with Nerd/ASCII fallback
|
||||
file_panel_collapsed: bool, // Whether the file panel is collapsed
|
||||
file_panel_width: u16, // Cached file panel width
|
||||
saved_sessions: Vec<SessionMeta>, // Cached list of saved sessions
|
||||
selected_session_index: usize, // Index of selected session in browser
|
||||
help_tab_index: usize, // Currently selected help tab (0-(HELP_TAB_COUNT-1))
|
||||
theme: Theme, // Current theme
|
||||
available_themes: Vec<String>, // Cached list of theme names
|
||||
selected_theme_index: usize, // Index of selected theme in browser
|
||||
selected_theme_index: usize, // Index of selected theme in browser
|
||||
pending_consent: Option<ConsentDialogState>, // Pending consent request
|
||||
system_status: String, // System/status messages (tool execution, status, etc)
|
||||
system_status: String, // System/status messages (tool execution, status, etc)
|
||||
toasts: ToastManager,
|
||||
/// Simple execution budget: maximum number of tool calls allowed per session.
|
||||
_execution_budget: usize,
|
||||
@@ -488,6 +490,7 @@ impl ChatApp {
|
||||
last_resize_tap: None,
|
||||
resize_snap_index: 0,
|
||||
last_snap_direction: None,
|
||||
last_ctrl_c: None,
|
||||
file_tree,
|
||||
file_icons,
|
||||
file_panel_collapsed: true,
|
||||
@@ -3992,6 +3995,12 @@ impl ChatApp {
|
||||
pub async fn handle_event(&mut self, event: Event) -> Result<AppState> {
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
|
||||
if let Some(last) = self.last_ctrl_c
|
||||
&& last.elapsed() > DOUBLE_CTRL_C_WINDOW
|
||||
{
|
||||
self.last_ctrl_c = None;
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Tick => {
|
||||
self.poll_repo_search();
|
||||
@@ -4012,6 +4021,15 @@ impl ChatApp {
|
||||
// Ignore paste events in other modes
|
||||
}
|
||||
Event::Key(key) => {
|
||||
let is_ctrl_c = matches!(
|
||||
(key.code, key.modifiers),
|
||||
(KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
if !is_ctrl_c {
|
||||
self.last_ctrl_c = None;
|
||||
}
|
||||
|
||||
// Handle consent dialog first (highest priority)
|
||||
if let Some(consent_state) = &self.pending_consent {
|
||||
match key.code {
|
||||
@@ -4259,16 +4277,30 @@ impl ChatApp {
|
||||
self.handle_workspace_resize(PaneDirection::Down);
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
(KeyCode::Char('q'), KeyModifiers::NONE) => {
|
||||
return Ok(AppState::Quit);
|
||||
}
|
||||
(KeyCode::Char('c'), modifiers)
|
||||
if modifiers.contains(KeyModifiers::CONTROL) =>
|
||||
{
|
||||
if self.cancel_active_generation()? {
|
||||
self.last_ctrl_c = None;
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
return Ok(AppState::Quit);
|
||||
|
||||
let now = Instant::now();
|
||||
if let Some(last) = self.last_ctrl_c
|
||||
&& now.duration_since(last) <= DOUBLE_CTRL_C_WINDOW
|
||||
{
|
||||
self.status = "Exiting…".to_string();
|
||||
self.set_system_status(String::new());
|
||||
self.last_ctrl_c = None;
|
||||
return Ok(AppState::Quit);
|
||||
}
|
||||
|
||||
self.last_ctrl_c = Some(now);
|
||||
self.status = "Press Ctrl+C again to quit".to_string();
|
||||
self.set_system_status(
|
||||
"Press Ctrl+C again to quit OWLEN".to_string(),
|
||||
);
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
(KeyCode::Char('j'), modifiers)
|
||||
if modifiers.contains(KeyModifiers::CONTROL) =>
|
||||
|
||||
@@ -3171,8 +3171,9 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
.fg(theme.user_message_role),
|
||||
)]),
|
||||
Line::from(" :h, :help → show this help"),
|
||||
Line::from(" :q, :quit → quit application"),
|
||||
Line::from(" :reload → reload configuration and themes"),
|
||||
Line::from(" :quit → quit application"),
|
||||
Line::from(" Ctrl+C twice → quit application"),
|
||||
Line::from(" :reload → reload configuration and themes"),
|
||||
Line::from(" :layout save/load → persist or restore pane layout"),
|
||||
Line::from(""),
|
||||
Line::from(vec![Span::styled(
|
||||
@@ -3200,8 +3201,7 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(theme.user_message_role),
|
||||
)]),
|
||||
Line::from(" :save [name] → save current session (with optional name)"),
|
||||
Line::from(" :w [name] → alias for :save"),
|
||||
Line::from(" :session save [name] → save current session (optional name)"),
|
||||
Line::from(" :load, :o → browse and load saved sessions"),
|
||||
Line::from(" :sessions, :ls → browse saved sessions"),
|
||||
Line::from(""),
|
||||
@@ -3223,6 +3223,9 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
)]),
|
||||
Line::from(" :open <path> → open file in code side panel"),
|
||||
Line::from(" :close → close the code side panel"),
|
||||
Line::from(" :w[!] [path] → write active file (optionally to path)"),
|
||||
Line::from(" :q[!] → close active file (append ! to discard)"),
|
||||
Line::from(" :wq[!] [path] → save then close active file"),
|
||||
// New mode and tool commands added in phases 0‑5
|
||||
Line::from(" :code → switch to code mode (CLI: owlen --code)"),
|
||||
Line::from(" :mode <chat|code> → change current mode explicitly"),
|
||||
@@ -3244,8 +3247,8 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(theme.user_message_role),
|
||||
)]),
|
||||
Line::from(" :save → save with auto-generated name"),
|
||||
Line::from(" :save my-session → save with custom name"),
|
||||
Line::from(" :session save → save with auto-generated name"),
|
||||
Line::from(" :session save my-session → save with custom name"),
|
||||
Line::from(" • AI generates description automatically (configurable)"),
|
||||
Line::from(" • Sessions stored in platform-specific directories"),
|
||||
Line::from(""),
|
||||
@@ -3386,7 +3389,7 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
),
|
||||
Span::raw(":Jump "),
|
||||
Span::styled(
|
||||
"Esc/q",
|
||||
"Esc",
|
||||
Style::default()
|
||||
.fg(theme.focused_panel_border)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
@@ -3416,7 +3419,7 @@ fn render_session_browser(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
Line::from(""),
|
||||
Line::from("No saved sessions found."),
|
||||
Line::from(""),
|
||||
Line::from("Save your current session with :save [name]"),
|
||||
Line::from("Save your current session with :session save [name]"),
|
||||
Line::from(""),
|
||||
Line::from("Press Esc to close."),
|
||||
];
|
||||
@@ -3650,7 +3653,7 @@ fn render_theme_browser(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
|
||||
let footer = Paragraph::new(vec![
|
||||
Line::from(""),
|
||||
Line::from("↑/↓ or j/k: Navigate · Enter: Apply theme · g/G: Top/Bottom · Esc/q: Cancel"),
|
||||
Line::from("↑/↓ or j/k: Navigate · Enter: Apply theme · g/G: Top/Bottom · Esc: Cancel"),
|
||||
])
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::default().fg(theme.placeholder).bg(theme.background));
|
||||
|
||||
Reference in New Issue
Block a user