Skip to content

Commit 3c37abe

Browse files
committed
feat: implement node switching in interactive mode
- Added special commands starting with ! for node control - !node<N> or !n<N> to switch to specific node (1-indexed) - !all to activate all nodes - !list to show all nodes with their status - !status to show currently active nodes - !help to show available commands - Visual indicators in prompt show active nodes - Commands only sent to active nodes - Comprehensive test coverage for node switching The prompt shows: - [● ● ●] when all nodes are active - [1 · 2] (2/3) when specific nodes are active - Numbers show active nodes, dots show inactive
1 parent 1961ca2 commit 3c37abe

2 files changed

Lines changed: 445 additions & 19 deletions

File tree

src/commands/interactive.rs

Lines changed: 166 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ struct NodeSession {
6767
channel: Channel<Msg>,
6868
working_dir: String,
6969
is_connected: bool,
70+
is_active: bool, // Whether this node is currently active for commands
7071
}
7172

7273
impl NodeSession {
@@ -244,6 +245,7 @@ impl InteractiveCommand {
244245
channel,
245246
working_dir,
246247
is_connected: true,
248+
is_active: true, // All nodes start as active
247249
})
248250
}
249251

@@ -386,6 +388,108 @@ impl InteractiveCommand {
386388
Ok(commands_executed)
387389
}
388390

