mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-12 20:53:34 +08:00
feat: add ecc2 ctrl-w pane commands
This commit is contained in:
@@ -45,8 +45,15 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dashboard.is_pane_command_mode() {
|
||||||
|
if dashboard.handle_pane_command_key(key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match (key.modifiers, key.code) {
|
match (key.modifiers, key.code) {
|
||||||
(KeyModifiers::CONTROL, KeyCode::Char('c')) => break,
|
(KeyModifiers::CONTROL, KeyCode::Char('c')) => break,
|
||||||
|
(KeyModifiers::CONTROL, KeyCode::Char('w')) => dashboard.begin_pane_command_mode(),
|
||||||
(_, KeyCode::Char('q')) => break,
|
(_, KeyCode::Char('q')) => break,
|
||||||
_ if dashboard.handle_pane_navigation_key(key) => {}
|
_ if dashboard.handle_pane_navigation_key(key) => {}
|
||||||
(_, KeyCode::Tab) => dashboard.next_pane(),
|
(_, KeyCode::Tab) => dashboard.next_pane(),
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ pub struct Dashboard {
|
|||||||
selected_session: usize,
|
selected_session: usize,
|
||||||
show_help: bool,
|
show_help: bool,
|
||||||
operator_note: Option<String>,
|
operator_note: Option<String>,
|
||||||
|
pane_command_mode: bool,
|
||||||
output_follow: bool,
|
output_follow: bool,
|
||||||
output_scroll_offset: usize,
|
output_scroll_offset: usize,
|
||||||
last_output_height: usize,
|
last_output_height: usize,
|
||||||
@@ -319,6 +320,7 @@ impl Dashboard {
|
|||||||
selected_session: 0,
|
selected_session: 0,
|
||||||
show_help: false,
|
show_help: false,
|
||||||
operator_note: None,
|
operator_note: None,
|
||||||
|
pane_command_mode: false,
|
||||||
output_follow: true,
|
output_follow: true,
|
||||||
output_scroll_offset: 0,
|
output_scroll_offset: 0,
|
||||||
last_output_height: 0,
|
last_output_height: 0,
|
||||||
@@ -911,6 +913,9 @@ impl Dashboard {
|
|||||||
self.search_scope.label(),
|
self.search_scope.label(),
|
||||||
self.search_agent_filter_label()
|
self.search_agent_filter_label()
|
||||||
)
|
)
|
||||||
|
} else if self.pane_command_mode {
|
||||||
|
" Ctrl+w | [h/j/k/l] move [1-4] focus [s/v/g] layout [+/-] resize [Esc] cancel |"
|
||||||
|
.to_string()
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
@@ -918,6 +923,7 @@ impl Dashboard {
|
|||||||
let text = if self.spawn_input.is_some()
|
let text = if self.spawn_input.is_some()
|
||||||
|| self.search_input.is_some()
|
|| self.search_input.is_some()
|
||||||
|| self.search_query.is_some()
|
|| self.search_query.is_some()
|
||||||
|
|| self.pane_command_mode
|
||||||
{
|
{
|
||||||
format!(" {search_prefix}")
|
format!(" {search_prefix}")
|
||||||
} else if let Some(note) = self.operator_note.as_ref() {
|
} else if let Some(note) = self.operator_note.as_ref() {
|
||||||
@@ -997,6 +1003,8 @@ impl Dashboard {
|
|||||||
" {:<7} Focus Sessions/Output/Metrics/Log directly",
|
" {:<7} Focus Sessions/Output/Metrics/Log directly",
|
||||||
self.pane_focus_shortcuts_label()
|
self.pane_focus_shortcuts_label()
|
||||||
),
|
),
|
||||||
|
" Ctrl+w Pane command mode: h/j/k/l move, s/v/g layout, 1-4 focus, +/- resize"
|
||||||
|
.to_string(),
|
||||||
" Tab Next pane".to_string(),
|
" Tab Next pane".to_string(),
|
||||||
" S-Tab Previous pane".to_string(),
|
" S-Tab Previous pane".to_string(),
|
||||||
format!(
|
format!(
|
||||||
@@ -1081,6 +1089,18 @@ impl Dashboard {
|
|||||||
self.move_pane_focus(PaneDirection::Down);
|
self.move_pane_focus(PaneDirection::Down);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn begin_pane_command_mode(&mut self) {
|
||||||
|
self.pane_command_mode = true;
|
||||||
|
self.set_operator_note(
|
||||||
|
"pane command mode | h/j/k/l move | s/v/g layout | 1-4 focus | +/- resize"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_pane_command_mode(&self) -> bool {
|
||||||
|
self.pane_command_mode
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_pane_navigation_key(&mut self, key: KeyEvent) -> bool {
|
pub fn handle_pane_navigation_key(&mut self, key: KeyEvent) -> bool {
|
||||||
match self.cfg.pane_navigation.action_for_key(key) {
|
match self.cfg.pane_navigation.action_for_key(key) {
|
||||||
Some(PaneNavigationAction::FocusSlot(slot)) => {
|
Some(PaneNavigationAction::FocusSlot(slot)) => {
|
||||||
@@ -1107,6 +1127,37 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_pane_command_key(&mut self, key: KeyEvent) -> bool {
|
||||||
|
if !self.pane_command_mode {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pane_command_mode = false;
|
||||||
|
match key.code {
|
||||||
|
crossterm::event::KeyCode::Esc => {
|
||||||
|
self.set_operator_note("pane command cancelled".to_string());
|
||||||
|
}
|
||||||
|
crossterm::event::KeyCode::Char('h') => self.focus_pane_left(),
|
||||||
|
crossterm::event::KeyCode::Char('j') => self.focus_pane_down(),
|
||||||
|
crossterm::event::KeyCode::Char('k') => self.focus_pane_up(),
|
||||||
|
crossterm::event::KeyCode::Char('l') => self.focus_pane_right(),
|
||||||
|
crossterm::event::KeyCode::Char('1') => self.focus_pane_number(1),
|
||||||
|
crossterm::event::KeyCode::Char('2') => self.focus_pane_number(2),
|
||||||
|
crossterm::event::KeyCode::Char('3') => self.focus_pane_number(3),
|
||||||
|
crossterm::event::KeyCode::Char('4') => self.focus_pane_number(4),
|
||||||
|
crossterm::event::KeyCode::Char('+') | crossterm::event::KeyCode::Char('=') => {
|
||||||
|
self.increase_pane_size()
|
||||||
|
}
|
||||||
|
crossterm::event::KeyCode::Char('-') => self.decrease_pane_size(),
|
||||||
|
crossterm::event::KeyCode::Char('s') => self.set_pane_layout(PaneLayout::Horizontal),
|
||||||
|
crossterm::event::KeyCode::Char('v') => self.set_pane_layout(PaneLayout::Vertical),
|
||||||
|
crossterm::event::KeyCode::Char('g') => self.set_pane_layout(PaneLayout::Grid),
|
||||||
|
_ => self.set_operator_note("unknown pane command".to_string()),
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn collapse_selected_pane(&mut self) {
|
pub fn collapse_selected_pane(&mut self) {
|
||||||
if self.selected_pane == Pane::Sessions {
|
if self.selected_pane == Pane::Sessions {
|
||||||
self.set_operator_note("cannot collapse sessions pane".to_string());
|
self.set_operator_note("cannot collapse sessions pane".to_string());
|
||||||
@@ -1144,6 +1195,11 @@ impl Dashboard {
|
|||||||
self.cycle_pane_layout_with_save(&config_path, |cfg| cfg.save());
|
self.cycle_pane_layout_with_save(&config_path, |cfg| cfg.save());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_pane_layout(&mut self, layout: PaneLayout) {
|
||||||
|
let config_path = crate::config::Config::config_path();
|
||||||
|
self.set_pane_layout_with_save(layout, &config_path, |cfg| cfg.save());
|
||||||
|
}
|
||||||
|
|
||||||
fn cycle_pane_layout_with_save<F>(&mut self, config_path: &std::path::Path, save: F)
|
fn cycle_pane_layout_with_save<F>(&mut self, config_path: &std::path::Path, save: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(&Config) -> anyhow::Result<()>,
|
F: FnOnce(&Config) -> anyhow::Result<()>,
|
||||||
@@ -1176,6 +1232,43 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_pane_layout_with_save<F>(
|
||||||
|
&mut self,
|
||||||
|
layout: PaneLayout,
|
||||||
|
config_path: &std::path::Path,
|
||||||
|
save: F,
|
||||||
|
) where
|
||||||
|
F: FnOnce(&Config) -> anyhow::Result<()>,
|
||||||
|
{
|
||||||
|
if self.cfg.pane_layout == layout {
|
||||||
|
self.set_operator_note(format!("pane layout already {}", self.layout_label()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let previous_layout = self.cfg.pane_layout;
|
||||||
|
let previous_pane_size = self.pane_size_percent;
|
||||||
|
let previous_selected_pane = self.selected_pane;
|
||||||
|
|
||||||
|
self.cfg.pane_layout = layout;
|
||||||
|
self.pane_size_percent = configured_pane_size(&self.cfg, self.cfg.pane_layout);
|
||||||
|
self.persist_current_pane_size();
|
||||||
|
self.ensure_selected_pane_visible();
|
||||||
|
|
||||||
|
match save(&self.cfg) {
|
||||||
|
Ok(()) => self.set_operator_note(format!(
|
||||||
|
"pane layout set to {} | saved to {}",
|
||||||
|
self.layout_label(),
|
||||||
|
config_path.display()
|
||||||
|
)),
|
||||||
|
Err(error) => {
|
||||||
|
self.cfg.pane_layout = previous_layout;
|
||||||
|
self.pane_size_percent = previous_pane_size;
|
||||||
|
self.selected_pane = previous_selected_pane;
|
||||||
|
self.set_operator_note(format!("failed to persist pane layout: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn auto_split_layout_after_spawn(&mut self, spawned_count: usize) -> Option<String> {
|
fn auto_split_layout_after_spawn(&mut self, spawned_count: usize) -> Option<String> {
|
||||||
let config_path = crate::config::Config::config_path();
|
let config_path = crate::config::Config::config_path();
|
||||||
self.auto_split_layout_after_spawn_with_save(spawned_count, &config_path, |cfg| cfg.save())
|
self.auto_split_layout_after_spawn_with_save(spawned_count, &config_path, |cfg| cfg.save())
|
||||||
@@ -8471,6 +8564,52 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
assert_eq!(dashboard.pane_move_shortcuts_label(), "a/s/w/d");
|
assert_eq!(dashboard.pane_move_shortcuts_label(), "a/s/w/d");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pane_command_mode_handles_focus_and_cancel() {
|
||||||
|
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||||
|
|
||||||
|
dashboard.begin_pane_command_mode();
|
||||||
|
assert!(dashboard.is_pane_command_mode());
|
||||||
|
|
||||||
|
assert!(dashboard.handle_pane_command_key(KeyEvent::new(
|
||||||
|
crossterm::event::KeyCode::Char('3'),
|
||||||
|
crossterm::event::KeyModifiers::NONE,
|
||||||
|
)));
|
||||||
|
assert_eq!(dashboard.selected_pane, Pane::Metrics);
|
||||||
|
assert!(!dashboard.is_pane_command_mode());
|
||||||
|
|
||||||
|
dashboard.begin_pane_command_mode();
|
||||||
|
assert!(dashboard.handle_pane_command_key(KeyEvent::new(
|
||||||
|
crossterm::event::KeyCode::Esc,
|
||||||
|
crossterm::event::KeyModifiers::NONE,
|
||||||
|
)));
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.operator_note.as_deref(),
|
||||||
|
Some("pane command cancelled")
|
||||||
|
);
|
||||||
|
assert!(!dashboard.is_pane_command_mode());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pane_command_mode_sets_layout() {
|
||||||
|
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||||
|
dashboard.cfg.pane_layout = PaneLayout::Horizontal;
|
||||||
|
|
||||||
|
dashboard.begin_pane_command_mode();
|
||||||
|
assert!(dashboard.handle_pane_command_key(KeyEvent::new(
|
||||||
|
crossterm::event::KeyCode::Char('g'),
|
||||||
|
crossterm::event::KeyModifiers::NONE,
|
||||||
|
)));
|
||||||
|
|
||||||
|
assert_eq!(dashboard.cfg.pane_layout, PaneLayout::Grid);
|
||||||
|
assert!(
|
||||||
|
dashboard
|
||||||
|
.operator_note
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|note| note.contains("pane layout set to grid | saved to "))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_pane_layout_rotates_and_hides_log_when_leaving_grid() {
|
fn cycle_pane_layout_rotates_and_hides_log_when_leaving_grid() {
|
||||||
let mut dashboard = test_dashboard(Vec::new(), 0);
|
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||||
@@ -8761,6 +8900,7 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
selected_session,
|
selected_session,
|
||||||
show_help: false,
|
show_help: false,
|
||||||
operator_note: None,
|
operator_note: None,
|
||||||
|
pane_command_mode: false,
|
||||||
output_follow: true,
|
output_follow: true,
|
||||||
output_scroll_offset: 0,
|
output_scroll_offset: 0,
|
||||||
last_output_height: 0,
|
last_output_height: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user