diff --git a/docs/implementations.md b/docs/implementations.md index 616e37f..7465d71 100644 --- a/docs/implementations.md +++ b/docs/implementations.md @@ -68,7 +68,6 @@ In host cli: In vibebox: -- use `:new` to prompt user to delete and create a session. - use `:exit` to exit vibebox. ### (Shows all the mounts/network/visibility) diff --git a/docs/tasks.md b/docs/tasks.md index c88f256..5f84524 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -41,7 +41,7 @@ 4. [x] use vm.lock to ensure process concurrency safety. 5. [x] wire up SessionManager. 6. [x] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself). -7. [ ] setup vibebox commands +7. [x] setup vibebox commands 8. [ ] setup cli commands. 9. [ ] fix ui overlap. 10. [ ] intensive integration test. diff --git a/src/bin/vibebox-cli.rs b/src/bin/vibebox-cli.rs index 61a3fd5..6e35e3c 100644 --- a/src/bin/vibebox-cli.rs +++ b/src/bin/vibebox-cli.rs @@ -9,9 +9,9 @@ use color_eyre::Result; use tracing_subscriber::EnvFilter; use vibebox::tui::{AppState, VmInfo}; -use vibebox::{SessionManager, config, instance, tui, vm, vm_manager}; +use vibebox::{SessionManager, commands, config, instance, tui, vm, vm_manager}; -const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 3000; +const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 30000; fn main() -> Result<()> { init_tracing(); @@ -65,7 +65,8 @@ fn main() -> Result<()> { } else { tracing::warn!("failed to initialize session manager"); } - let app = Arc::new(Mutex::new(AppState::new(cwd.clone(), vm_info))); + let commands = commands::build_commands(); + let app = Arc::new(Mutex::new(AppState::new(cwd.clone(), vm_info, commands))); { let mut locked = app.lock().expect("app state poisoned"); diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..700889f --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,111 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use crate::tui::{AppState, VibeboxCommands}; +use crate::vm::IoControl; + +#[derive(Clone, Copy)] +enum CommandKind { + Help, + Exit, +} + +struct CommandSpec { + name: &'static str, + description: &'static str, + kind: CommandKind, + shell_alias: Option<&'static str>, +} + +const COMMAND_SPECS: &[CommandSpec] = &[ + CommandSpec { + name: ":help", + description: "Show Vibebox commands.", + kind: CommandKind::Help, + shell_alias: Some("vibebox_help"), + }, + CommandSpec { + name: ":exit", + description: "Exit Vibebox.", + kind: CommandKind::Exit, + shell_alias: Some("exit"), + }, +]; + +pub struct CommandHandlers { + handlers: HashMap>, +} + +impl CommandHandlers { + fn new() -> Self { + Self { + handlers: HashMap::new(), + } + } + + fn register(&mut self, name: &str, handler: F) + where + F: Fn() + Send + Sync + 'static, + { + self.handlers.insert(name.to_string(), Box::new(handler)); + } + + pub fn handle(&self, line: &str) -> bool { + if let Some(handler) = self.handlers.get(line) { + handler(); + true + } else { + false + } + } +} + +pub fn build_commands() -> VibeboxCommands { + let mut commands = VibeboxCommands::new_empty(); + for spec in COMMAND_SPECS { + commands.add_command(spec.name, spec.description); + } + commands +} + +pub fn render_shell_script() -> String { + let mut lines = Vec::new(); + lines.push("vibebox_help() {".to_string()); + lines.push(" cat <<'VIBEBOX_HELP'".to_string()); + lines.push("Vibebox Commands".to_string()); + for spec in COMMAND_SPECS { + lines.push(format!("{} {}", spec.name, spec.description)); + } + lines.push("VIBEBOX_HELP".to_string()); + lines.push("}".to_string()); + for spec in COMMAND_SPECS { + if let Some(alias) = spec.shell_alias { + lines.push(format!("alias {}='{}'", spec.name, alias)); + } + } + lines.join("\n") +} + +pub fn build_handlers(app: Arc>, io_control: Arc) -> CommandHandlers { + let mut handlers = CommandHandlers::new(); + for spec in COMMAND_SPECS { + match spec.kind { + CommandKind::Help => { + let app = app.clone(); + handlers.register(spec.name, move || { + if let Ok(mut locked) = app.lock() { + let _ = crate::tui::render_commands_component(&mut locked); + } + }); + } + CommandKind::Exit => { + let io_control = io_control.clone(); + handlers.register(spec.name, move || { + io_control.request_terminal_restore(); + std::process::exit(0); + }); + } + } + } + handlers +} diff --git a/src/instance.rs b/src/instance.rs index 287ec80..4773a59 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -19,8 +19,9 @@ use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use uuid::Uuid; use crate::{ + commands, session_manager::{INSTANCE_DIR_NAME, INSTANCE_TOML_FILENAME}, - tui::{self, AppState}, + tui::AppState, vm::{self, LoginAction, VmInput}, }; @@ -344,7 +345,8 @@ pub(crate) fn build_ssh_login_actions( .replace("__SSH_USER__", &ssh_user) .replace("__SUDO_PASSWORD__", &sudo_password) .replace("__PROJECT_NAME__", project_name) - .replace("__KEY_PATH__", &key_path); + .replace("__KEY_PATH__", &key_path) + .replace("__VIBEBOX_SHELL_SCRIPT__", &commands::render_shell_script()); let setup = vm::script_command_from_content("ssh_setup", &setup_script) .expect("ssh setup script contained invalid marker"); @@ -385,18 +387,8 @@ fn spawn_ssh_io( let mut line_buf = String::new(); - let on_line = { - let app = app.clone(); - move |line: &str| { - if line == ":help" { - if let Ok(mut locked) = app.lock() { - let _ = tui::render_commands_component(&mut locked); - } - return true; - } - false - } - }; + let handlers = commands::build_handlers(app.clone(), io_control.clone()); + let on_line = move |line: &str| handlers.handle(line); let on_output = move |bytes: &[u8]| { if ssh_connected_for_output.load(Ordering::SeqCst) { diff --git a/src/lib.rs b/src/lib.rs index 5bd766f..7f4f82d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod commands; pub mod instance; pub mod session_manager; pub mod tui; diff --git a/src/ssh.sh b/src/ssh.sh index 1caf118..6492cf3 100644 --- a/src/ssh.sh +++ b/src/ssh.sh @@ -77,6 +77,21 @@ if [ ! -e "${USER_HOME}/.claude" ]; then fi chown -h "${SSH_USER}:${SSH_USER}" "${USER_HOME}/.codex" "${USER_HOME}/.claude" 2>/dev/null || true +# Vibebox shell commands +install -d -m 755 /etc/profile.d +cat > /etc/profile.d/vibebox.sh <<'VIBEBOX_SHELL_EOF' +__VIBEBOX_SHELL_SCRIPT__ +VIBEBOX_SHELL_EOF +chmod 644 /etc/profile.d/vibebox.sh + +if ! grep -q "vibebox-aliases" "${USER_HOME}/.bashrc" 2>/dev/null; then + { + echo "" + echo "# vibebox-aliases" + echo ". /etc/profile.d/vibebox.sh" + } >> "${USER_HOME}/.bashrc" +fi + # Install Mise MISE_BIN="${USER_HOME}/.local/bin/mise" if [ ! -x "$MISE_BIN" ] && ! command -v mise >/dev/null 2>&1; then diff --git a/src/tui.rs b/src/tui.rs index af026f9..28cc430 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -50,11 +50,7 @@ pub struct AppState { } impl AppState { - pub fn new(cwd: PathBuf, vm_info: VmInfo) -> Self { - let mut commands = VibeboxCommands::default(); - commands.add_command(":new", "Create a new session."); - commands.add_command(":exit", "Exit Vibebox."); - + pub fn new(cwd: PathBuf, vm_info: VmInfo, commands: VibeboxCommands) -> Self { Self { cwd, vm_info, @@ -69,6 +65,10 @@ pub struct VibeboxCommands { } impl VibeboxCommands { + pub fn new_empty() -> Self { + Self { items: Vec::new() } + } + pub fn add_command(&mut self, name: impl Into, description: impl Into) { self.items.push(VibeboxCommand { name: name.into(),