391+
/// Parse and handle special commands (starting with !)
392+
fn handle_special_command(command: &str, sessions: &mut [NodeSession]) -> Result<bool> {
393+
if !command.starts_with('!') {
394+
return Ok(false); // Not a special command
395+
}
396+
397+
let cmd = command.trim_start_matches('!').to_lowercase();
398+
399+
match cmd.as_str() {
400+
"all" => {
401+
// Activate all nodes
402+
for session in sessions.iter_mut() {
403+
if session.is_connected {
404+
session.is_active = true;
405+
}
406+
}
407+
println!("All nodes activated");
408+
Ok(true)
409+
}
410+
"list" | "nodes" | "ls" => {
411+
// List all nodes with their status
412+
println!("\nNodes status:");
413+
for (i, session) in sessions.iter().enumerate() {
414+
let status = if !session.is_connected {
415+
"disconnected"
416+
} else if session.is_active {
417+
"active"
418+
} else {
419+
"inactive"
420+
};
421+
println!(" [{}] {} - {}", i + 1, session.node, status);
422+
}
423+
println!();
424+
Ok(true)
425+
}
426+
"status" => {
427+
// Show current active nodes
428+
let active_nodes: Vec<String> = sessions
429+
.iter()
430+
.filter(|s| s.is_active && s.is_connected)
431+
.map(|s| s.node.to_string())
432+
.collect();
433+
434+
if active_nodes.is_empty() {
435+
println!("No active nodes");
436+
} else {
437+
println!("Active nodes: {}", active_nodes.join(", "));
438+
}
439+
Ok(true)
440+
}
441+
"help" | "?" => {
442+
println!("\nSpecial commands:");
443+
println!(" !all - Activate all nodes");
444+
println!(" !node<N> - Switch to node N (e.g., !node1)");
445+
println!(" !n<N> - Shorthand for !node<N>");
446+
println!(" !list, !nodes - List all nodes with status");
447+
println!(" !status - Show active nodes");
448+
println!(" !help - Show this help");
449+
println!(" exit - Exit interactive mode");
450+
println!();
451+
Ok(true)
452+
}
453+
_ => {
454+
// Check for node selection commands
455+
if let Some(node_num) = cmd.strip_prefix("node") {
456+
Self::switch_to_node(node_num, sessions)
457+
} else if let Some(node_num) = cmd.strip_prefix('n') {
458+
Self::switch_to_node(node_num, sessions)
459+
} else {
460+
println!("Unknown command: !{cmd}. Type !help for available commands.");
461+
Ok(true)
462+
}
463+
}
464+
}
465+
}
466+
467+
/// Switch to a specific node by number
468+
fn switch_to_node(node_num: &str, sessions: &mut [NodeSession]) -> Result<bool> {
469+
match node_num.parse::<usize>() {
470+
Ok(num) if num > 0 && num <= sessions.len() => {
471+
// Deactivate all nodes first
472+
for session in sessions.iter_mut() {
473+
session.is_active = false;
474+
}
475+
476+
// Activate the selected node
477+
let index = num - 1;
478+
if sessions[index].is_connected {
479+
sessions[index].is_active = true;
480+
println!("Switched to node {}: {}", num, sessions[index].node);
481+
} else {
482+
println!("Node {num} is disconnected");
483+
}
484+
Ok(true)
485+
}
486+
_ => {
487+
println!("Invalid node number. Use 1-{}", sessions.len());
488+
Ok(true)
489+
}
490+
}
491+
}
492+
389493
/// Run interactive mode with multiple nodes (multiplex)
390494
async fn run_multiplex_mode(&self, mut sessions: Vec<NodeSession>) -> Result<usize> {
391495
let mut commands_executed = 0;
@@ -404,7 +508,7 @@ impl InteractiveCommand {
404508
"Interactive multiplex mode started. Commands will be sent to all {} nodes.",
405509
sessions.len()
406510
);
407-
println!("Type 'exit' or press Ctrl+D to quit.");
511+
println!("Type 'exit' or press Ctrl+D to quit. Type '!help' for special commands.");
408512
println!();
409513

410514
// Main interactive loop
@@ -414,51 +518,94 @@ impl InteractiveCommand {
414518
println!("\nInterrupted by user. Exiting...");
415519
break;
416520
}
417-
// Show node status
418-
print!("[");
419-
for (i, session) in sessions.iter().enumerate() {
420-
if i > 0 {
421-
print!(" ");
521+
// Build prompt with node status
522+
let active_count = sessions
523+
.iter()
524+
.filter(|s| s.is_active && s.is_connected)
525+
.count();
526+
let total_connected = sessions.iter().filter(|s| s.is_connected).count();
527+
528+
let prompt = if active_count == total_connected {
529+
// All nodes active - show simple status
530+
let mut status = String::from("[");
531+
for (i, session) in sessions.iter().enumerate() {
532+
if i > 0 {
533+
status.push(' ');
534+
}
535+
if session.is_connected {
536+
status.push_str(&"●".green().to_string());
537+
} else {
538+
status.push_str(&"○".red().to_string());
539+
}
422540
}
423-
if session.is_connected {
424-
print!("{}", "●".green());
425-
} else {
426-
print!("{}", "○".red());
541+
status.push_str("] bssh> ");
542+
status
543+
} else {
544+
// Some nodes inactive - show which are active
545+
let mut status = String::from("[");
546+
for (i, session) in sessions.iter().enumerate() {
547+
if i > 0 {
548+
status.push(' ');
549+
}
550+
if !session.is_connected {
551+
status.push_str(&"○".red().to_string());
552+
} else if session.is_active {
553+
status.push_str(&format!("{}", (i + 1).to_string().green()));
554+
} else {
555+
status.push_str(&"·".yellow().to_string());
556+
}
427557
}
428-
}
429-
print!("] ");
430-
io::stdout().flush()?;
558+
status.push_str(&format!("] ({active_count}/{total_connected}) bssh> "));
559+
status
560+
};
431561

432562
// Read input
433-
let prompt = "bssh> ";
434-
match rl.readline(prompt) {
563+
match rl.readline(&prompt) {
435564
Ok(line) => {
436565
if line.trim() == "exit" {
437566
break;
438567
}
439568

569+
// Check for special commands first
570+
if line.trim().starts_with('!')
571+
&& Self::handle_special_command(&line, &mut sessions)?
572+
{
573+
continue; // Command was handled, continue to next iteration
574+
}
575+
440576
rl.add_history_entry(&line)?;
441577

442-
// Send command to all connected nodes
578+
// Send command only to active nodes
579+
let mut command_sent = false;
443580
for session in &mut sessions {
444-
if session.is_connected {
581+
if session.is_connected && session.is_active {
445582
if let Err(e) = session.send_command(&line).await {
446583
eprintln!(
447584
"Failed to send command to {}: {}",
448585
session.node.to_string().red(),
449586
e
450587
);
451588
session.is_connected = false;
589+
} else {
590+
command_sent = true;
452591
}
453592
}
454593
}
455-
commands_executed += 1;
594+
595+
if command_sent {
596+
commands_executed += 1;
597+
} else {
598+
eprintln!(
599+
"No active nodes to send command to. Use !list to see nodes or !all to activate all."
600+
);
601+
continue;
602+
}
456603

457604
// Wait a bit for output and collect from all nodes
458605
tokio::time::sleep(Duration::from_millis(500)).await;
459606

460607
for session in &mut sessions {
461-
if session.is_connected {
608+
if session.is_connected && session.is_active {
462609
while let Ok(Some(output)) = session.read_output().await {
463610
// Print output with node prefix
464611
for line in output.lines() {

0 commit comments

Comments
 (0)