Merge branch 'development' into development_builders
* development: minor example fixes feat: add comprehensive SSH agent management command refactor: Harden and improve SSH agent module ... feat: add editable ttyd dashboard mode feat: add CLI for dashboard management and 4-pane layout fix: Fix build ... refactor: update SSH agent examples and module structure feat: add tmux dashboard with ttyd integration refactor: Remove is_tmux_server_not_running_error function wip: pushing the code to sync in other branch refactor: Improve tmux API consistency and formatting # Conflicts: # lib/osal/core/net.v # lib/virt/podman/factory.v
This commit is contained in:
0
examples/osal/sshagent.vsh → examples/osal/sshagent/sshagent.vsh
Normal file → Executable file
0
examples/osal/sshagent.vsh → examples/osal/sshagent/sshagent.vsh
Normal file → Executable file
@@ -1,51 +0,0 @@
|
||||
module main
|
||||
|
||||
import freeflowuniverse.herolib.osal.sshagent
|
||||
import freeflowuniverse.herolib.osal.linux
|
||||
|
||||
fn do1() ! {
|
||||
mut agent := sshagent.new()!
|
||||
println(agent)
|
||||
k := agent.get(name: 'kds') or { panic('notgound') }
|
||||
println(k)
|
||||
|
||||
mut k2 := agent.get(name: 'books') or { panic('notgound') }
|
||||
k2.load()!
|
||||
println(k2.agent)
|
||||
|
||||
println(agent)
|
||||
|
||||
k2.forget()!
|
||||
println(k2.agent)
|
||||
|
||||
// println(agent)
|
||||
}
|
||||
|
||||
fn test_user_mgmt() ! {
|
||||
mut lf := linux.new()!
|
||||
// Test user creation
|
||||
lf.user_create(
|
||||
name: 'testuser'
|
||||
sshkey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM3/2K7R8A/l0kM0/d'
|
||||
)!
|
||||
|
||||
// Test ssh key creation
|
||||
lf.sshkey_create(
|
||||
username: 'testuser'
|
||||
sshkey_name: 'testkey'
|
||||
)!
|
||||
|
||||
// Test ssh key deletion
|
||||
lf.sshkey_delete(
|
||||
username: 'testuser'
|
||||
sshkey_name: 'testkey'
|
||||
)!
|
||||
|
||||
// Test user deletion
|
||||
lf.user_delete(name: 'testuser')!
|
||||
}
|
||||
|
||||
fn main() {
|
||||
do1() or { panic(err) }
|
||||
test_user_mgmt() or { panic(err) }
|
||||
}
|
||||
168
examples/osal/sshagent/sshagent_example.vsh
Executable file
168
examples/osal/sshagent/sshagent_example.vsh
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.osal.sshagent
|
||||
import freeflowuniverse.herolib.osal.linux
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
fn demo_sshagent_basic() ! {
|
||||
console.print_header('SSH Agent Basic Demo')
|
||||
|
||||
// Create SSH agent
|
||||
mut agent := sshagent.new()!
|
||||
console.print_debug('SSH Agent initialized')
|
||||
|
||||
// Show current status
|
||||
console.print_header('Current SSH Agent Status:')
|
||||
println(agent)
|
||||
|
||||
// Show diagnostics
|
||||
diag := agent.diagnostics()
|
||||
console.print_header('SSH Agent Diagnostics:')
|
||||
for key, value in diag {
|
||||
console.print_item('${key}: ${value}')
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_sshagent_key_management() ! {
|
||||
console.print_header('SSH Agent Key Management Demo')
|
||||
|
||||
mut agent := sshagent.new()!
|
||||
|
||||
// Generate a test key if it doesn't exist
|
||||
test_key_name := 'herolib_demo_key'
|
||||
|
||||
// Clean up any existing test key first
|
||||
if existing_key := agent.get(name: test_key_name) {
|
||||
console.print_debug('Removing existing test key...')
|
||||
// Remove existing key files
|
||||
mut key_file := agent.homepath.file_get_new('${test_key_name}')!
|
||||
mut pub_key_file := agent.homepath.file_get_new('${test_key_name}.pub')!
|
||||
key_file.delete() or {}
|
||||
pub_key_file.delete() or {}
|
||||
} else {
|
||||
console.print_debug('No existing test key found')
|
||||
}
|
||||
|
||||
// Generate new key with empty passphrase
|
||||
console.print_debug('Generating new SSH key: ${test_key_name}')
|
||||
mut new_key := agent.generate(test_key_name, '')!
|
||||
console.print_green('✓ Generated new SSH key: ${test_key_name}')
|
||||
|
||||
// Show key information
|
||||
console.print_item('Key name: ${new_key.name}')
|
||||
console.print_item('Key type: ${new_key.cat}')
|
||||
console.print_item('Key loaded: ${new_key.loaded}')
|
||||
|
||||
// Demonstrate key operations without loading (to avoid passphrase issues)
|
||||
console.print_header('Key file operations:')
|
||||
mut key_path := new_key.keypath()!
|
||||
mut pub_key_path := new_key.keypath_pub()!
|
||||
console.print_item('Private key path: ${key_path.path}')
|
||||
console.print_item('Public key path: ${pub_key_path.path}')
|
||||
|
||||
// Show public key content
|
||||
pub_key_content := new_key.keypub()!
|
||||
preview_len := if pub_key_content.len > 60 { 60 } else { pub_key_content.len }
|
||||
console.print_item('Public key: ${pub_key_content[0..preview_len]}...')
|
||||
|
||||
// Show agent status
|
||||
console.print_header('Agent status after key generation:')
|
||||
println(agent)
|
||||
|
||||
// Clean up test key
|
||||
console.print_debug('Cleaning up test key...')
|
||||
key_path.delete()!
|
||||
pub_key_path.delete()!
|
||||
console.print_green('✓ Test key cleaned up')
|
||||
}
|
||||
|
||||
fn demo_sshagent_with_existing_keys() ! {
|
||||
console.print_header('SSH Agent with Existing Keys Demo')
|
||||
|
||||
mut agent := sshagent.new()!
|
||||
|
||||
if agent.keys.len == 0 {
|
||||
console.print_debug('No SSH keys found. Generating example key...')
|
||||
mut key := agent.generate('example_demo_key', '')!
|
||||
key.load()!
|
||||
console.print_green('✓ Created and loaded example key')
|
||||
}
|
||||
|
||||
console.print_header('Available SSH keys:')
|
||||
for key in agent.keys {
|
||||
status := if key.loaded { 'LOADED' } else { 'NOT LOADED' }
|
||||
console.print_item('${key.name} - ${status} (${key.cat})')
|
||||
}
|
||||
|
||||
// Try to work with the first available key
|
||||
if agent.keys.len > 0 {
|
||||
mut first_key := agent.keys[0]
|
||||
console.print_header('Working with key: ${first_key.name}')
|
||||
|
||||
if first_key.loaded {
|
||||
console.print_debug('Key is loaded, showing public key info...')
|
||||
pubkey := first_key.keypub() or { 'Could not read public key' }
|
||||
preview_len := if pubkey.len > 50 { 50 } else { pubkey.len }
|
||||
console.print_item('Public key preview: ${pubkey[0..preview_len]}...')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_user_mgmt() ! {
|
||||
console.print_header('User Management Test')
|
||||
|
||||
// Note: This requires root privileges and should be run carefully
|
||||
console.print_debug('User management test requires root privileges')
|
||||
console.print_debug('Skipping user management test in this demo')
|
||||
|
||||
// Uncomment below to test user management (requires root)
|
||||
/*
|
||||
mut lf := linux.new()!
|
||||
// Test user creation
|
||||
lf.user_create(
|
||||
name: 'testuser'
|
||||
sshkey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM3/2K7R8A/l0kM0/d'
|
||||
)!
|
||||
|
||||
// Test ssh key creation
|
||||
lf.sshkey_create(
|
||||
username: 'testuser'
|
||||
sshkey_name: 'testkey'
|
||||
)!
|
||||
|
||||
// Test ssh key deletion
|
||||
lf.sshkey_delete(
|
||||
username: 'testuser'
|
||||
sshkey_name: 'testkey'
|
||||
)!
|
||||
|
||||
// Test user deletion
|
||||
lf.user_delete(name: 'testuser')!
|
||||
*/
|
||||
}
|
||||
|
||||
fn main() {
|
||||
console.print_header('🔑 SSH Agent Example - HeroLib')
|
||||
|
||||
demo_sshagent_basic() or {
|
||||
console.print_stderr('❌ Basic demo failed: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
demo_sshagent_key_management() or {
|
||||
console.print_stderr('❌ Key management demo failed: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
demo_sshagent_with_existing_keys() or {
|
||||
console.print_stderr('❌ Existing keys demo failed: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
test_user_mgmt() or {
|
||||
console.print_stderr('❌ User management test failed: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
console.print_header('🎉 All SSH Agent demos completed successfully!')
|
||||
}
|
||||
85
examples/osal/sshagent/sshagent_example2.vsh
Executable file
85
examples/osal/sshagent/sshagent_example2.vsh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.osal.sshagent
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
console.print_header('SSH Agent Management Example')
|
||||
|
||||
// Create SSH agent with single instance guarantee
|
||||
mut agent := sshagent.new_single()!
|
||||
println('SSH Agent initialized and ensured single instance')
|
||||
|
||||
// Show diagnostics
|
||||
diag := agent.diagnostics()
|
||||
console.print_header('SSH Agent Diagnostics:')
|
||||
for key, value in diag {
|
||||
console.print_item('${key}: ${value}')
|
||||
}
|
||||
|
||||
// Show current agent status
|
||||
println(agent)
|
||||
|
||||
// Example: Generate a test key if no keys exist
|
||||
if agent.keys.len == 0 {
|
||||
console.print_header('No keys found, generating example key...')
|
||||
mut key := agent.generate('example_key', '')!
|
||||
console.print_debug('Generated key: ${key}')
|
||||
|
||||
// Load the generated key
|
||||
key.load()!
|
||||
console.print_debug('Key loaded into agent')
|
||||
}
|
||||
|
||||
// Example: Working with existing keys
|
||||
if agent.keys.len > 0 {
|
||||
console.print_header('Working with existing keys...')
|
||||
|
||||
for i, key in agent.keys {
|
||||
console.print_debug('Key ${i + 1}: ${key.name}')
|
||||
console.print_debug(' Type: ${key.cat}')
|
||||
console.print_debug(' Loaded: ${key.loaded}')
|
||||
console.print_debug(' Email: ${key.email}')
|
||||
|
||||
if !key.loaded {
|
||||
console.print_debug(' Loading key...')
|
||||
mut key_mut := key
|
||||
key_mut.load() or {
|
||||
console.print_debug(' Failed to load: ${err}')
|
||||
continue
|
||||
}
|
||||
console.print_debug(' ✓ Key loaded successfully')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Add a key from private key content
|
||||
console.print_header('Example: Adding a key from content...')
|
||||
console.print_debug('Note: This would normally use real private key content')
|
||||
console.print_debug('For security, we skip this in the example')
|
||||
|
||||
// Example: Generate and manage a new key
|
||||
console.print_header('Example: Generate a new test key...')
|
||||
test_key_name := 'test_key_example'
|
||||
|
||||
// Check if test key already exists
|
||||
existing_key := agent.get(name: test_key_name) or {
|
||||
console.print_debug('Test key does not exist, generating...')
|
||||
|
||||
// Generate new key
|
||||
mut new_key := agent.generate(test_key_name, '')!
|
||||
console.print_debug('✓ Generated new key: ${new_key.name}')
|
||||
|
||||
// Load it
|
||||
new_key.load()!
|
||||
console.print_debug('✓ Key loaded into agent')
|
||||
|
||||
new_key
|
||||
}
|
||||
|
||||
console.print_debug('Test key exists: ${existing_key.name}')
|
||||
|
||||
// Show final agent status
|
||||
console.print_header('Final SSH Agent Status:')
|
||||
println(agent)
|
||||
|
||||
console.print_header('SSH Agent example completed successfully')
|
||||
255
examples/sshagent/hero_sshagent_examples.md
Normal file
255
examples/sshagent/hero_sshagent_examples.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Hero SSH Agent Management Tool
|
||||
|
||||
The Hero SSH Agent Management Tool provides comprehensive SSH agent lifecycle management with cross-platform compatibility for macOS and Linux. It integrates seamlessly with shell profiles and implements intelligent SSH agent lifecycle management, automatic key discovery and loading, and remote key deployment capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔐 **Single SSH Agent Instance Enforcement**: Ensures exactly one SSH agent is running with persistent socket management
|
||||
- 🚀 **Smart Key Loading**: Auto-loads single keys from ~/.ssh/ directory with passphrase prompting
|
||||
- 🌐 **Remote Key Deployment**: Interactive SSH key deployment to remote machines
|
||||
- 🔄 **Agent Health Verification**: Comprehensive health checks through ssh-agent -l functionality
|
||||
- 📝 **Shell Profile Integration**: Automatic initialization in shell profiles
|
||||
- 🎯 **Cross-Platform**: Works on macOS and Linux
|
||||
|
||||
## Installation
|
||||
|
||||
The SSH agent functionality is built into the hero binary. After compiling hero:
|
||||
|
||||
```bash
|
||||
./cli/compile.vsh
|
||||
```
|
||||
|
||||
The hero binary will be available at `/Users/mahmoud/hero/bin/hero`
|
||||
|
||||
## Commands
|
||||
|
||||
### `hero sshagent profile`
|
||||
|
||||
Primary initialization command that ensures exactly one SSH agent is running on a consistent socket, performs health checks, manages agent lifecycle without losing existing keys, and automatically loads SSH keys when only one is present in ~/.ssh/ directory.
|
||||
|
||||
```bash
|
||||
# Initialize SSH agent with smart key loading
|
||||
hero sshagent profile
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Ensures single SSH agent instance
|
||||
- Verifies agent health and responsiveness
|
||||
- Auto-loads single SSH keys
|
||||
- Updates shell profile for automatic initialization
|
||||
- Preserves existing loaded keys
|
||||
|
||||
### `hero sshagent list`
|
||||
|
||||
Lists all available SSH keys and their current status.
|
||||
|
||||
```bash
|
||||
# List all SSH keys
|
||||
hero sshagent list
|
||||
```
|
||||
|
||||
### `hero sshagent status`
|
||||
|
||||
Shows comprehensive SSH agent status and diagnostics.
|
||||
|
||||
```bash
|
||||
# Show agent status
|
||||
hero sshagent status
|
||||
```
|
||||
|
||||
### `hero sshagent generate`
|
||||
|
||||
Generates a new SSH key pair.
|
||||
|
||||
```bash
|
||||
# Generate new SSH key
|
||||
hero sshagent generate -n my_new_key
|
||||
|
||||
# Generate and immediately load
|
||||
hero sshagent generate -n my_new_key -l
|
||||
```
|
||||
|
||||
### `hero sshagent load`
|
||||
|
||||
Loads a specific SSH key into the agent.
|
||||
|
||||
```bash
|
||||
# Load specific key
|
||||
hero sshagent load -n my_key
|
||||
```
|
||||
|
||||
### `hero sshagent forget`
|
||||
|
||||
Removes a specific SSH key from the agent.
|
||||
|
||||
```bash
|
||||
# Remove key from agent
|
||||
hero sshagent forget -n my_key
|
||||
```
|
||||
|
||||
### `hero sshagent reset`
|
||||
|
||||
Removes all loaded SSH keys from the agent.
|
||||
|
||||
```bash
|
||||
# Reset agent (remove all keys)
|
||||
hero sshagent reset
|
||||
```
|
||||
|
||||
### `hero sshagent push`
|
||||
|
||||
Interactive SSH key deployment to remote machines with automatic key selection when multiple keys exist, target specification via user@hostname format, and streamlined single-key auto-selection.
|
||||
|
||||
```bash
|
||||
# Deploy SSH key to remote machine
|
||||
hero sshagent push -t user@hostname
|
||||
|
||||
# Deploy specific key to remote machine
|
||||
hero sshagent push -t user@hostname -k my_key
|
||||
|
||||
# Deploy to custom port
|
||||
hero sshagent push -t user@hostname:2222
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Automatic key selection when only one key exists
|
||||
- Interactive key selection for multiple keys
|
||||
- Support for custom SSH ports
|
||||
- Uses ssh-copy-id when available, falls back to manual deployment
|
||||
|
||||
### `hero sshagent auth`
|
||||
|
||||
Remote SSH key authorization ensuring proper key installation on target machines, support for explicit key specification via -key parameter, and verification of successful key addition.
|
||||
|
||||
```bash
|
||||
# Verify SSH key authorization
|
||||
hero sshagent auth -t user@hostname
|
||||
|
||||
# Verify specific key authorization
|
||||
hero sshagent auth -t user@hostname -k my_key
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
```bash
|
||||
# 1. Initialize SSH agent
|
||||
hero sshagent profile
|
||||
|
||||
# 2. Check status
|
||||
hero sshagent status
|
||||
|
||||
# 3. Generate a new key if needed
|
||||
hero sshagent generate -n production_key
|
||||
|
||||
# 4. Load the key
|
||||
hero sshagent load -n production_key
|
||||
|
||||
# 5. Deploy to remote server
|
||||
hero sshagent push -t user@production-server.com
|
||||
|
||||
# 6. Verify authorization
|
||||
hero sshagent auth -t user@production-server.com
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```bash
|
||||
# Deploy specific key to multiple servers
|
||||
hero sshagent push -t user@server1.com -k production_key
|
||||
hero sshagent push -t user@server2.com:2222 -k production_key
|
||||
|
||||
# Verify access to all servers
|
||||
hero sshagent auth -t user@server1.com -k production_key
|
||||
hero sshagent auth -t user@server2.com:2222 -k production_key
|
||||
|
||||
# List all keys and their status
|
||||
hero sshagent list
|
||||
|
||||
# Reset agent if needed
|
||||
hero sshagent reset
|
||||
```
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Cross-Platform Compatibility
|
||||
|
||||
- **Socket Management**: Uses ~/.ssh/hero-agent.sock for consistent socket location
|
||||
- **Shell Integration**: Supports ~/.profile, ~/.bash_profile, ~/.bashrc, and ~/.zshrc
|
||||
- **Process Management**: Robust SSH agent lifecycle management
|
||||
- **Platform Support**: macOS and Linux (Windows not supported)
|
||||
|
||||
### Security Features
|
||||
|
||||
- **Single Agent Enforcement**: Prevents multiple conflicting agents
|
||||
- **Secure Socket Paths**: Uses user home directory for socket files
|
||||
- **Proper Permissions**: Ensures correct file permissions (0700 for .ssh, 0600 for keys)
|
||||
- **Input Validation**: Validates all user inputs and target specifications
|
||||
- **Connection Testing**: Verifies SSH connections before reporting success
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Network Connectivity**: Handles network failures gracefully
|
||||
- **Authentication Failures**: Provides clear error messages for auth issues
|
||||
- **Key Management**: Validates key existence and format
|
||||
- **Target Validation**: Ensures proper target format (user@hostname[:port])
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `HERO_DEBUG=1`: Enable debug output for troubleshooting
|
||||
|
||||
## Integration with Development Environments
|
||||
|
||||
The tool is designed to work seamlessly with development environments:
|
||||
|
||||
- **Preserves existing SSH agent state** during initialization
|
||||
- **Non-destructive operations** that don't interfere with existing workflows
|
||||
- **Shell profile integration** for automatic initialization
|
||||
- **Compatible with existing SSH configurations**
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Multiple SSH agents running**
|
||||
|
||||
```bash
|
||||
hero sshagent profile # Will cleanup and ensure single agent
|
||||
```
|
||||
|
||||
2. **Keys not loading**
|
||||
|
||||
```bash
|
||||
hero sshagent status # Check agent status
|
||||
hero sshagent reset # Reset if needed
|
||||
```
|
||||
|
||||
3. **Remote deployment failures**
|
||||
|
||||
```bash
|
||||
hero sshagent auth -t user@hostname # Verify connectivity
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug output for detailed troubleshooting:
|
||||
|
||||
```bash
|
||||
HERO_DEBUG=1 hero sshagent profile
|
||||
```
|
||||
|
||||
## Shell Profile Integration
|
||||
|
||||
The `hero sshagent profile` command automatically adds initialization code to your shell profile:
|
||||
|
||||
```bash
|
||||
# Hero SSH Agent initialization
|
||||
if [ -f "/Users/username/.ssh/hero-agent.sock" ]; then
|
||||
export SSH_AUTH_SOCK="/Users/username/.ssh/hero-agent.sock"
|
||||
fi
|
||||
```
|
||||
|
||||
This ensures the SSH agent is available in new shell sessions.
|
||||
105
examples/sshagent/test_hero_sshagent.vsh
Executable file
105
examples/sshagent/test_hero_sshagent.vsh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import os
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
// Test script for Hero SSH Agent functionality
|
||||
// This script demonstrates the key features of the hero sshagent command
|
||||
|
||||
fn main() {
|
||||
console.print_header('🔑 Hero SSH Agent Test Suite')
|
||||
os.execute('${os.dir(os.dir(@FILE))}/cli/compile.vsh')
|
||||
|
||||
hero_bin := '${os.home_dir()}/hero/bin/hero'
|
||||
|
||||
// Check if hero binary exists
|
||||
if !os.exists(hero_bin) {
|
||||
console.print_stderr('Hero binary not found at ${hero_bin}')
|
||||
console.print_stderr('Please compile hero first with: ./cli/compile.vsh')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
console.print_green('✓ Hero binary found at ${hero_bin}')
|
||||
|
||||
// Test 1: Profile initialization
|
||||
console.print_header('Test 1: Profile Initialization')
|
||||
result1 := os.execute('${hero_bin} sshagent profile')
|
||||
if result1.exit_code == 0 && result1.output.contains('Hero SSH Agent Profile Initialization') {
|
||||
console.print_green('✓ Profile initialization successful')
|
||||
} else {
|
||||
console.print_stderr('❌ Profile initialization failed: ${result1.output}')
|
||||
}
|
||||
|
||||
// Test 2: Status check
|
||||
console.print_header('Test 2: Status Check')
|
||||
result2 := os.execute('${hero_bin} sshagent status')
|
||||
if result2.exit_code == 0 && result2.output.contains("- SSH Agent Status") {
|
||||
console.print_green('✓ Status check successful')
|
||||
println(result2.output)
|
||||
} else {
|
||||
console.print_stderr('❌ Status check failed: ${result2.output}')
|
||||
}
|
||||
|
||||
// Test 3: List keys
|
||||
console.print_header('Test 3: List SSH Keys')
|
||||
result3 := os.execute('${hero_bin} sshagent list')
|
||||
if result3.exit_code == 0 && result3.output.contains('SSH Keys Status') {
|
||||
console.print_green('✓ List keys successful')
|
||||
println(result3.output)
|
||||
} else {
|
||||
console.print_stderr('❌ List keys failed: ${result3.output}')
|
||||
}
|
||||
|
||||
// Test 4: Generate test key
|
||||
console.print_header('Test 4: Generate Test Key')
|
||||
test_key_name := 'hero_test_${os.getpid()}'
|
||||
result4 := os.execute('${hero_bin} sshagent generate -n ${test_key_name}')
|
||||
if result4.exit_code == 0 && result4.output.contains('Generating SSH key') {
|
||||
console.print_green('✓ Key generation successful')
|
||||
println(result4.output)
|
||||
|
||||
// Cleanup: remove test key files
|
||||
test_key_path := '${os.home_dir()}/.ssh/${test_key_name}'
|
||||
test_pub_path := '${test_key_path}.pub'
|
||||
|
||||
if os.exists(test_key_path) {
|
||||
os.rm(test_key_path) or {}
|
||||
console.print_debug('Cleaned up test private key')
|
||||
}
|
||||
if os.exists(test_pub_path) {
|
||||
os.rm(test_pub_path) or {}
|
||||
console.print_debug('Cleaned up test public key')
|
||||
}
|
||||
} else {
|
||||
console.print_stderr('❌ Key generation failed: ${result4.output}')
|
||||
}
|
||||
|
||||
// Test 5: Help output
|
||||
console.print_header('Test 5: Help Output')
|
||||
result5 := os.execute('${hero_bin} sshagent')
|
||||
if result5.exit_code == 1 && result5.output.contains('Hero SSH Agent Management Tool') {
|
||||
console.print_green('✓ Help output is correct')
|
||||
} else {
|
||||
console.print_stderr('❌ Help output unexpected')
|
||||
}
|
||||
|
||||
console.print_header('🎉 Test Suite Complete')
|
||||
console.print_green('Hero SSH Agent is ready for use!')
|
||||
|
||||
// Show usage examples
|
||||
console.print_header('Usage Examples:')
|
||||
println('')
|
||||
println('Initialize SSH agent:')
|
||||
println(' ${hero_bin} sshagent profile')
|
||||
println('')
|
||||
println('Check status:')
|
||||
println(' ${hero_bin} sshagent status')
|
||||
println('')
|
||||
println('Deploy key to remote server:')
|
||||
println(' ${hero_bin} sshagent push -t user@server.com')
|
||||
println('')
|
||||
println('Verify authorization:')
|
||||
println(' ${hero_bin} sshagent auth -t user@server.com')
|
||||
println('')
|
||||
println('For more examples, see: examples/sshagent/hero_sshagent_examples.md')
|
||||
}
|
||||
389
examples/tmux/server_dashboard.vsh
Executable file
389
examples/tmux/server_dashboard.vsh
Executable file
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.osal.tmux
|
||||
import freeflowuniverse.herolib.osal.core as osal
|
||||
import time
|
||||
import os
|
||||
|
||||
// Configuration
|
||||
const session_name = 'server_dashboard'
|
||||
const window_name = 'dashboard'
|
||||
const python_port = 8000
|
||||
const ttyd_port = 7890
|
||||
|
||||
// Command line argument handling
|
||||
fn show_help() {
|
||||
println('=== Tmux Server Dashboard ===')
|
||||
println('Usage:')
|
||||
println(' ${os.args[0]} # Start the dashboard')
|
||||
println(' ${os.args[0]} -editable # Start dashboard with editable ttyd')
|
||||
println(' ${os.args[0]} -down # Stop dashboard and cleanup')
|
||||
println(' ${os.args[0]} -status # Show dashboard status')
|
||||
println(' ${os.args[0]} -restart # Restart the dashboard')
|
||||
println(' ${os.args[0]} -help # Show this help')
|
||||
println('')
|
||||
println('Dashboard includes:')
|
||||
println(' • Python HTTP Server (port ${python_port})')
|
||||
println(' • Counter service (updates every 5 seconds)')
|
||||
println(' • Hero Web (compile and run hero web server)')
|
||||
println(' • CPU Monitor (htop)')
|
||||
println(' • Web access via ttyd (port ${ttyd_port})')
|
||||
println('')
|
||||
println('ttyd modes:')
|
||||
println(' • Default: read-only access to terminal')
|
||||
println(' • -editable: allows writing/editing in the terminal')
|
||||
}
|
||||
|
||||
fn stop_dashboard() ! {
|
||||
println('=== Stopping Dashboard ===')
|
||||
|
||||
// Kill ttyd processes
|
||||
println('Stopping ttyd processes...')
|
||||
os.execute('pkill ttyd')
|
||||
|
||||
// Kill tmux session
|
||||
println('Stopping tmux session...')
|
||||
mut t := tmux.new()!
|
||||
if t.session_exist(session_name) {
|
||||
mut session := t.session_get(session_name)!
|
||||
session.stop()!
|
||||
println('✓ Tmux session "${session_name}" stopped')
|
||||
} else {
|
||||
println('• Session "${session_name}" not found')
|
||||
}
|
||||
|
||||
// Check for any remaining processes on our ports
|
||||
println('Checking for processes on ports...')
|
||||
|
||||
// Check Python server port
|
||||
python_check := os.execute('lsof -i :${python_port}')
|
||||
if python_check.exit_code == 0 {
|
||||
println('• Found processes on port ${python_port}')
|
||||
println(python_check.output)
|
||||
} else {
|
||||
println('✓ Port ${python_port} is free')
|
||||
}
|
||||
|
||||
// Check ttyd port
|
||||
ttyd_check := os.execute('lsof -i :${ttyd_port}')
|
||||
if ttyd_check.exit_code == 0 {
|
||||
println('• Found processes on port ${ttyd_port}')
|
||||
println(ttyd_check.output)
|
||||
} else {
|
||||
println('✓ Port ${ttyd_port} is free')
|
||||
}
|
||||
|
||||
println('=== Dashboard stopped ===')
|
||||
}
|
||||
|
||||
fn show_status() ! {
|
||||
println('=== Dashboard Status ===')
|
||||
|
||||
mut t := tmux.new()!
|
||||
|
||||
// Check tmux session
|
||||
if t.session_exist(session_name) {
|
||||
println('✓ Tmux session "${session_name}" is running')
|
||||
|
||||
mut session := t.session_get(session_name)!
|
||||
mut window := session.window_get(name: window_name) or {
|
||||
println('✗ Window "${window_name}" not found')
|
||||
return
|
||||
}
|
||||
println('✓ Window "${window_name}" exists with ${window.panes.len} panes')
|
||||
|
||||
// Show pane details
|
||||
for i, pane in window.panes {
|
||||
service_name := match i {
|
||||
0 { 'Python HTTP Server' }
|
||||
1 { 'Counter Service' }
|
||||
2 { 'Hero Web Service' }
|
||||
3 { 'CPU Monitor' }
|
||||
else { 'Service ${i + 1}' }
|
||||
}
|
||||
|
||||
mut pane_mut := pane
|
||||
stats := pane_mut.stats() or {
|
||||
println(' Pane ${i + 1} (${service_name}): ID=%${pane.id}, PID=${pane.pid} (stats unavailable)')
|
||||
continue
|
||||
}
|
||||
|
||||
memory_mb := f64(stats.memory_bytes) / (1024.0 * 1024.0)
|
||||
println(' Pane ${i + 1} (${service_name}): ID=%${pane.id}, CPU=${stats.cpu_percent:.1f}%, Memory=${memory_mb:.1f}MB')
|
||||
}
|
||||
} else {
|
||||
println('✗ Tmux session "${session_name}" not running')
|
||||
}
|
||||
|
||||
// Check ports
|
||||
python_check := os.execute('lsof -i :${python_port}')
|
||||
if python_check.exit_code == 0 {
|
||||
println('✓ Python server running on port ${python_port}')
|
||||
} else {
|
||||
println('✗ No process on port ${python_port}')
|
||||
}
|
||||
|
||||
ttyd_check := os.execute('lsof -i :${ttyd_port}')
|
||||
if ttyd_check.exit_code == 0 {
|
||||
println('✓ ttyd running on port ${ttyd_port}')
|
||||
} else {
|
||||
println('✗ No process on port ${ttyd_port}')
|
||||
}
|
||||
|
||||
println('')
|
||||
println('Access URLs:')
|
||||
println(' • Python Server: http://localhost:${python_port}')
|
||||
println(' • Web Terminal: http://localhost:${ttyd_port}')
|
||||
println(' • Tmux attach: tmux attach-session -t ${session_name}')
|
||||
}
|
||||
|
||||
fn restart_dashboard() ! {
|
||||
println('=== Restarting Dashboard ===')
|
||||
stop_dashboard()!
|
||||
time.sleep(2000 * time.millisecond) // Wait 2 seconds
|
||||
start_dashboard()!
|
||||
}
|
||||
|
||||
fn start_dashboard_with_mode(ttyd_editable bool) ! {
|
||||
println('=== Server Dashboard with 4 Panes ===')
|
||||
println('Setting up tmux session with:')
|
||||
println(' 1. Python HTTP Server (port ${python_port})')
|
||||
println(' 2. Counter Service (updates every 5 seconds)')
|
||||
println(' 3. Hero Web (compile and run hero web server)')
|
||||
println(' 4. CPU Monitor (htop)')
|
||||
println('')
|
||||
|
||||
// Initialize tmux
|
||||
mut t := tmux.new()!
|
||||
|
||||
if !t.is_running()! {
|
||||
println('Starting tmux server...')
|
||||
t.start()!
|
||||
}
|
||||
|
||||
// Clean up existing session if it exists
|
||||
if t.session_exist(session_name) {
|
||||
println('Cleaning up existing ${session_name} session...')
|
||||
t.session_delete(session_name)!
|
||||
}
|
||||
|
||||
// Create new session
|
||||
println('Creating ${session_name} session...')
|
||||
mut session := t.session_create(name: session_name)!
|
||||
|
||||
// Create main window with initial bash shell
|
||||
println('Creating dashboard window...')
|
||||
mut window := session.window_new(name: window_name, cmd: 'bash', reset: true)!
|
||||
|
||||
// Wait for initial setup
|
||||
time.sleep(500 * time.millisecond)
|
||||
t.scan()!
|
||||
|
||||
println('\n=== Setting up 4-pane layout ===')
|
||||
|
||||
// Get the main window
|
||||
window = session.window_get(name: window_name)!
|
||||
|
||||
// Split horizontally first (left and right halves)
|
||||
println('1. Splitting horizontally for left/right layout...')
|
||||
mut right_pane := window.pane_split_horizontal('bash')!
|
||||
time.sleep(300 * time.millisecond)
|
||||
window.scan()!
|
||||
|
||||
// Split left pane vertically (top-left and bottom-left)
|
||||
println('2. Splitting left pane vertically...')
|
||||
window.scan()!
|
||||
if window.panes.len >= 2 {
|
||||
mut left_pane := window.panes[0] // First pane should be the left one
|
||||
left_pane.select()!
|
||||
time.sleep(200 * time.millisecond)
|
||||
mut bottom_left_pane := window.pane_split_vertical('bash')!
|
||||
time.sleep(300 * time.millisecond)
|
||||
window.scan()!
|
||||
}
|
||||
|
||||
// Split right pane vertically (top-right and bottom-right)
|
||||
println('3. Splitting right pane vertically...')
|
||||
window.scan()!
|
||||
if window.panes.len >= 3 {
|
||||
// Find the rightmost pane (should be the last one after horizontal split)
|
||||
mut right_pane_current := window.panes[window.panes.len - 1]
|
||||
right_pane_current.select()!
|
||||
time.sleep(200 * time.millisecond)
|
||||
mut bottom_right_pane := window.pane_split_vertical('bash')!
|
||||
time.sleep(300 * time.millisecond)
|
||||
window.scan()!
|
||||
}
|
||||
|
||||
// Set a proper 2x2 tiled layout using tmux command
|
||||
println('4. Setting 2x2 tiled layout...')
|
||||
os.execute('tmux select-layout -t ${session_name}:${window_name} tiled')
|
||||
time.sleep(500 * time.millisecond)
|
||||
window.scan()!
|
||||
|
||||
println('5. Layout complete! We now have 4 panes in 2x2 grid.')
|
||||
|
||||
// Refresh to get all panes
|
||||
window.scan()!
|
||||
println('\nCurrent panes: ${window.panes.len}')
|
||||
for i, pane in window.panes {
|
||||
println(' Pane ${i}: ID=%${pane.id}, PID=${pane.pid}')
|
||||
}
|
||||
|
||||
if window.panes.len < 4 {
|
||||
eprintln('Expected 4 panes, but got ${window.panes.len}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println('\n=== Starting services in each pane ===')
|
||||
|
||||
// Pane 1 (top-left): Python HTTP Server
|
||||
println('Starting Python HTTP Server in pane 1...')
|
||||
mut pane1 := window.panes[0]
|
||||
pane1.select()!
|
||||
pane1.send_command('echo "=== Python HTTP Server Port 8000 ==="')!
|
||||
pane1.send_command('cd /tmp && python3 -m http.server ${python_port}')!
|
||||
|
||||
time.sleep(500 * time.millisecond)
|
||||
|
||||
// Pane 2 (bottom-left): Counter Service
|
||||
println('Starting Counter Service in pane 2...')
|
||||
mut pane2 := window.panes[1]
|
||||
pane2.select()!
|
||||
pane2.send_command('echo "=== Counter Service - Updates every 5 seconds ==="')!
|
||||
pane2.send_command('while true; do echo "Count: $(date)"; sleep 5; done')!
|
||||
|
||||
time.sleep(500 * time.millisecond)
|
||||
|
||||
// Pane 3 (top-right): Hero Web
|
||||
println('Starting Hero Web in pane 3...')
|
||||
mut pane3 := window.panes[2]
|
||||
pane3.select()!
|
||||
pane3.send_command('echo "=== Hero Web Server ==="')!
|
||||
pane3.send_command('hero web')!
|
||||
|
||||
time.sleep(500 * time.millisecond)
|
||||
|
||||
// Pane 4 (bottom-right): CPU Monitor
|
||||
println('Starting CPU Monitor in pane 4...')
|
||||
mut pane4 := window.panes[3]
|
||||
pane4.select()!
|
||||
pane4.send_command('echo "=== CPU Monitor ==="')!
|
||||
pane4.send_command('htop')!
|
||||
|
||||
println('\n=== All services started! ===')
|
||||
|
||||
// Wait a moment for services to initialize
|
||||
time.sleep(2000 * time.millisecond)
|
||||
|
||||
// Refresh and show current state
|
||||
t.scan()!
|
||||
window = session.window_get(name: window_name)!
|
||||
|
||||
println('\n=== Current Dashboard State ===')
|
||||
for i, mut pane in window.panes {
|
||||
stats := pane.stats() or {
|
||||
println(' Pane ${i + 1}: ID=%${pane.id}, PID=${pane.pid} (stats unavailable)')
|
||||
continue
|
||||
}
|
||||
memory_mb := f64(stats.memory_bytes) / (1024.0 * 1024.0)
|
||||
service_name := match i {
|
||||
0 { 'Python Server' }
|
||||
1 { 'Counter Service' }
|
||||
2 { 'Hero Web' }
|
||||
3 { 'CPU Monitor' }
|
||||
else { 'Unknown' }
|
||||
}
|
||||
println(' Pane ${i + 1} (${service_name}): ID=%${pane.id}, CPU=${stats.cpu_percent:.1f}%, Memory=${memory_mb:.1f}MB')
|
||||
}
|
||||
|
||||
println('\n=== Access Information ===')
|
||||
println('• Python HTTP Server: http://localhost:${python_port}')
|
||||
println('• Tmux Session: tmux attach-session -t ${session_name}')
|
||||
println('')
|
||||
println('=== Pane Resize Commands ===')
|
||||
println('To resize panes, attach to the session and use:')
|
||||
println(' Ctrl+B then Arrow Keys (hold Ctrl+B and press arrow keys)')
|
||||
println(' Or programmatically:')
|
||||
for i, pane in window.panes {
|
||||
service_name := match i {
|
||||
0 { 'Python Server' }
|
||||
1 { 'Counter Service' }
|
||||
2 { 'Hero Web' }
|
||||
3 { 'CPU Monitor' }
|
||||
else { 'Unknown' }
|
||||
}
|
||||
println(' # Resize ${service_name} pane:')
|
||||
println(' tmux resize-pane -t ${session_name}:${window_name}.%${pane.id} -U 5 # Up')
|
||||
println(' tmux resize-pane -t ${session_name}:${window_name}.%${pane.id} -D 5 # Down')
|
||||
println(' tmux resize-pane -t ${session_name}:${window_name}.%${pane.id} -L 5 # Left')
|
||||
println(' tmux resize-pane -t ${session_name}:${window_name}.%${pane.id} -R 5 # Right')
|
||||
}
|
||||
|
||||
println('\n=== Dashboard is running! ===')
|
||||
println('Attach to view: tmux attach-session -t ${session_name}')
|
||||
println('Press Ctrl+B then d to detach from session')
|
||||
println('To stop all services: tmux kill-session -t ${session_name}')
|
||||
println('Running the browser-based dashboard: TTYD')
|
||||
|
||||
mode_str := if ttyd_editable { 'editable' } else { 'read-only' }
|
||||
println('Starting ttyd in ${mode_str} mode...')
|
||||
|
||||
window.run_ttyd(port: ttyd_port, editable: ttyd_editable) or {
|
||||
println('Failed to start ttyd: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
fn start_dashboard() ! {
|
||||
start_dashboard_with_mode(false)!
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut ttyd_editable := false // Local flag for ttyd editable mode
|
||||
|
||||
// Main execution with argument handling
|
||||
if os.args.len > 1 {
|
||||
command := os.args[1]
|
||||
match command {
|
||||
'-editable' {
|
||||
ttyd_editable = true
|
||||
start_dashboard_with_mode(ttyd_editable) or {
|
||||
eprintln('Error starting dashboard: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
'-down' {
|
||||
stop_dashboard() or {
|
||||
eprintln('Error stopping dashboard: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
'-status' {
|
||||
show_status() or {
|
||||
eprintln('Error getting status: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
'-restart' {
|
||||
restart_dashboard() or {
|
||||
eprintln('Error restarting dashboard: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
'-help', '--help', '-h' {
|
||||
show_help()
|
||||
}
|
||||
else {
|
||||
eprintln('Unknown command: ${command}')
|
||||
show_help()
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No arguments - start the dashboard
|
||||
start_dashboard_with_mode(ttyd_editable) or {
|
||||
eprintln('Error starting dashboard: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
120
examples/tmux/tmux.vsh
Executable file
120
examples/tmux/tmux.vsh
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.osal.tmux
|
||||
import freeflowuniverse.herolib.osal.core as osal
|
||||
import time
|
||||
|
||||
// Constants for display formatting
|
||||
const bytes_to_mb = 1024.0 * 1024.0
|
||||
const cpu_precision = 1
|
||||
const memory_precision = 3
|
||||
|
||||
println('=== Tmux Pane Example ===')
|
||||
|
||||
mut t := tmux.new()!
|
||||
|
||||
if !t.is_running()! {
|
||||
println('Starting tmux server...')
|
||||
t.start()!
|
||||
}
|
||||
|
||||
if t.session_exist('demo') {
|
||||
println('Deleting existing demo session...')
|
||||
t.session_delete('demo')!
|
||||
}
|
||||
|
||||
// Create session and window
|
||||
println('Creating demo session...')
|
||||
mut session := t.session_create(name: 'demo')!
|
||||
|
||||
println('Creating main window with htop...')
|
||||
mut window := session.window_new(name: 'main', cmd: 'htop', reset: true)!
|
||||
|
||||
// Wait a moment for the window to be created
|
||||
time.sleep(500 * time.millisecond)
|
||||
|
||||
// Refresh to get current state
|
||||
t.scan()!
|
||||
|
||||
println('\n=== Current Tmux State ===')
|
||||
println(t)
|
||||
|
||||
// Get the window and demonstrate pane functionality
|
||||
mut main_window := session.window_get(name: 'main')!
|
||||
|
||||
println('\n=== Window Pane Information ===')
|
||||
println('Window: ${main_window.name} (ID: ${main_window.id})')
|
||||
println('Number of panes: ${main_window.panes.len}')
|
||||
|
||||
for i, mut pane in main_window.panes {
|
||||
println('Pane ${i}: ID=%${pane.id}, PID=${pane.pid}, Active=${pane.active}, Cmd="${pane.cmd}"')
|
||||
|
||||
// Get pane stats
|
||||
stats := pane.stats() or {
|
||||
println(' Could not get stats: ${err}')
|
||||
continue
|
||||
}
|
||||
memory_mb := f64(stats.memory_bytes) / bytes_to_mb
|
||||
println(' CPU: ${stats.cpu_percent:.1f}%, Memory: ${stats.memory_percent:.3f}% (${memory_mb:.1f} MB)')
|
||||
}
|
||||
|
||||
// Get the active pane
|
||||
if mut active_pane := main_window.pane_active() {
|
||||
println('\n=== Active Pane Details ===')
|
||||
println('Active pane ID: %${active_pane.id}')
|
||||
println('Process ID: ${active_pane.pid}')
|
||||
println('Command: ${active_pane.cmd}')
|
||||
|
||||
// Get process information
|
||||
process_info := active_pane.processinfo_main() or {
|
||||
println('Could not get process info: ${err}')
|
||||
osal.ProcessInfo{}
|
||||
}
|
||||
if process_info.pid > 0 {
|
||||
println('Process info: PID=${process_info.pid}, Command=${process_info.cmd}')
|
||||
}
|
||||
|
||||
// Get recent logs
|
||||
println('\n=== Recent Pane Output ===')
|
||||
logs := active_pane.logs_all() or {
|
||||
println('Could not get logs: ${err}')
|
||||
''
|
||||
}
|
||||
if logs.len > 0 {
|
||||
lines := logs.split_into_lines()
|
||||
// Show last 5 lines
|
||||
start_idx := if lines.len > 5 { lines.len - 5 } else { 0 }
|
||||
for i in start_idx .. lines.len {
|
||||
if lines[i].trim_space().len > 0 {
|
||||
println(' ${lines[i]}')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println('No active pane found')
|
||||
}
|
||||
|
||||
println('\n=== Creating Additional Windows ===')
|
||||
|
||||
// Create more windows to demonstrate multiple panes
|
||||
mut monitor_window := session.window_new(name: 'monitor', cmd: 'top', reset: true)!
|
||||
mut logs_window := session.window_new(name: 'logs', cmd: 'tail -f /var/log/system.log', reset: true)!
|
||||
|
||||
time.sleep(500 * time.millisecond)
|
||||
t.scan()!
|
||||
|
||||
println('\n=== Final Tmux State ===')
|
||||
println(t)
|
||||
|
||||
println('\n=== Window Statistics ===')
|
||||
for mut win in session.windows {
|
||||
println('Window: ${win.name}')
|
||||
window_stats := win.stats() or {
|
||||
println(' Could not get window stats: ${err}')
|
||||
continue
|
||||
}
|
||||
memory_mb := f64(window_stats.memory_bytes) / bytes_to_mb
|
||||
println(' Total CPU: ${window_stats.cpu_percent:.1f}%')
|
||||
println(' Total Memory: ${window_stats.memory_percent:.3f}% (${memory_mb:.1f} MB)')
|
||||
println(' Panes: ${win.panes.len}')
|
||||
}
|
||||
143
examples/tmux/tmux_pane_resize.vsh
Executable file
143
examples/tmux/tmux_pane_resize.vsh
Executable file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.osal.tmux
|
||||
import time
|
||||
|
||||
println('=== Tmux Pane Resizing Example ===')
|
||||
|
||||
mut t := tmux.new()!
|
||||
|
||||
if !t.is_running()! {
|
||||
println('Starting tmux server...')
|
||||
t.start()!
|
||||
}
|
||||
|
||||
if t.session_exist('resize_demo') {
|
||||
println('Deleting existing resize_demo session...')
|
||||
t.session_delete('resize_demo')!
|
||||
}
|
||||
|
||||
// Create session and window
|
||||
println('Creating resize_demo session...')
|
||||
mut session := t.session_create(name: 'resize_demo')!
|
||||
|
||||
println('Creating main window...')
|
||||
mut window := session.window_new(name: 'main', cmd: 'bash', reset: true)!
|
||||
|
||||
time.sleep(500 * time.millisecond)
|
||||
t.scan()!
|
||||
|
||||
// Create a 2x2 grid of panes
|
||||
println('\n=== Creating 2x2 Grid of Panes ===')
|
||||
|
||||
// Split horizontally first (left | right)
|
||||
mut right_pane := window.pane_split_horizontal('htop')!
|
||||
time.sleep(300 * time.millisecond)
|
||||
|
||||
// Split left pane vertically (top-left, bottom-left)
|
||||
window.scan()!
|
||||
if window.panes.len > 1 {
|
||||
mut left_pane := window.panes[1] // The original bash pane
|
||||
left_pane.select()!
|
||||
time.sleep(200 * time.millisecond)
|
||||
}
|
||||
mut bottom_left_pane := window.pane_split_vertical('top')!
|
||||
time.sleep(300 * time.millisecond)
|
||||
|
||||
// Split right pane vertically (top-right, bottom-right)
|
||||
window.scan()!
|
||||
for mut pane in window.panes {
|
||||
if pane.cmd.contains('htop') {
|
||||
pane.select()!
|
||||
break
|
||||
}
|
||||
}
|
||||
time.sleep(200 * time.millisecond)
|
||||
mut bottom_right_pane := window.pane_split_vertical('tail -f /var/log/system.log')!
|
||||
time.sleep(500 * time.millisecond)
|
||||
|
||||
window.scan()!
|
||||
println('Created 2x2 grid with ${window.panes.len} panes:')
|
||||
for i, pane in window.panes {
|
||||
println(' Pane ${i}: ID=%${pane.id}, Cmd="${pane.cmd}"')
|
||||
}
|
||||
|
||||
// Demonstrate resizing operations
|
||||
println('\n=== Demonstrating Pane Resizing ===')
|
||||
|
||||
// Get references to panes for resizing
|
||||
window.scan()!
|
||||
if window.panes.len >= 4 {
|
||||
mut top_left := window.panes[1] // bash
|
||||
mut top_right := window.panes[0] // htop
|
||||
mut bottom_left := window.panes[2] // top
|
||||
mut bottom_right := window.panes[3] // tail
|
||||
|
||||
println('Resizing top-left pane (bash) to be wider...')
|
||||
top_left.select()!
|
||||
time.sleep(200 * time.millisecond)
|
||||
top_left.resize_right(10)!
|
||||
time.sleep(1000 * time.millisecond)
|
||||
|
||||
println('Resizing top-right pane (htop) to be taller...')
|
||||
top_right.select()!
|
||||
time.sleep(200 * time.millisecond)
|
||||
top_right.resize_down(5)!
|
||||
time.sleep(1000 * time.millisecond)
|
||||
|
||||
println('Resizing bottom-left pane (top) to be narrower...')
|
||||
bottom_left.select()!
|
||||
time.sleep(200 * time.millisecond)
|
||||
bottom_left.resize_left(5)!
|
||||
time.sleep(1000 * time.millisecond)
|
||||
|
||||
println('Resizing bottom-right pane (tail) to be shorter...')
|
||||
bottom_right.select()!
|
||||
time.sleep(200 * time.millisecond)
|
||||
bottom_right.resize_up(3)!
|
||||
time.sleep(1000 * time.millisecond)
|
||||
|
||||
// Demonstrate using the generic resize method
|
||||
println('Using generic resize method to make top-left pane taller...')
|
||||
top_left.select()!
|
||||
time.sleep(200 * time.millisecond)
|
||||
top_left.resize(direction: 'down', cells: 3)!
|
||||
time.sleep(1000 * time.millisecond)
|
||||
}
|
||||
|
||||
// Send some commands to make the panes more interesting
|
||||
println('\n=== Adding Content to Panes ===')
|
||||
window.scan()!
|
||||
if window.panes.len >= 4 {
|
||||
// Send commands to bash pane
|
||||
mut bash_pane := window.panes[1]
|
||||
bash_pane.send_command('echo "=== Bash Pane ==="')!
|
||||
bash_pane.send_command('ls -la')!
|
||||
bash_pane.send_command('pwd')!
|
||||
time.sleep(500 * time.millisecond)
|
||||
|
||||
// Send command to top pane
|
||||
mut top_pane := window.panes[2]
|
||||
top_pane.send_command('echo "=== Top Pane ==="')!
|
||||
time.sleep(500 * time.millisecond)
|
||||
}
|
||||
|
||||
println('\n=== Final Layout ===')
|
||||
t.scan()!
|
||||
println('Session: ${session.name}')
|
||||
println('Window: ${window.name} (${window.panes.len} panes)')
|
||||
for i, pane in window.panes {
|
||||
println(' ${i + 1}. Pane %${pane.id} - ${pane.cmd}')
|
||||
}
|
||||
|
||||
println('\n=== Pane Resize Operations Available ===')
|
||||
println('✓ resize_up(cells) - Make pane taller by shrinking pane above')
|
||||
println('✓ resize_down(cells) - Make pane taller by shrinking pane below')
|
||||
println('✓ resize_left(cells) - Make pane wider by shrinking pane to the left')
|
||||
println('✓ resize_right(cells) - Make pane wider by shrinking pane to the right')
|
||||
println('✓ resize(direction: "up/down/left/right", cells: N) - Generic resize method')
|
||||
|
||||
println('\nExample completed! You can attach to the session with:')
|
||||
println(' tmux attach-session -t resize_demo')
|
||||
println('\nThen use Ctrl+B followed by arrow keys to manually resize panes,')
|
||||
println('or Ctrl+B followed by Alt+arrow keys for larger resize steps.')
|
||||
170
examples/tmux/tmux_panes.vsh
Executable file
170
examples/tmux/tmux_panes.vsh
Executable file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.osal.tmux
|
||||
import time
|
||||
|
||||
println('=== Tmux Pane Splitting Example ===')
|
||||
|
||||
mut t := tmux.new()!
|
||||
|
||||
if !t.is_running()! {
|
||||
println('Starting tmux server...')
|
||||
t.start()!
|
||||
}
|
||||
|
||||
if t.session_exist('panes_demo') {
|
||||
println('Deleting existing panes_demo session...')
|
||||
t.session_delete('panes_demo')!
|
||||
}
|
||||
|
||||
// Create session and initial window
|
||||
println('Creating panes_demo session...')
|
||||
mut session := t.session_create(name: 'panes_demo')!
|
||||
|
||||
println('Creating main window...')
|
||||
mut window := session.window_new(name: 'main', cmd: 'bash', reset: true)!
|
||||
|
||||
// Wait for initial setup
|
||||
time.sleep(500 * time.millisecond)
|
||||
t.scan()!
|
||||
|
||||
println('\n=== Initial State ===')
|
||||
println('Window: ${window.name} (ID: ${window.id})')
|
||||
println('Number of panes: ${window.panes.len}')
|
||||
|
||||
// Split the window horizontally (side by side)
|
||||
println('\n=== Splitting Horizontally (Side by Side) ===')
|
||||
mut right_pane := window.pane_split_horizontal('htop')!
|
||||
time.sleep(500 * time.millisecond)
|
||||
window.scan()!
|
||||
|
||||
println('After horizontal split:')
|
||||
println('Number of panes: ${window.panes.len}')
|
||||
for i, mut pane in window.panes {
|
||||
println(' Pane ${i}: ID=%${pane.id}, PID=${pane.pid}, Active=${pane.active}, Cmd="${pane.cmd}"')
|
||||
}
|
||||
|
||||
// Split the right pane vertically (top and bottom)
|
||||
println('\n=== Splitting Right Pane Vertically (Top and Bottom) ===')
|
||||
// Get a fresh reference to the right pane after the first split
|
||||
window.scan()!
|
||||
if window.panes.len > 0 {
|
||||
// Find the pane with htop command (the one we just created)
|
||||
mut right_pane_fresh := &window.panes[0]
|
||||
for mut pane in window.panes {
|
||||
if pane.cmd.contains('htop') {
|
||||
right_pane_fresh = pane
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Select the right pane to make it active
|
||||
right_pane_fresh.select()!
|
||||
time.sleep(200 * time.millisecond)
|
||||
}
|
||||
|
||||
mut bottom_pane := window.pane_split_vertical('top')!
|
||||
time.sleep(500 * time.millisecond)
|
||||
window.scan()!
|
||||
|
||||
println('After vertical split of right pane:')
|
||||
println('Number of panes: ${window.panes.len}')
|
||||
for i, mut pane in window.panes {
|
||||
println(' Pane ${i}: ID=%${pane.id}, PID=${pane.pid}, Active=${pane.active}, Cmd="${pane.cmd}"')
|
||||
}
|
||||
|
||||
// Send commands to different panes
|
||||
println('\n=== Sending Commands to Panes ===')
|
||||
|
||||
// Get the first pane (left side) and send some commands
|
||||
if window.panes.len > 0 {
|
||||
mut left_pane := window.panes[0]
|
||||
println('Sending commands to left pane (ID: %${left_pane.id})')
|
||||
|
||||
left_pane.send_command('echo "Hello from left pane!"')!
|
||||
time.sleep(200 * time.millisecond)
|
||||
|
||||
left_pane.send_command('ls -la')!
|
||||
time.sleep(200 * time.millisecond)
|
||||
|
||||
left_pane.send_command('pwd')!
|
||||
time.sleep(200 * time.millisecond)
|
||||
}
|
||||
|
||||
// Send command to bottom pane
|
||||
if window.panes.len > 2 {
|
||||
mut bottom_pane_ref := window.panes[2]
|
||||
println('Sending command to bottom pane (ID: %${bottom_pane_ref.id})')
|
||||
bottom_pane_ref.send_command('echo "Hello from bottom pane!"')!
|
||||
time.sleep(200 * time.millisecond)
|
||||
}
|
||||
|
||||
// Capture output from panes
|
||||
println('\n=== Capturing Pane Output ===')
|
||||
for i, mut pane in window.panes {
|
||||
println('Output from Pane ${i} (ID: %${pane.id}):')
|
||||
logs := pane.logs_all() or {
|
||||
println(' Could not get logs: ${err}')
|
||||
continue
|
||||
}
|
||||
|
||||
if logs.len > 0 {
|
||||
lines := logs.split_into_lines()
|
||||
// Show last 3 lines
|
||||
start_idx := if lines.len > 3 { lines.len - 3 } else { 0 }
|
||||
for j in start_idx .. lines.len {
|
||||
if lines[j].trim_space().len > 0 {
|
||||
println(' ${lines[j]}')
|
||||
}
|
||||
}
|
||||
}
|
||||
println('')
|
||||
}
|
||||
|
||||
// Demonstrate pane selection
|
||||
println('\n=== Demonstrating Pane Selection ===')
|
||||
for i, mut pane in window.panes {
|
||||
println('Selecting pane ${i} (ID: %${pane.id})')
|
||||
pane.select()!
|
||||
time.sleep(300 * time.millisecond)
|
||||
}
|
||||
|
||||
// Final state
|
||||
println('\n=== Final Tmux State ===')
|
||||
t.scan()!
|
||||
println(t)
|
||||
|
||||
println('\n=== Pane Management Summary ===')
|
||||
println('Created ${window.panes.len} panes in window "${window.name}":')
|
||||
for i, pane in window.panes {
|
||||
println(' ${i + 1}. Pane %${pane.id} - PID: ${pane.pid} - Command: ${pane.cmd}')
|
||||
}
|
||||
|
||||
// Demonstrate killing a pane
|
||||
println('\n=== Demonstrating Pane Killing ===')
|
||||
if window.panes.len > 2 {
|
||||
mut pane_to_kill := window.panes[2] // Kill the bottom pane
|
||||
println('Killing pane %${pane_to_kill.id} (${pane_to_kill.cmd})')
|
||||
pane_to_kill.kill()!
|
||||
time.sleep(500 * time.millisecond)
|
||||
window.scan()!
|
||||
|
||||
println('After killing pane:')
|
||||
println('Number of panes: ${window.panes.len}')
|
||||
for i, pane in window.panes {
|
||||
println(' Pane ${i}: ID=%${pane.id}, PID=${pane.pid}, Cmd="${pane.cmd}"')
|
||||
}
|
||||
}
|
||||
|
||||
println('\n=== Available Pane Operations ===')
|
||||
println('✓ Split panes horizontally (side by side)')
|
||||
println('✓ Split panes vertically (top and bottom)')
|
||||
println('✓ Send commands to specific panes')
|
||||
println('✓ Send raw keys to panes')
|
||||
println('✓ Select/activate panes')
|
||||
println('✓ Capture pane output')
|
||||
println('✓ Get pane process information')
|
||||
println('✓ Kill individual panes')
|
||||
|
||||
println('\nExample completed! You can attach to the session with:')
|
||||
println(' tmux attach-session -t panes_demo')
|
||||
Reference in New Issue
Block a user