feat: add declarative tmux and ttyd management
- Implement `tmux.pane_split` action - Add declarative `tmux.session_ttyd` and `tmux.window_ttyd` - Include `tmux.session_ttyd_stop`, `window_ttyd_stop`, `ttyd_stop_all` - Update tmux documentation and add usage examples - Improve robustness of tmux session and window scanning
This commit is contained in:
44
examples/tmux/tmux_cleanup.heroscript
Normal file
44
examples/tmux/tmux_cleanup.heroscript
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
// Kill specific windows first (optional - sessions will kill all windows anyway)
|
||||||
|
!!tmux.window_delete
|
||||||
|
name:"dev|editor"
|
||||||
|
|
||||||
|
!!tmux.window_delete
|
||||||
|
name:"dev|server"
|
||||||
|
|
||||||
|
!!tmux.window_delete
|
||||||
|
name:"dev|logs"
|
||||||
|
|
||||||
|
!!tmux.window_delete
|
||||||
|
name:"dev|services"
|
||||||
|
|
||||||
|
!!tmux.window_delete
|
||||||
|
name:"monitoring|htop"
|
||||||
|
|
||||||
|
!!tmux.window_delete
|
||||||
|
name:"monitoring|network"
|
||||||
|
|
||||||
|
// Delete all sessions (this will kill all windows and panes within them)
|
||||||
|
!!tmux.session_delete
|
||||||
|
name:'dev'
|
||||||
|
|
||||||
|
!!tmux.session_delete
|
||||||
|
name:'monitoring'
|
||||||
|
|
||||||
|
// Optional: Kill any remaining panes explicitly (usually not needed after session delete)
|
||||||
|
!!tmux.pane_kill
|
||||||
|
name:"dev|editor|main"
|
||||||
|
|
||||||
|
!!tmux.pane_kill
|
||||||
|
name:"dev|server|main"
|
||||||
|
|
||||||
|
!!tmux.pane_kill
|
||||||
|
name:"monitoring|htop|main"
|
||||||
|
|
||||||
|
// Stop any remaining ttyd processes system-wide
|
||||||
|
// This will clean up all ttyd processes regardless of which sessions exist
|
||||||
|
!!tmux.ttyd_stop_all
|
||||||
105
examples/tmux/tmux_setup.heroscript
Normal file
105
examples/tmux/tmux_setup.heroscript
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Create development session
|
||||||
|
!!tmux.session_create
|
||||||
|
name:'dev'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
// Create monitoring session
|
||||||
|
!!tmux.session_create
|
||||||
|
name:'monitoring'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
// Create development windows (use reset to ensure clean state)
|
||||||
|
!!tmux.window_create
|
||||||
|
name:"dev|editor"
|
||||||
|
cmd:'vim'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
!!tmux.window_create
|
||||||
|
name:"dev|server"
|
||||||
|
cmd:'python3 -m http.server 8000'
|
||||||
|
env:'PORT=8000,DEBUG=true'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
!!tmux.window_create
|
||||||
|
name:"dev|logs"
|
||||||
|
cmd:'tail -f /var/log/system.log'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
// Create monitoring windows
|
||||||
|
!!tmux.window_create
|
||||||
|
name:"monitoring|htop"
|
||||||
|
cmd:'htop'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
!!tmux.window_create
|
||||||
|
name:"monitoring|network"
|
||||||
|
cmd:'netstat -tuln'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
// Create a multi-service window with 4 panes
|
||||||
|
!!tmux.window_create
|
||||||
|
name:"dev|services"
|
||||||
|
cmd:'htop'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
// Split the services window into 4 panes (1 initial + 3 splits = 4 total)
|
||||||
|
!!tmux.pane_split
|
||||||
|
name:"dev|services"
|
||||||
|
cmd:'python3 -m http.server 3000'
|
||||||
|
horizontal:true
|
||||||
|
|
||||||
|
!!tmux.pane_split
|
||||||
|
name:"dev|services"
|
||||||
|
cmd:'watch -n 1 ps aux'
|
||||||
|
horizontal:false
|
||||||
|
|
||||||
|
!!tmux.pane_split
|
||||||
|
name:"dev|services"
|
||||||
|
cmd:'tail -f /var/log/system.log'
|
||||||
|
horizontal:true
|
||||||
|
|
||||||
|
!!tmux.pane_split
|
||||||
|
name:"dev|services"
|
||||||
|
cmd:'echo Fourth pane ready for commands'
|
||||||
|
horizontal:false
|
||||||
|
|
||||||
|
// Execute welcome commands in panes
|
||||||
|
!!tmux.pane_execute
|
||||||
|
name:"dev|editor|main"
|
||||||
|
cmd:'echo Welcome to the editor pane'
|
||||||
|
|
||||||
|
!!tmux.pane_execute
|
||||||
|
name:"dev|server|main"
|
||||||
|
cmd:'echo Starting development server'
|
||||||
|
|
||||||
|
!!tmux.pane_execute
|
||||||
|
name:"monitoring|htop|main"
|
||||||
|
cmd:'echo System monitoring active'
|
||||||
|
|
||||||
|
// Split panes for better workflow
|
||||||
|
!!tmux.pane_split
|
||||||
|
name:"dev|editor"
|
||||||
|
cmd:'echo Split pane for terminal'
|
||||||
|
horizontal:false
|
||||||
|
|
||||||
|
!!tmux.pane_split
|
||||||
|
name:"monitoring|htop"
|
||||||
|
cmd:'watch -n 1 df -h'
|
||||||
|
horizontal:true
|
||||||
|
|
||||||
|
// Expose sessions and windows via web browser using ttyd
|
||||||
|
!!tmux.session_ttyd
|
||||||
|
name:'dev'
|
||||||
|
port:8080
|
||||||
|
editable:true
|
||||||
|
|
||||||
|
!!tmux.window_ttyd
|
||||||
|
name:"monitoring|htop"
|
||||||
|
port:8081
|
||||||
|
editable:false
|
||||||
|
|
||||||
|
// Expose the 4-pane services window via web
|
||||||
|
!!tmux.window_ttyd
|
||||||
|
name:"dev|services"
|
||||||
|
port:7681
|
||||||
|
editable:false
|
||||||
@@ -7,6 +7,7 @@ import freeflowuniverse.herolib.web.site
|
|||||||
import freeflowuniverse.herolib.web.docusaurus
|
import freeflowuniverse.herolib.web.docusaurus
|
||||||
import freeflowuniverse.herolib.clients.openai
|
import freeflowuniverse.herolib.clients.openai
|
||||||
import freeflowuniverse.herolib.clients.giteaclient
|
import freeflowuniverse.herolib.clients.giteaclient
|
||||||
|
import freeflowuniverse.herolib.osal.tmux
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// run – entry point for all HeroScript play‑commands
|
// run – entry point for all HeroScript play‑commands
|
||||||
@@ -39,6 +40,9 @@ pub fn run(args_ PlayArgs) ! {
|
|||||||
// Git actions
|
// Git actions
|
||||||
play_git(mut plbook)!
|
play_git(mut plbook)!
|
||||||
|
|
||||||
|
// Tmux actions
|
||||||
|
tmux.play(mut plbook)!
|
||||||
|
|
||||||
// Business model (e.g. currency, bizmodel)
|
// Business model (e.g. currency, bizmodel)
|
||||||
bizmodel.play(mut plbook)!
|
bizmodel.play(mut plbook)!
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ pub fn play(mut plbook PlayBook) ! {
|
|||||||
play_window_delete(mut plbook, mut tmux_instance)!
|
play_window_delete(mut plbook, mut tmux_instance)!
|
||||||
play_pane_execute(mut plbook, mut tmux_instance)!
|
play_pane_execute(mut plbook, mut tmux_instance)!
|
||||||
play_pane_kill(mut plbook, mut tmux_instance)!
|
play_pane_kill(mut plbook, mut tmux_instance)!
|
||||||
// TODO: Implement pane_create, pane_delete, pane_split when pane API is extended
|
play_pane_split(mut plbook, mut tmux_instance)!
|
||||||
|
play_session_ttyd(mut plbook, mut tmux_instance)!
|
||||||
|
play_window_ttyd(mut plbook, mut tmux_instance)!
|
||||||
|
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)!
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParsedWindowName {
|
struct ParsedWindowName {
|
||||||
@@ -43,8 +48,8 @@ fn parse_window_name(name string) !ParsedWindowName {
|
|||||||
return error('Window name must be in format "session|window", got: ${name}')
|
return error('Window name must be in format "session|window", got: ${name}')
|
||||||
}
|
}
|
||||||
return ParsedWindowName{
|
return ParsedWindowName{
|
||||||
session: texttools.name_fix(parts[0])
|
session: texttools.name_fix_token(parts[0])
|
||||||
window: texttools.name_fix(parts[1])
|
window: texttools.name_fix_token(parts[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +59,9 @@ fn parse_pane_name(name string) !ParsedPaneName {
|
|||||||
return error('Pane name must be in format "session|window|pane", got: ${name}')
|
return error('Pane name must be in format "session|window|pane", got: ${name}')
|
||||||
}
|
}
|
||||||
return ParsedPaneName{
|
return ParsedPaneName{
|
||||||
session: texttools.name_fix(parts[0])
|
session: texttools.name_fix_token(parts[0])
|
||||||
window: texttools.name_fix(parts[1])
|
window: texttools.name_fix_token(parts[1])
|
||||||
pane: texttools.name_fix(parts[2])
|
pane: texttools.name_fix_token(parts[2])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,3 +202,138 @@ fn play_pane_kill(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
|||||||
action.done = true
|
action.done = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn play_pane_split(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||||
|
mut actions := plbook.find(filter: 'tmux.pane_split')!
|
||||||
|
for mut action in actions {
|
||||||
|
mut p := action.params
|
||||||
|
name := p.get('name')!
|
||||||
|
cmd := p.get_default('cmd', '')!
|
||||||
|
horizontal := p.get_default_false('horizontal')
|
||||||
|
parsed := parse_window_name(name)!
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the session and window
|
||||||
|
if tmux_instance.session_exist(parsed.session) {
|
||||||
|
mut session := tmux_instance.session_get(parsed.session)!
|
||||||
|
if session.window_exist(name: parsed.window) {
|
||||||
|
mut window := session.window_get(name: parsed.window)!
|
||||||
|
|
||||||
|
// Split the pane
|
||||||
|
window.pane_split(
|
||||||
|
cmd: cmd
|
||||||
|
horizontal: horizontal
|
||||||
|
env: env
|
||||||
|
)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_session_ttyd(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||||
|
mut actions := plbook.find(filter: 'tmux.session_ttyd')!
|
||||||
|
for mut action in actions {
|
||||||
|
mut p := action.params
|
||||||
|
session_name := p.get('name')!
|
||||||
|
port := p.get_int('port')!
|
||||||
|
editable := p.get_default_false('editable')
|
||||||
|
|
||||||
|
if tmux_instance.session_exist(session_name) {
|
||||||
|
mut session := tmux_instance.session_get(session_name)!
|
||||||
|
session.run_ttyd(
|
||||||
|
port: port
|
||||||
|
editable: editable
|
||||||
|
)!
|
||||||
|
}
|
||||||
|
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_window_ttyd(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||||
|
mut actions := plbook.find(filter: 'tmux.window_ttyd')!
|
||||||
|
for mut action in actions {
|
||||||
|
mut p := action.params
|
||||||
|
name := p.get('name')!
|
||||||
|
port := p.get_int('port')!
|
||||||
|
editable := p.get_default_false('editable')
|
||||||
|
parsed := parse_window_name(name)!
|
||||||
|
|
||||||
|
if tmux_instance.session_exist(parsed.session) {
|
||||||
|
mut session := tmux_instance.session_get(parsed.session)!
|
||||||
|
if session.window_exist(name: parsed.window) {
|
||||||
|
mut window := session.window_get(name: parsed.window)!
|
||||||
|
window.run_ttyd(
|
||||||
|
port: port
|
||||||
|
editable: editable
|
||||||
|
)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tmux.session_ttyd_stop actions
|
||||||
|
fn play_session_ttyd_stop(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||||
|
for mut action in plbook.find(filter: 'tmux.session_ttyd_stop')! {
|
||||||
|
if action.done {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mut p := action.params
|
||||||
|
session_name := p.get('name')!
|
||||||
|
port := p.get_int('port')!
|
||||||
|
|
||||||
|
mut session := tmux_instance.session_get(session_name)!
|
||||||
|
session.stop_ttyd(port)!
|
||||||
|
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tmux.window_ttyd_stop actions
|
||||||
|
fn play_window_ttyd_stop(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||||
|
for mut action in plbook.find(filter: 'tmux.window_ttyd_stop')! {
|
||||||
|
if action.done {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mut p := action.params
|
||||||
|
name := p.get('name')!
|
||||||
|
port := p.get_int('port')!
|
||||||
|
|
||||||
|
parsed := parse_window_name(name)!
|
||||||
|
mut session := tmux_instance.session_get(parsed.session)!
|
||||||
|
mut window := session.window_get(name: parsed.window)!
|
||||||
|
window.stop_ttyd(port)!
|
||||||
|
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tmux.ttyd_stop_all actions
|
||||||
|
fn play_ttyd_stop_all(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||||
|
for mut action in plbook.find(filter: 'tmux.ttyd_stop_all')! {
|
||||||
|
if action.done {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_all_ttyd()!
|
||||||
|
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# TMUX
|
# TMUX
|
||||||
|
|
||||||
|
|
||||||
TMUX is a very capable process manager.
|
TMUX is a very capable process manager.
|
||||||
|
|
||||||
> TODO: TTYD, need to integrate with TMUX for exposing TMUX over http
|
> TODO: TTYD, need to integrate with TMUX for exposing TMUX over http
|
||||||
@@ -11,7 +10,6 @@ TMUX is a very capable process manager.
|
|||||||
- session = is a set of windows, it has a name and groups windows
|
- session = is a set of windows, it has a name and groups windows
|
||||||
- window = is typically one process running (you can have panes but in our implementation we skip this)
|
- window = is typically one process running (you can have panes but in our implementation we skip this)
|
||||||
|
|
||||||
|
|
||||||
## structure
|
## structure
|
||||||
|
|
||||||
tmux library provides functions for managing tmux sessions
|
tmux library provides functions for managing tmux sessions
|
||||||
@@ -20,33 +18,156 @@ tmux library provides functions for managing tmux sessions
|
|||||||
- then windows (is where you see the app running)
|
- then windows (is where you see the app running)
|
||||||
- then panes in windows (we don't support yet)
|
- then panes in windows (we don't support yet)
|
||||||
|
|
||||||
|
|
||||||
## to attach to a tmux session
|
## to attach to a tmux session
|
||||||
|
|
||||||
> TODO:
|
>
|
||||||
## HeroScript Usage Examples
|
## HeroScript Declarative Support
|
||||||
|
|
||||||
|
The tmux module supports declarative configuration through heroscript, allowing you to define tmux sessions, windows, and panes in a structured way.
|
||||||
|
|
||||||
|
### Running HeroScript
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hero run -p <heroscript_file>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Actions
|
||||||
|
|
||||||
|
#### Session Management
|
||||||
|
|
||||||
```heroscript
|
```heroscript
|
||||||
!!tmux.session_create
|
// Create a new session
|
||||||
|
!!tmux.session_create
|
||||||
name:'mysession'
|
name:'mysession'
|
||||||
reset:true
|
reset:true // Optional: delete existing session first
|
||||||
|
|
||||||
!!tmux.session_delete
|
// Delete a session
|
||||||
|
!!tmux.session_delete
|
||||||
name:'mysession'
|
name:'mysession'
|
||||||
|
```
|
||||||
|
|
||||||
!!tmux.window_create
|
#### Window Management
|
||||||
|
|
||||||
|
```heroscript
|
||||||
|
// Create a new window
|
||||||
|
!!tmux.window_create
|
||||||
|
name:"mysession|mywindow" // Format: session|window
|
||||||
|
cmd:'htop' // Optional: command to run
|
||||||
|
env:'VAR1=value1,VAR2=value2' // Optional: environment variables
|
||||||
|
reset:true // Optional: recreate if exists
|
||||||
|
|
||||||
|
// Delete a window
|
||||||
|
!!tmux.window_delete
|
||||||
name:"mysession|mywindow"
|
name:"mysession|mywindow"
|
||||||
cmd:'htop'
|
```
|
||||||
env:'VAR1=value1,VAR2=value2'
|
|
||||||
reset:true
|
|
||||||
|
|
||||||
!!tmux.window_delete
|
#### Pane Management
|
||||||
name:"mysession|mywindow"
|
|
||||||
|
|
||||||
!!tmux.pane_execute
|
```heroscript
|
||||||
name:"mysession|mywindow|mypane"
|
// Execute command in a pane
|
||||||
|
!!tmux.pane_execute
|
||||||
|
name:"mysession|mywindow|mypane" // Format: session|window|pane
|
||||||
cmd:'ls -la'
|
cmd:'ls -la'
|
||||||
|
|
||||||
!!tmux.pane_kill
|
// Kill a pane
|
||||||
|
!!tmux.pane_kill
|
||||||
name:"mysession|mywindow|mypane"
|
name:"mysession|mywindow|mypane"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Ttyd Management
|
||||||
|
|
||||||
|
```heroscript
|
||||||
|
// Start ttyd for session access
|
||||||
|
!!tmux.session_ttyd
|
||||||
|
name:'mysession'
|
||||||
|
port:8080
|
||||||
|
editable:true // Optional: allows write access
|
||||||
|
|
||||||
|
// Start ttyd for window access
|
||||||
|
!!tmux.window_ttyd
|
||||||
|
name:"mysession|mywindow"
|
||||||
|
port:8081
|
||||||
|
editable:false // Optional: read-only access
|
||||||
|
|
||||||
|
// Stop ttyd for session
|
||||||
|
!!tmux.session_ttyd_stop
|
||||||
|
name:'mysession'
|
||||||
|
port:8080
|
||||||
|
|
||||||
|
// Stop ttyd for window
|
||||||
|
!!tmux.window_ttyd_stop
|
||||||
|
name:"mysession|mywindow"
|
||||||
|
port:8081
|
||||||
|
|
||||||
|
// Stop all ttyd processes
|
||||||
|
!!tmux.ttyd_stop_all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Example
|
||||||
|
|
||||||
|
```heroscript
|
||||||
|
#!/usr/bin/env hero
|
||||||
|
|
||||||
|
// Create development environment
|
||||||
|
!!tmux.session_create
|
||||||
|
name:'dev'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
!!tmux.window_create
|
||||||
|
name:"dev|editor"
|
||||||
|
cmd:'vim'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
!!tmux.window_create
|
||||||
|
name:"dev|server"
|
||||||
|
cmd:'python3 -m http.server 8000'
|
||||||
|
env:'PORT=8000,DEBUG=true'
|
||||||
|
reset:true
|
||||||
|
|
||||||
|
!!tmux.pane_execute
|
||||||
|
name:"dev|editor|main"
|
||||||
|
cmd:'echo "Welcome to development!"'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming Convention
|
||||||
|
|
||||||
|
- **Sessions**: Simple names like `dev`, `monitoring`, `main`
|
||||||
|
- **Windows**: Use pipe separator: `session|window` (e.g., `dev|editor`)
|
||||||
|
- **Panes**: Use pipe separator: `session|window|pane` (e.g., `dev|editor|main`)
|
||||||
|
|
||||||
|
Names are automatically normalized using `texttools.name_fix()` for consistency.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
### Setup and Cleanup Scripts
|
||||||
|
|
||||||
|
Two example heroscripts are provided to demonstrate complete tmux environment management:
|
||||||
|
|
||||||
|
#### 1. Setup Script (`tmux_setup.heroscript`)
|
||||||
|
|
||||||
|
Creates a complete development environment with multiple sessions and windows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hero run examples/tmux/tmux_setup.heroscript
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates:
|
||||||
|
|
||||||
|
- **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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hero run examples/tmux/tmux_cleanup.heroscript
|
||||||
|
```
|
||||||
|
|
||||||
|
This removes:
|
||||||
|
|
||||||
|
- All windows from both sessions
|
||||||
|
- Both dev and monitoring sessions
|
||||||
|
- All associated panes
|
||||||
|
- All ttyd web processes (ports 8080, 8081, 7681)
|
||||||
|
|||||||
@@ -61,16 +61,42 @@ pub fn (mut s Session) scan() ! {
|
|||||||
|
|
||||||
mut current_windows := map[string]bool{}
|
mut current_windows := map[string]bool{}
|
||||||
for line in result.split_into_lines() {
|
for line in result.split_into_lines() {
|
||||||
if line.contains('|') {
|
line_trimmed := line.trim_space()
|
||||||
parts := line.split('|')
|
if line_trimmed.len == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line_trimmed.contains('|') {
|
||||||
|
parts := line_trimmed.split('|')
|
||||||
if parts.len >= 3 && parts[0].len > 0 && parts[1].len > 0 {
|
if parts.len >= 3 && parts[0].len > 0 && parts[1].len > 0 {
|
||||||
window_name := texttools.name_fix(parts[0])
|
// Safely extract window name with additional validation
|
||||||
|
raw_window_name := parts[0].trim_space()
|
||||||
|
if raw_window_name.len == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use safer name processing instead of texttools.name_fix
|
||||||
|
mut window_name := raw_window_name.to_lower().trim_space()
|
||||||
|
// Replace problematic characters with underscores
|
||||||
|
window_name = window_name.replace(' ', '_').replace('-', '_').replace('.',
|
||||||
|
'_')
|
||||||
|
|
||||||
|
// Remove any non-ASCII characters safely
|
||||||
|
mut safe_name := ''
|
||||||
|
for c in window_name {
|
||||||
|
if c.is_letter() || c.is_digit() || c == `_` {
|
||||||
|
safe_name += c.ascii_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window_name = safe_name
|
||||||
|
|
||||||
if window_name.len == 0 {
|
if window_name.len == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
window_id := parts[1].replace('@', '').int()
|
window_id := parts[1].replace('@', '').int()
|
||||||
window_active := parts[2] == '1'
|
window_active := parts[2] == '1'
|
||||||
|
|
||||||
|
// Safe map assignment
|
||||||
current_windows[window_name] = true
|
current_windows[window_name] = true
|
||||||
|
|
||||||
// Update existing window or create new one
|
// Update existing window or create new one
|
||||||
@@ -102,7 +128,24 @@ pub fn (mut s Session) scan() ! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove windows that no longer exist in tmux
|
// Remove windows that no longer exist in tmux
|
||||||
s.windows = s.windows.filter(it.name.len > 0 && current_windows[it.name] == true)
|
mut valid_windows := []&Window{}
|
||||||
|
for window in s.windows {
|
||||||
|
// Safety check: ensure window.name is valid
|
||||||
|
if window.name.len > 0 {
|
||||||
|
// Avoid map access entirely - check if window still exists by comparing with current windows
|
||||||
|
mut window_exists := false
|
||||||
|
for current_name, _ in current_windows {
|
||||||
|
if window.name == current_name {
|
||||||
|
window_exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if window_exists {
|
||||||
|
valid_windows << window
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.windows = valid_windows
|
||||||
}
|
}
|
||||||
|
|
||||||
// window_name is the name of the window in session main (will always be called session main)
|
// window_name is the name of the window in session main (will always be called session main)
|
||||||
@@ -217,7 +260,7 @@ pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window {
|
|||||||
if args.name.len == 0 {
|
if args.name.len == 0 {
|
||||||
return error('Window name cannot be empty')
|
return error('Window name cannot be empty')
|
||||||
}
|
}
|
||||||
args.name = texttools.name_fix(args.name)
|
args.name = texttools.name_fix_token(args.name)
|
||||||
for w in s.windows {
|
for w in s.windows {
|
||||||
if w.name.len > 0 && w.name == args.name {
|
if w.name.len > 0 && w.name == args.name {
|
||||||
if (args.id > 0 && w.id == args.id) || args.id == 0 {
|
if (args.id > 0 && w.id == args.id) || args.id == 0 {
|
||||||
@@ -231,7 +274,7 @@ pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window {
|
|||||||
pub fn (mut s Session) window_delete(args_ WindowGetArgs) ! {
|
pub fn (mut s Session) window_delete(args_ WindowGetArgs) ! {
|
||||||
// $if debug { console.print_debug(" - window delete: $args_")}
|
// $if debug { console.print_debug(" - window delete: $args_")}
|
||||||
mut args := args_
|
mut args := args_
|
||||||
args.name = texttools.name_fix(args.name)
|
args.name = texttools.name_fix_token(args.name)
|
||||||
if !(s.window_exist(args)) {
|
if !(s.window_exist(args)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -284,3 +327,16 @@ pub fn (mut s Session) run_ttyd(args TtydArgs) ! {
|
|||||||
pub fn (mut s Session) run_ttyd_readonly(port int) ! {
|
pub fn (mut s Session) run_ttyd_readonly(port int) ! {
|
||||||
s.run_ttyd(port: port, editable: false)!
|
s.run_ttyd(port: port, editable: false)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop ttyd for this session by killing the process on the specified port
|
||||||
|
pub fn (mut s Session) stop_ttyd(port int) ! {
|
||||||
|
// Kill any process running on the specified port
|
||||||
|
cmd := 'lsof -ti:${port} | xargs kill -9'
|
||||||
|
osal.execute_silent(cmd) or { return error("Can't stop ttyd on port ${port}: ${err}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop all ttyd processes (kills all ttyd processes system-wide)
|
||||||
|
pub fn stop_all_ttyd() ! {
|
||||||
|
cmd := 'pkill ttyd'
|
||||||
|
osal.execute_silent(cmd) or { return error("Can't stop all ttyd processes: ${err}") }
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,11 +37,27 @@ pub fn (mut w Window) scan() ! {
|
|||||||
|
|
||||||
mut current_panes := map[int]bool{}
|
mut current_panes := map[int]bool{}
|
||||||
for line in result.split_into_lines() {
|
for line in result.split_into_lines() {
|
||||||
if line.contains('|') {
|
line_trimmed := line.trim_space()
|
||||||
parts := line.split('|')
|
if line_trimmed.len == 0 {
|
||||||
if parts.len >= 3 {
|
continue
|
||||||
pane_id := parts[0].replace('%', '').int()
|
}
|
||||||
pane_pid := parts[1].int()
|
if line_trimmed.contains('|') {
|
||||||
|
parts := line_trimmed.split('|')
|
||||||
|
if parts.len >= 3 && parts[0].len > 0 && parts[1].len > 0 {
|
||||||
|
// Safely parse pane ID
|
||||||
|
pane_id_str := parts[0].replace('%', '').trim_space()
|
||||||
|
if pane_id_str.len == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pane_id := pane_id_str.int()
|
||||||
|
|
||||||
|
// Safely parse PID
|
||||||
|
pane_pid_str := parts[1].trim_space()
|
||||||
|
if pane_pid_str.len == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pane_pid := pane_pid_str.int()
|
||||||
|
|
||||||
pane_active := parts[2] == '1'
|
pane_active := parts[2] == '1'
|
||||||
pane_cmd := if parts.len > 3 { parts[3] } else { '' }
|
pane_cmd := if parts.len > 3 { parts[3] } else { '' }
|
||||||
|
|
||||||
@@ -77,7 +93,14 @@ pub fn (mut w Window) scan() ! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove panes that no longer exist
|
// Remove panes that no longer exist
|
||||||
w.panes = w.panes.filter(current_panes[it.id] == true)
|
mut valid_panes := []&Pane{}
|
||||||
|
for pane in w.panes {
|
||||||
|
// Use safe map access with 'in' operator first
|
||||||
|
if pane.id in current_panes && current_panes[pane.id] == true {
|
||||||
|
valid_panes << pane
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.panes = valid_panes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (mut w Window) stop() ! {
|
pub fn (mut w Window) stop() ! {
|
||||||
@@ -237,14 +260,8 @@ pub fn (mut w Window) pane_split(args PaneSplitArgs) !&Pane {
|
|||||||
w.panes << &new_pane
|
w.panes << &new_pane
|
||||||
w.scan()!
|
w.scan()!
|
||||||
|
|
||||||
// Return reference to the new pane
|
// Return the new pane reference
|
||||||
for mut pane in w.panes {
|
return &new_pane
|
||||||
if pane.id == pane_id {
|
|
||||||
return pane
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return error('Could not find newly created pane with ID ${pane_id}')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split pane horizontally (side by side)
|
// Split pane horizontally (side by side)
|
||||||
@@ -289,3 +306,10 @@ pub fn (mut w Window) run_ttyd(args TtydArgs) ! {
|
|||||||
pub fn (mut w Window) run_ttyd_readonly(port int) ! {
|
pub fn (mut w Window) run_ttyd_readonly(port int) ! {
|
||||||
w.run_ttyd(port: port, editable: false)!
|
w.run_ttyd(port: port, editable: false)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop ttyd for this window by killing the process on the specified port
|
||||||
|
pub fn (mut w Window) stop_ttyd(port int) ! {
|
||||||
|
// Kill any process running on the specified port
|
||||||
|
cmd := 'lsof -ti:${port} | xargs kill -9'
|
||||||
|
osal.execute_silent(cmd) or { return error("Can't stop ttyd on port ${port}: ${err}") }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user