diff --git a/README.md b/README.md index 6d07900..09fa4fe 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Simple detachable shell sessions with zero configuration. Not a complex multiple - ๐Ÿงน **Auto-Cleanup**: Automatic cleanup of dead sessions - ๐Ÿ”„ **Session Switching**: Simple attach/detach without complex multiplexing - ๐Ÿท๏ธ **Named Sessions**: Give meaningful names to your sessions +- ๐Ÿ‘๏ธ **Session Indicators**: Visual indicators showing you're in an NDS session - ๐Ÿง **Cross-Platform**: Works on Linux and macOS ## ๐ŸŽฏ Philosophy @@ -55,13 +56,40 @@ sudo cp target/release/nds /usr/local/bin/ ``` +## ๐ŸŽฌ Demo + +See NDS in action with key features: + +```bash +$ nds new "web-server" +Creating new session 'web-server'... +โฌข user@host:~/project$ # โ† Notice session indicator! + +$ nds list +Active Sessions: +ID Name Status PID Age +a03fa0b6 web-server running 84405 2m + +$ export NDS_PROMPT_STYLE=full +[nds:web-server] user@host:~/project$ # โ† Customizable indicators + +$ nds # Interactive TUI mode +โ”Œโ”€ NDS Session Manager โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ > web-server running 2m โ”‚ +โ”‚ data-proc running 1m โ”‚ +โ”‚ [a]ttach [k]ill [q]uit โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +> ๐ŸŽฅ **Interactive Demo Coming Soon**: Full asciinema recording showcasing all features + ## ๐Ÿš€ Quick Start ```bash # Create a new session nds new -# List sessions +# List sessions nds list # Attach to a session @@ -134,6 +162,42 @@ nds history -s abc123 # History for specific session - `Ctrl+D` - Detach from current session (when at empty prompt) - `Enter, ~s` - Switch to another session interactively +### Session Indicators + +NDS automatically shows you when you're inside a session with subtle visual indicators: + +- **Terminal Title**: Shows `NDS: session-name` in your terminal window title +- **Prompt Indicator**: Adds a visual prefix to your shell prompt + +#### Indicator Styles + +Control how session indicators appear with the `NDS_PROMPT_STYLE` environment variable: + +```bash +# Subtle symbol (default) - shows โฌข prefix +export NDS_PROMPT_STYLE=subtle +โฌข user@host:~$ + +# Full session info - shows [nds:session-name] prefix +export NDS_PROMPT_STYLE=full +[nds:my-project] user@host:~$ + +# Minimal - shows [nds] prefix +export NDS_PROMPT_STYLE=minimal +[nds] user@host:~$ + +# Disable indicators completely +export NDS_PROMPT_STYLE=none +user@host:~$ +``` + +#### How It Works + +- **Automatic**: No configuration files to modify - works out of the box +- **Shell Agnostic**: Works with bash, zsh, and other shells +- **Non-invasive**: Doesn't modify your existing shell configuration +- **User Control**: Can be customized or disabled entirely + ## ๐Ÿ—๏ธ Architecture NDS uses a simple and robust architecture: @@ -212,6 +276,11 @@ export NDS_SHELL=/bin/zsh NDS_SESSION_ID # Current session ID when attached NDS_SESSION_NAME # Current session name (if set) +# Session indicator customization +export NDS_PROMPT_STYLE=subtle # Options: subtle, full, minimal, none +NDS_SESSION_DISPLAY # Display name used in indicators (auto-set) +NDS_PROMPT_PREFIX # Computed prefix based on style (auto-set) + # Change detach key binding (coming soon) export NDS_DETACH_KEY="ctrl-a d" ``` diff --git a/src/pty/spawn.rs b/src/pty/spawn.rs index c1c8e20..75662b4 100644 --- a/src/pty/spawn.rs +++ b/src/pty/spawn.rs @@ -243,19 +243,23 @@ impl PtyProcess { std::env::set_var("NDS_SESSION_NAME", session_id); } + // Set session indicator environment variables + Self::set_session_indicator_env(session_id, &name); + // Set restrictive umask for session isolation unsafe { libc::umask(0o077); // Only owner can read/write/execute new files } - // Get shell + // Get shell and prepare with session indicator let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()); + + // Create shell startup command with session indicator setup + let shell_args = Self::prepare_shell_with_indicator(&shell, session_id, &name)?; - // Execute shell - let shell_cstr = std::ffi::CString::new(shell.as_str()).unwrap(); - let args = vec![shell_cstr.clone()]; - - execvp(&shell_cstr, &args) + // Execute shell with prepared arguments + let shell_cstr = std::ffi::CString::new(shell_args.0.as_str()).unwrap(); + execvp(&shell_cstr, &shell_args.1) .map_err(|e| NdsError::ProcessError(format!("execvp failed: {}", e)))?; // Should never reach here @@ -273,6 +277,9 @@ impl PtyProcess { session.name.as_ref().unwrap_or(&session.id), ); + // Set session indicator environment variables for attach + Self::set_session_indicator_env(&session.id, &session.name); + // Save current terminal state let stdin_fd = 0; let original_termios = save_terminal_state(stdin_fd)?; @@ -867,6 +874,141 @@ impl PtyProcess { Ok(()) } + + /// Set session indicator environment variables + fn set_session_indicator_env(session_id: &str, name: &Option) { + // Set prompt style (user can override with NDS_PROMPT_STYLE=none to disable) + if std::env::var("NDS_PROMPT_STYLE").is_err() { + std::env::set_var("NDS_PROMPT_STYLE", "subtle"); + } + + // Set session display name for prompt + let display_name = if let Some(ref session_name) = name { + session_name.clone() + } else { + // Use first 6 chars of session ID for brevity + session_id.chars().take(6).collect::() + }; + std::env::set_var("NDS_SESSION_DISPLAY", &display_name); + + // Set prompt prefix based on style + let style = std::env::var("NDS_PROMPT_STYLE").unwrap_or_else(|_| "subtle".to_string()); + let prefix = match style.as_str() { + "full" => format!("[nds:{}]", display_name), + "minimal" => "[nds]".to_string(), + "symbol" => "โฌข".to_string(), + "none" => String::new(), + _ => "โฌข".to_string(), // default to subtle symbol + }; + std::env::set_var("NDS_PROMPT_PREFIX", &prefix); + } + + /// Prepare shell execution with session indicator setup + fn prepare_shell_with_indicator( + shell: &str, + session_id: &str, + name: &Option, + ) -> Result<(String, Vec)> { + let prompt_style = std::env::var("NDS_PROMPT_STYLE").unwrap_or_else(|_| "subtle".to_string()); + + // If disabled, just return normal shell + if prompt_style == "none" { + let shell_cstr = std::ffi::CString::new(shell).unwrap(); + return Ok((shell.to_string(), vec![shell_cstr])); + } + + // Create shell initialization script for session indicator + let display_name = if let Some(ref session_name) = name { + session_name.clone() + } else { + session_id.chars().take(6).collect::() + }; + + // Shell-specific setup for prompt modification + let setup_script = if shell.contains("zsh") { + format!( + r#" +# NDS Session Indicator Setup for Zsh +if [[ -n "$NDS_SESSION_ID" && "$NDS_PROMPT_STYLE" != "none" ]]; then + case "$NDS_PROMPT_STYLE" in + "full") NDS_PREFIX="[nds:{}] " ;; + "minimal") NDS_PREFIX="[nds] " ;; + "symbol") NDS_PREFIX="โฌข " ;; + *) NDS_PREFIX="โฌข " ;; + esac + + # Set terminal title + print -Pn "\e]0;NDS: {}\a" + + # Modify PS1 if it exists, otherwise create a basic one + if [[ -n "$PS1" ]]; then + export PS1="$NDS_PREFIX$PS1" + else + export PS1="$NDS_PREFIX%n@%m:%~$ " + fi +fi +"#, + display_name, display_name + ) + } else if shell.contains("bash") { + format!( + r#" +# NDS Session Indicator Setup for Bash +if [[ -n "$NDS_SESSION_ID" && "$NDS_PROMPT_STYLE" != "none" ]]; then + case "$NDS_PROMPT_STYLE" in + "full") NDS_PREFIX="[nds:{}] " ;; + "minimal") NDS_PREFIX="[nds] " ;; + "symbol") NDS_PREFIX="โฌข " ;; + *) NDS_PREFIX="โฌข " ;; + esac + + # Set terminal title + echo -ne "\033]0;NDS: {}\007" + + # Modify PS1 if it exists, otherwise create a basic one + if [[ -n "$PS1" ]]; then + export PS1="$NDS_PREFIX$PS1" + else + export PS1="$NDS_PREFIX\u@\h:\w\$ " + fi +fi +"#, + display_name, display_name + ) + } else { + // Generic shell setup + format!( + r#" +# NDS Session Indicator Setup (Generic) +if [ -n "$NDS_SESSION_ID" ] && [ "$NDS_PROMPT_STYLE" != "none" ]; then + case "$NDS_PROMPT_STYLE" in + "full") NDS_PREFIX="[nds:{}] " ;; + "minimal") NDS_PREFIX="[nds] " ;; + *) NDS_PREFIX="โฌข " ;; + esac + + if [ -n "$PS1" ]; then + export PS1="$NDS_PREFIX$PS1" + else + export PS1="$NDS_PREFIX\$ " + fi +fi +"#, + display_name + ) + }; + + // Execute shell with initialization script + let shell_cstr = std::ffi::CString::new(shell).unwrap(); + let init_flag = std::ffi::CString::new("-c").unwrap(); + let full_command = format!("{}{}", setup_script, shell); + let command_cstr = std::ffi::CString::new(full_command).unwrap(); + + Ok(( + shell.to_string(), + vec![shell_cstr, init_flag, command_cstr], + )) + } } impl Drop for PtyProcess {