feat: Add declarative tmux module functions
- Implement `tmux.session_ensure` for idempotent create - Implement `tmux.window_ensure` with 1 to 16 pane layouts - Implement `tmux.pane_ensure` to configure individual panes - Add new declarative tmux example scripts - Update docs for imperative and declarative paradigms
This commit is contained in:
@@ -19,7 +19,7 @@ The SSH agent functionality is built into the hero binary. After compiling hero:
|
||||
./cli/compile.vsh
|
||||
```
|
||||
|
||||
The hero binary will be available at `/Users/mahmoud/hero/bin/hero`
|
||||
The hero binary will be available at `~/hero/bin/hero`
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
218
examples/tmux/heroscripts/TESTING.md
Normal file
218
examples/tmux/heroscripts/TESTING.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Testing the Declarative TMUX Implementation
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Build the hero binary** with the new tmux functionality:
|
||||
|
||||
```bash
|
||||
cd /Users/mahmoud/code/github/freeflowuniverse/herolib
|
||||
./cli/compile.vsh
|
||||
```
|
||||
|
||||
2. **Ensure tmux is installed** on your system:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install tmux
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install tmux
|
||||
```
|
||||
|
||||
## Test Scripts
|
||||
|
||||
### 1. Simple Declarative Test (Recommended First Test)
|
||||
|
||||
```bash
|
||||
/Users/mahmoud/hero/bin/hero run -p examples/tmux/simple_declarative_test.heroscript
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
- Creates a session named "test"
|
||||
- Creates a window "demo" with 2 panes
|
||||
- Each pane displays a welcome message
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
# List tmux sessions
|
||||
tmux list-sessions
|
||||
|
||||
# Attach to the test session
|
||||
tmux attach-session -t test
|
||||
|
||||
# You should see 2 panes side by side with messages
|
||||
```
|
||||
|
||||
### 2. Full Declarative Example
|
||||
|
||||
```bash
|
||||
/Users/mahmoud/hero/bin/hero run -p examples/tmux/declarative_example.heroscript
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
- Creates "dev" session with 4-pane workspace
|
||||
- Creates "monitoring" session with 2-pane system view
|
||||
- Starts ttyd web access on ports 8080 and 8081
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
# Check sessions
|
||||
tmux list-sessions
|
||||
|
||||
# Check web access (if ttyd is installed)
|
||||
open http://localhost:8080 # Dev session
|
||||
open http://localhost:8081 # Monitoring session
|
||||
|
||||
# Attach to sessions
|
||||
tmux attach-session -t dev
|
||||
tmux attach-session -t monitoring
|
||||
```
|
||||
|
||||
### 3. Paradigm Comparison Test
|
||||
|
||||
```bash
|
||||
/Users/mahmoud/hero/bin/hero run -p examples/tmux/imperative_vs_declarative.heroscript
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
- Creates "imperative_demo" session using step-by-step commands
|
||||
- Creates "declarative_demo" session using state-based configuration
|
||||
- Demonstrates both approaches working together
|
||||
|
||||
## Testing Individual Functions
|
||||
|
||||
### Test Session Ensure (Idempotent)
|
||||
|
||||
Create a test file:
|
||||
|
||||
```heroscript
|
||||
!!tmux.session_ensure
|
||||
name:"test_session"
|
||||
```
|
||||
|
||||
Run it multiple times - should not create duplicates:
|
||||
|
||||
```bash
|
||||
/Users/mahmoud/hero/bin/hero run -p test_session.heroscript
|
||||
/Users/mahmoud/hero/bin/hero run -p test_session.heroscript # Should be safe to run again
|
||||
```
|
||||
|
||||
### Test Window Layouts
|
||||
|
||||
Test different pane layouts:
|
||||
|
||||
```heroscript
|
||||
!!tmux.session_ensure
|
||||
name:"layout_test"
|
||||
|
||||
!!tmux.window_ensure
|
||||
name:"layout_test|test_1pane"
|
||||
cat:"1pane"
|
||||
|
||||
!!tmux.window_ensure
|
||||
name:"layout_test|test_2pane"
|
||||
cat:"2pane"
|
||||
|
||||
!!tmux.window_ensure
|
||||
name:"layout_test|test_4pane"
|
||||
cat:"4pane"
|
||||
```
|
||||
|
||||
### Test Pane Configuration
|
||||
|
||||
```heroscript
|
||||
!!tmux.session_ensure
|
||||
name:"pane_test"
|
||||
|
||||
!!tmux.window_ensure
|
||||
name:"pane_test|demo"
|
||||
cat:"2pane"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"pane_test|demo|1"
|
||||
label:"first"
|
||||
cmd:"echo Hello from pane 1"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"pane_test|demo|2"
|
||||
label:"second"
|
||||
cmd:"htop"
|
||||
env:"TERM=xterm-256color"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"tmux server not running"**
|
||||
|
||||
```bash
|
||||
# Start tmux server
|
||||
tmux new-session -d -s temp
|
||||
tmux kill-session -t temp
|
||||
```
|
||||
|
||||
2. **"Permission denied" for ttyd**
|
||||
|
||||
```bash
|
||||
# Install ttyd if needed
|
||||
brew install ttyd # macOS
|
||||
```
|
||||
|
||||
3. **Syntax errors in heroscript**
|
||||
- Ensure no nested quotes in command strings
|
||||
- Use double quotes for all parameter values
|
||||
- Avoid special characters like `!` in commands
|
||||
|
||||
### Verification Commands
|
||||
|
||||
```bash
|
||||
# List all tmux sessions
|
||||
tmux list-sessions
|
||||
|
||||
# List windows in a session
|
||||
tmux list-windows -t session_name
|
||||
|
||||
# List panes in a window
|
||||
tmux list-panes -t session_name:window_name
|
||||
|
||||
# Kill all tmux sessions (cleanup)
|
||||
tmux kill-server
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
### Declarative Functions Should
|
||||
|
||||
1. **Be Idempotent**: Running the same script multiple times should not create duplicates
|
||||
2. **Handle Dependencies**: Automatically create sessions if windows are requested
|
||||
3. **Respect Layouts**: Create the correct number of panes based on category
|
||||
4. **Execute Commands**: Run specified commands in the correct panes
|
||||
5. **Set Environment**: Apply environment variables to panes
|
||||
|
||||
### Success Indicators
|
||||
|
||||
- ✅ Scripts run without syntax errors
|
||||
- ✅ Sessions and windows are created as specified
|
||||
- ✅ Pane layouts match the requested categories
|
||||
- ✅ Commands execute in the correct panes
|
||||
- ✅ Re-running scripts doesn't create duplicates
|
||||
- ✅ Environment variables are properly set
|
||||
|
||||
## Cleanup
|
||||
|
||||
After testing, clean up tmux sessions:
|
||||
|
||||
```bash
|
||||
# Kill specific sessions
|
||||
tmux kill-session -t test
|
||||
tmux kill-session -t dev
|
||||
tmux kill-session -t monitoring
|
||||
|
||||
# Or kill all sessions
|
||||
tmux kill-server
|
||||
16
examples/tmux/heroscripts/debug_test.heroscript
Normal file
16
examples/tmux/heroscripts/debug_test.heroscript
Normal file
@@ -0,0 +1,16 @@
|
||||
// Debug test - no ttyd, just basic declarative functionality
|
||||
|
||||
// Ensure a test session exists
|
||||
!!tmux.session_ensure
|
||||
name:"debug_test"
|
||||
|
||||
// Ensure a simple 2-pane window exists
|
||||
!!tmux.window_ensure
|
||||
name:"debug_test|demo"
|
||||
cat:"2pane"
|
||||
|
||||
// Try to configure just one pane
|
||||
!!tmux.pane_ensure
|
||||
name:"debug_test|demo|1"
|
||||
label:"test"
|
||||
cmd:"echo Hello from pane 1"
|
||||
64
examples/tmux/heroscripts/declarative_example.heroscript
Normal file
64
examples/tmux/heroscripts/declarative_example.heroscript
Normal file
@@ -0,0 +1,64 @@
|
||||
// Declarative TMUX Configuration Example
|
||||
// This demonstrates the declarative paradigm where we define the desired state
|
||||
|
||||
// Ensure a development session exists
|
||||
!!tmux.session_ensure
|
||||
name:'dev'
|
||||
|
||||
// Ensure a monitoring session exists
|
||||
!!tmux.session_ensure
|
||||
name:'monitoring'
|
||||
|
||||
// Ensure a 4-pane development window exists
|
||||
!!tmux.window_ensure
|
||||
name:"dev|workspace"
|
||||
cat:"4pane"
|
||||
|
||||
// Ensure a 2-pane monitoring window exists
|
||||
!!tmux.window_ensure
|
||||
name:"monitoring|system"
|
||||
cat:"2pane"
|
||||
|
||||
// Ensure specific panes with commands
|
||||
!!tmux.pane_ensure
|
||||
name:"dev|workspace|1"
|
||||
label:"editor"
|
||||
cmd:"echo Starting editor... && sleep 1 && echo Editor ready"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"dev|workspace|2"
|
||||
label:"server"
|
||||
cmd:"echo Starting development server... && sleep 2 && echo Server running on port 3000"
|
||||
env:"PORT=3000,NODE_ENV=development"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"dev|workspace|3"
|
||||
label:"logs"
|
||||
cmd:"echo Monitoring logs... && tail -f /dev/null"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"dev|workspace|4"
|
||||
label:"terminal"
|
||||
cmd:"echo Terminal ready for commands"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"monitoring|system|1"
|
||||
label:"htop"
|
||||
cmd:"htop"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"monitoring|system|2"
|
||||
label:"network"
|
||||
cmd:"echo Network monitoring... && netstat -tuln"
|
||||
|
||||
// Start web access for the development session
|
||||
!!tmux.session_ttyd
|
||||
name:'dev'
|
||||
port:8080
|
||||
editable:true
|
||||
|
||||
// Start web access for the monitoring session
|
||||
!!tmux.session_ttyd
|
||||
name:'monitoring'
|
||||
port:8081
|
||||
editable:false
|
||||
108
examples/tmux/heroscripts/imperative_vs_declarative.heroscript
Normal file
108
examples/tmux/heroscripts/imperative_vs_declarative.heroscript
Normal file
@@ -0,0 +1,108 @@
|
||||
// TMUX Configuration: Imperative vs Declarative Examples
|
||||
// This file demonstrates both paradigms for comparison
|
||||
|
||||
// =============================================================================
|
||||
// IMPERATIVE APPROACH - Action after action builds the output
|
||||
// =============================================================================
|
||||
|
||||
// Create sessions step by step
|
||||
!!tmux.session_create
|
||||
name:'imperative_demo'
|
||||
reset:true
|
||||
|
||||
// Create windows one by one
|
||||
!!tmux.window_create
|
||||
name:"imperative_demo|editor"
|
||||
cmd:'vim'
|
||||
reset:true
|
||||
|
||||
!!tmux.window_create
|
||||
name:"imperative_demo|server"
|
||||
cmd:'python3 -m http.server 8000'
|
||||
env:'PORT=8000,DEBUG=true'
|
||||
reset:true
|
||||
|
||||
// Split panes manually
|
||||
!!tmux.pane_split
|
||||
name:"imperative_demo|editor"
|
||||
cmd:'htop'
|
||||
horizontal:true
|
||||
|
||||
!!tmux.pane_split
|
||||
name:"imperative_demo|editor"
|
||||
cmd:'tail -f /var/log/system.log'
|
||||
horizontal:false
|
||||
|
||||
// Execute commands in specific panes
|
||||
!!tmux.pane_execute
|
||||
name:"imperative_demo|editor|main"
|
||||
cmd:"echo Imperative setup complete!"
|
||||
|
||||
// =============================================================================
|
||||
// DECLARATIVE APPROACH - Describe the desired state
|
||||
// =============================================================================
|
||||
|
||||
// Ensure sessions exist (idempotent)
|
||||
!!tmux.session_ensure
|
||||
name:'declarative_demo'
|
||||
|
||||
// Ensure windows with specific layouts exist
|
||||
!!tmux.window_ensure
|
||||
name:"declarative_demo|workspace"
|
||||
cat:"4pane" // Automatically creates 4-pane layout
|
||||
|
||||
!!tmux.window_ensure
|
||||
name:"declarative_demo|monitoring"
|
||||
cat:"2pane" // Automatically creates 2-pane layout
|
||||
|
||||
// Ensure specific panes with their desired state
|
||||
!!tmux.pane_ensure
|
||||
name:"declarative_demo|workspace|1"
|
||||
label:"editor"
|
||||
cmd:"echo Editor pane ready && vim"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"declarative_demo|workspace|2"
|
||||
label:"server"
|
||||
cmd:"echo Starting server... && python3 -m http.server 8000"
|
||||
env:"PORT=8000,DEBUG=true"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"declarative_demo|workspace|3"
|
||||
label:"logs"
|
||||
cmd:"echo Monitoring logs... && tail -f /var/log/system.log"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"declarative_demo|workspace|4"
|
||||
label:"terminal"
|
||||
cmd:"echo Terminal ready for commands"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"declarative_demo|monitoring|1"
|
||||
label:"system"
|
||||
cmd:"htop"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"declarative_demo|monitoring|2"
|
||||
label:"network"
|
||||
cmd:"netstat -tuln"
|
||||
|
||||
// =============================================================================
|
||||
// COMPARISON SUMMARY
|
||||
// =============================================================================
|
||||
|
||||
// IMPERATIVE:
|
||||
// - Explicit step-by-step actions
|
||||
// - Order matters
|
||||
// - More control over exact process
|
||||
// - Can fail if intermediate steps fail
|
||||
// - Good for one-time setup scripts
|
||||
|
||||
// DECLARATIVE:
|
||||
// - Describes desired end state
|
||||
// - Idempotent (can run multiple times safely)
|
||||
// - Automatically handles missing dependencies
|
||||
// - More resilient to partial failures
|
||||
// - Good for configuration management
|
||||
|
||||
// Both approaches can be mixed in the same script as needed!
|
||||
23
examples/tmux/heroscripts/simple_declarative_test.heroscript
Normal file
23
examples/tmux/heroscripts/simple_declarative_test.heroscript
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env hero
|
||||
|
||||
// Simple test of declarative tmux functionality
|
||||
|
||||
// Ensure a test session exists
|
||||
!!tmux.session_ensure
|
||||
name:"test"
|
||||
|
||||
// Ensure a simple 2-pane window exists
|
||||
!!tmux.window_ensure
|
||||
name:"test|demo"
|
||||
cat:"2pane"
|
||||
|
||||
// Configure the panes
|
||||
!!tmux.pane_ensure
|
||||
name:"test|demo|1"
|
||||
label:"first"
|
||||
cmd:"echo First pane ready"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"test|demo|2"
|
||||
label:"second"
|
||||
cmd:"echo Second pane ready"
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env hero
|
||||
|
||||
// Tmux Cleanup Script - Tears down all tmux sessions, windows, and panes
|
||||
// Run this after tmux_setup.heroscript to clean up everything
|
||||
|
||||
@@ -17,6 +17,7 @@ pub fn play(mut plbook PlayBook) ! {
|
||||
tmux_instance.start()!
|
||||
}
|
||||
|
||||
// Imperative functions (action after action)
|
||||
play_session_create(mut plbook, mut tmux_instance)!
|
||||
play_session_delete(mut plbook, mut tmux_instance)!
|
||||
play_window_create(mut plbook, mut tmux_instance)!
|
||||
@@ -29,6 +30,11 @@ pub fn play(mut plbook PlayBook) ! {
|
||||
play_session_ttyd_stop(mut plbook, mut tmux_instance)!
|
||||
play_window_ttyd_stop(mut plbook, mut tmux_instance)!
|
||||
play_ttyd_stop_all(mut plbook, mut tmux_instance)!
|
||||
|
||||
// Declarative functions (desired state)
|
||||
play_session_ensure(mut plbook, mut tmux_instance)!
|
||||
play_window_ensure(mut plbook, mut tmux_instance)!
|
||||
play_pane_ensure(mut plbook, mut tmux_instance)!
|
||||
}
|
||||
|
||||
struct ParsedWindowName {
|
||||
@@ -337,3 +343,384 @@ fn play_ttyd_stop_all(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
// DECLARATIVE FUNCTIONS - Ensure desired state exists
|
||||
|
||||
// Ensure session exists (declarative)
|
||||
fn play_session_ensure(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
mut actions := plbook.find(filter: 'tmux.session_ensure')!
|
||||
for mut action in actions {
|
||||
mut p := action.params
|
||||
session_name := p.get('name')!
|
||||
|
||||
// Ensure session exists, create if it doesn't
|
||||
if !tmux_instance.session_exist(session_name) {
|
||||
tmux_instance.session_create(name: session_name)!
|
||||
}
|
||||
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
// Pane layout configurations for different categories
|
||||
struct PaneLayout {
|
||||
splits []PaneSplit
|
||||
}
|
||||
|
||||
struct PaneSplit {
|
||||
horizontal bool
|
||||
target_pane int // which pane to split (0-based index)
|
||||
}
|
||||
|
||||
// Get pane layout configuration based on category
|
||||
fn get_pane_layout(category string) PaneLayout {
|
||||
match category {
|
||||
'1pane' {
|
||||
return PaneLayout{
|
||||
splits: []
|
||||
}
|
||||
}
|
||||
'2pane' {
|
||||
return PaneLayout{
|
||||
splits: [
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 0
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
'4pane' {
|
||||
return PaneLayout{
|
||||
splits: [
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 0
|
||||
}, // Split horizontally first
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 0
|
||||
}, // Split left pane vertically
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 1
|
||||
}, // Split right pane vertically
|
||||
]
|
||||
}
|
||||
}
|
||||
'6pane' {
|
||||
return PaneLayout{
|
||||
splits: [
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 0
|
||||
}, // Split horizontally
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 1
|
||||
}, // Split right pane horizontally
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 0
|
||||
}, // Split left pane vertically
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 1
|
||||
}, // Split middle pane vertically
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 2
|
||||
}, // Split right pane vertically
|
||||
]
|
||||
}
|
||||
}
|
||||
'8pane' {
|
||||
return PaneLayout{
|
||||
splits: [
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 0
|
||||
}, // Split horizontally
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 0
|
||||
}, // Split left vertically
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 1
|
||||
}, // Split right vertically
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 0
|
||||
}, // Split top-left horizontally
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 1
|
||||
}, // Split bottom-left horizontally
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 2
|
||||
}, // Split top-right horizontally
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 3
|
||||
}, // Split bottom-right horizontally
|
||||
]
|
||||
}
|
||||
}
|
||||
'12pane' {
|
||||
return PaneLayout{
|
||||
splits: [
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 0
|
||||
}, // Split horizontally (2 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 1
|
||||
}, // Split right horizontally (3 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 0
|
||||
}, // Split left vertically (4 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 1
|
||||
}, // Split middle vertically (5 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 2
|
||||
}, // Split right vertically (6 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 0
|
||||
}, // Split top-left horizontally (7 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 1
|
||||
}, // Split bottom-left horizontally (8 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 2
|
||||
}, // Split top-middle horizontally (9 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 3
|
||||
}, // Split bottom-middle horizontally (10 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 4
|
||||
}, // Split top-right horizontally (11 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 5
|
||||
}, // Split bottom-right horizontally (12 panes)
|
||||
]
|
||||
}
|
||||
}
|
||||
'16pane' {
|
||||
return PaneLayout{
|
||||
splits: [
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 0
|
||||
}, // Split horizontally (2 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 0
|
||||
}, // Split left vertically (3 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 1
|
||||
}, // Split right vertically (4 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 0
|
||||
}, // Split top-left horizontally (5 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 1
|
||||
}, // Split bottom-left horizontally (6 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 2
|
||||
}, // Split top-right horizontally (7 panes)
|
||||
PaneSplit{
|
||||
horizontal: true
|
||||
target_pane: 3
|
||||
}, // Split bottom-right horizontally (8 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 0
|
||||
}, // Split first quarter vertically (9 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 1
|
||||
}, // Split second quarter vertically (10 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 2
|
||||
}, // Split third quarter vertically (11 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 3
|
||||
}, // Split fourth quarter vertically (12 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 4
|
||||
}, // Split fifth quarter vertically (13 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 5
|
||||
}, // Split sixth quarter vertically (14 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 6
|
||||
}, // Split seventh quarter vertically (15 panes)
|
||||
PaneSplit{
|
||||
horizontal: false
|
||||
target_pane: 7
|
||||
}, // Split eighth quarter vertically (16 panes)
|
||||
]
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Default to 1pane if unknown category
|
||||
return PaneLayout{
|
||||
splits: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure window exists with specified pane layout (declarative)
|
||||
fn play_window_ensure(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
mut actions := plbook.find(filter: 'tmux.window_ensure')!
|
||||
for mut action in actions {
|
||||
mut p := action.params
|
||||
name := p.get('name')!
|
||||
parsed := parse_window_name(name)!
|
||||
category := p.get_default('cat', '1pane')!
|
||||
cmd := p.get_default('cmd', '')!
|
||||
|
||||
// Parse environment variables if provided
|
||||
mut env := map[string]string{}
|
||||
if env_str := p.get_default('env', '') {
|
||||
env_pairs := env_str.split(',')
|
||||
for pair in env_pairs {
|
||||
kv := pair.split('=')
|
||||
if kv.len == 2 {
|
||||
env[kv[0].trim_space()] = kv[1].trim_space()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure session exists
|
||||
mut session := if tmux_instance.session_exist(parsed.session) {
|
||||
tmux_instance.session_get(parsed.session)!
|
||||
} else {
|
||||
tmux_instance.session_create(name: parsed.session)!
|
||||
}
|
||||
|
||||
// Check if window already exists with correct pane layout
|
||||
mut window_exists := session.window_exist(name: parsed.window)
|
||||
mut window := if window_exists {
|
||||
session.window_get(name: parsed.window)!
|
||||
} else {
|
||||
// Create new window
|
||||
session.window_new(
|
||||
name: parsed.window
|
||||
cmd: cmd
|
||||
env: env
|
||||
)!
|
||||
}
|
||||
|
||||
// Ensure correct pane layout
|
||||
layout := get_pane_layout(category)
|
||||
current_pane_count := window.panes.len
|
||||
|
||||
// If we need more panes, create them according to layout
|
||||
if layout.splits.len + 1 > current_pane_count {
|
||||
// We need to create the layout from scratch
|
||||
// First, ensure we have at least one pane (the window should have one by default)
|
||||
window.scan()! // Refresh pane information
|
||||
|
||||
// Apply splits according to layout
|
||||
for split in layout.splits {
|
||||
// For simplicity, we'll split the active pane
|
||||
// In a more sophisticated implementation, we could track specific panes
|
||||
window.pane_split(
|
||||
cmd: cmd
|
||||
horizontal: split.horizontal
|
||||
env: env
|
||||
)!
|
||||
}
|
||||
}
|
||||
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure specific pane exists with command and label (declarative)
|
||||
fn play_pane_ensure(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
mut actions := plbook.find(filter: 'tmux.pane_ensure')!
|
||||
for mut action in actions {
|
||||
mut p := action.params
|
||||
name := p.get('name')!
|
||||
parsed := parse_pane_name(name)!
|
||||
cmd := p.get_default('cmd', '')!
|
||||
label := p.get_default('label', '')!
|
||||
|
||||
// Parse environment variables if provided
|
||||
mut env := map[string]string{}
|
||||
if env_str := p.get_default('env', '') {
|
||||
env_pairs := env_str.split(',')
|
||||
for pair in env_pairs {
|
||||
kv := pair.split('=')
|
||||
if kv.len == 2 {
|
||||
env[kv[0].trim_space()] = kv[1].trim_space()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure session exists
|
||||
mut session := if tmux_instance.session_exist(parsed.session) {
|
||||
tmux_instance.session_get(parsed.session)!
|
||||
} else {
|
||||
tmux_instance.session_create(name: parsed.session)!
|
||||
}
|
||||
|
||||
// Ensure window exists
|
||||
mut window := if session.window_exist(name: parsed.window) {
|
||||
session.window_get(name: parsed.window)!
|
||||
} else {
|
||||
session.window_new(name: parsed.window)!
|
||||
}
|
||||
|
||||
// Refresh pane information
|
||||
window.scan()!
|
||||
|
||||
// Check if we need to create more panes or execute command in existing pane
|
||||
pane_number := parsed.pane.int()
|
||||
|
||||
// Ensure we have enough panes (create splits if needed)
|
||||
for window.panes.len < pane_number {
|
||||
window.pane_split(
|
||||
cmd: '/bin/bash'
|
||||
horizontal: window.panes.len % 2 == 0 // Alternate between horizontal and vertical
|
||||
env: env
|
||||
)!
|
||||
}
|
||||
|
||||
// Execute command in the specified pane if provided
|
||||
if cmd.len > 0 {
|
||||
// Find the target pane (by index, since tmux pane IDs can vary)
|
||||
if pane_number > 0 && pane_number <= window.panes.len {
|
||||
mut target_pane := window.panes[pane_number - 1] // Convert to 0-based index
|
||||
target_pane.send_command(cmd)!
|
||||
}
|
||||
}
|
||||
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,29 @@ tmux library provides functions for managing tmux sessions
|
||||
## to attach to a tmux session
|
||||
|
||||
>
|
||||
## HeroScript Declarative Support
|
||||
## HeroScript Programming Paradigms
|
||||
|
||||
The tmux module supports declarative configuration through heroscript, allowing you to define tmux sessions, windows, and panes in a structured way.
|
||||
The tmux module supports both **Imperative** and **Declarative** programming paradigms through heroscript, allowing you to choose the approach that best fits your use case.
|
||||
|
||||
### Imperative vs Declarative
|
||||
|
||||
**Imperative Approach:**
|
||||
|
||||
- Explicit step-by-step actions
|
||||
- Order matters
|
||||
- More control over exact process
|
||||
- Can fail if intermediate steps fail
|
||||
- Good for one-time setup scripts
|
||||
|
||||
**Declarative Approach:**
|
||||
|
||||
- Describes desired end state
|
||||
- Idempotent (can run multiple times safely)
|
||||
- Automatically handles missing dependencies
|
||||
- More resilient to partial failures
|
||||
- Good for configuration management
|
||||
|
||||
Both paradigms can be mixed in the same script as needed!
|
||||
|
||||
### Running HeroScript
|
||||
|
||||
@@ -31,7 +51,7 @@ The tmux module supports declarative configuration through heroscript, allowing
|
||||
hero run -p <heroscript_file>
|
||||
```
|
||||
|
||||
### Supported Actions
|
||||
### Imperative Actions (Traditional)
|
||||
|
||||
#### Session Management
|
||||
|
||||
@@ -74,6 +94,17 @@ hero run -p <heroscript_file>
|
||||
name:"mysession|mywindow|mypane"
|
||||
```
|
||||
|
||||
#### Pane Splitting
|
||||
|
||||
```heroscript
|
||||
// Split a pane horizontally or vertically
|
||||
!!tmux.pane_split
|
||||
name:"mysession|mywindow"
|
||||
cmd:'htop'
|
||||
horizontal:true // true for horizontal, false for vertical
|
||||
env:'VAR1=value1'
|
||||
```
|
||||
|
||||
#### Ttyd Management
|
||||
|
||||
```heroscript
|
||||
@@ -103,7 +134,51 @@ hero run -p <heroscript_file>
|
||||
!!tmux.ttyd_stop_all
|
||||
```
|
||||
|
||||
### Complete Example
|
||||
### Declarative Actions (State-Based)
|
||||
|
||||
#### Session Ensure
|
||||
|
||||
```heroscript
|
||||
// Ensure session exists (idempotent)
|
||||
!!tmux.session_ensure
|
||||
name:'mysession'
|
||||
```
|
||||
|
||||
#### Window Ensure with Pane Layouts
|
||||
|
||||
```heroscript
|
||||
// Ensure window exists with specific pane layout
|
||||
!!tmux.window_ensure
|
||||
name:"mysession|mywindow"
|
||||
cat:"4pane" // Supported: 16pane, 12pane, 8pane, 6pane, 4pane, 2pane, 1pane
|
||||
cmd:'bash' // Optional: default command for panes
|
||||
env:'VAR1=value1,VAR2=value2' // Optional: environment variables
|
||||
```
|
||||
|
||||
#### Pane Ensure
|
||||
|
||||
```heroscript
|
||||
// Ensure specific pane exists with command
|
||||
!!tmux.pane_ensure
|
||||
name:"mysession|mywindow|1" // Pane number (1-based)
|
||||
label:'editor' // Optional: descriptive label
|
||||
cmd:'vim' // Optional: command to run
|
||||
env:'EDITOR=vim' // Optional: environment variables
|
||||
```
|
||||
|
||||
### Pane Layout Categories
|
||||
|
||||
The declarative `window_ensure` action supports predefined pane layouts:
|
||||
|
||||
- **1pane**: Single pane (default)
|
||||
- **2pane**: Two panes side by side
|
||||
- **4pane**: Four panes in a 2x2 grid
|
||||
- **6pane**: Six panes in a 2x3 layout
|
||||
- **8pane**: Eight panes in a 2x4 layout
|
||||
- **12pane**: Twelve panes in a 3x4 layout
|
||||
- **16pane**: Sixteen panes in a 4x4 layout
|
||||
|
||||
### Complete Imperative Example
|
||||
|
||||
```heroscript
|
||||
#!/usr/bin/env hero
|
||||
@@ -137,37 +212,70 @@ hero run -p <heroscript_file>
|
||||
|
||||
Names are automatically normalized using `texttools.name_fix()` for consistency.
|
||||
|
||||
### Complete Declarative Example
|
||||
|
||||
```heroscript
|
||||
#!/usr/bin/env hero
|
||||
|
||||
// Ensure sessions exist
|
||||
!!tmux.session_ensure
|
||||
name:'dev'
|
||||
|
||||
// Ensure 4-pane development workspace
|
||||
!!tmux.window_ensure
|
||||
name:"dev|workspace"
|
||||
cat:"4pane"
|
||||
|
||||
// Configure each pane with specific commands
|
||||
!!tmux.pane_ensure
|
||||
name:"dev|workspace|1"
|
||||
label:'editor'
|
||||
cmd:'vim'
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"dev|workspace|2"
|
||||
label:'server'
|
||||
cmd:'python3 -m http.server 8000'
|
||||
env:'PORT=8000'
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"dev|workspace|3"
|
||||
label:'logs'
|
||||
cmd:'tail -f /var/log/system.log'
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"dev|workspace|4"
|
||||
label:'terminal'
|
||||
cmd:'echo "Ready for commands"'
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Setup and Cleanup Scripts
|
||||
### Example Scripts
|
||||
|
||||
Two example heroscripts are provided to demonstrate complete tmux environment management:
|
||||
Several example heroscripts are provided to demonstrate both paradigms:
|
||||
|
||||
#### 1. Setup Script (`tmux_setup.heroscript`)
|
||||
#### 1. Declarative Example (`declarative_example.heroscript`)
|
||||
|
||||
Creates a complete development environment with multiple sessions and windows:
|
||||
Pure declarative approach showing state-based configuration:
|
||||
|
||||
```bash
|
||||
hero run examples/tmux/tmux_setup.heroscript
|
||||
hero run examples/tmux/declarative_example.heroscript
|
||||
```
|
||||
|
||||
This creates:
|
||||
#### 2. Paradigm Comparison (`imperative_vs_declarative.heroscript`)
|
||||
|
||||
- **dev session** with editor, server, logs, and a 4-pane services window
|
||||
- **monitoring session** with htop and network monitoring windows
|
||||
- **Web access** via ttyd on ports 8080, 8081, and 7681
|
||||
|
||||
#### 2. Cleanup Script (`tmux_cleanup.heroscript`)
|
||||
|
||||
Tears down all created tmux resources:
|
||||
Side-by-side comparison of both approaches:
|
||||
|
||||
```bash
|
||||
hero run examples/tmux/tmux_cleanup.heroscript
|
||||
hero run examples/tmux/imperative_vs_declarative.heroscript
|
||||
```
|
||||
|
||||
This removes:
|
||||
#### 3. Setup and Cleanup Scripts
|
||||
|
||||
- All windows from both sessions
|
||||
- Both dev and monitoring sessions
|
||||
- All associated panes
|
||||
- All ttyd web processes (ports 8080, 8081, 7681)
|
||||
Traditional imperative scripts for environment management:
|
||||
|
||||
```bash
|
||||
hero run examples/tmux/tmux_setup.heroscript # Setup
|
||||
hero run examples/tmux/tmux_cleanup.heroscript # Cleanup
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user