Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
```
Expand Down
154 changes: 148 additions & 6 deletions src/pty/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)?;
Expand Down Expand Up @@ -867,6 +874,141 @@ impl PtyProcess {

Ok(())
}

/// Set session indicator environment variables
fn set_session_indicator_env(session_id: &str, name: &Option<String>) {
// 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::<String>()
};
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<String>,
) -> Result<(String, Vec<std::ffi::CString>)> {
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::<String>()
};

// 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 {
Expand Down
Loading