Skip to content

osal.tmux #

TMUX

TMUX is a very capable process manager.

TODO: TTYD, need to integrate with TMUX for exposing TMUX over http

Concepts

  • tmux = is the factory, it represents the tmux process manager, linked to a node
  • 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)

structure

tmux library provides functions for managing tmux sessions

  • session is the top one
  • then windows (is where you see the app running)
  • then panes in windows (we don't support yet)

to attach to a tmux session

HeroScript Programming Paradigms

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

hero run -p <heroscript_file>

Imperative Actions (Traditional)

Session Management

// Create a new session
!!tmux.session_create
    name:'mysession'
    reset:true  // Optional: delete existing session first

// Delete a session
!!tmux.session_delete
    name:'mysession'

Window Management

// 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'

Pane Management

// Execute command in a pane
!!tmux.pane_execute
    name:'mysession|mywindow|mypane'  // Format: session|window|pane
    cmd:'ls -la'

// Kill a pane
!!tmux.pane_kill
    name:'mysession|mywindow|mypane'

Pane Splitting

// 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

// 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

Declarative Actions (State-Based)

Session Ensure

// Ensure session exists (idempotent)
!!tmux.session_ensure
    name:'mysession'

Window Ensure with Pane Layouts

// 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

// 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

// Multi-line commands are supported using proper heroscript syntax
!!tmux.pane_ensure
    name:'mysession|mywindow|2'
    label:'setup'
    cmd:'
        echo "Starting setup..."
        mkdir -p /tmp/workspace
        cd /tmp/workspace
        echo "Setup complete"
    '

Multi-line Commands

The tmux module supports multi-line commands in heroscripts using proper multi-line parameter syntax. Multi-line commands are automatically converted to temporary shell scripts for execution.

Syntax

Use the multi-line parameter format with quotes:

!!tmux.pane_ensure
    name:'session|window|pane'
    cmd:'
        command1
        command2
        command3
    '

Features

  • Automatic Script Generation: Multi-line commands are converted to temporary shell scripts
  • Sequential Execution: All commands execute in order within the same shell context
  • Error Handling: Scripts include proper bash shebang and error handling
  • Temporary Files: Scripts are stored in /tmp/tmux/{session}/pane_{id}_script.sh

Example

!!tmux.pane_ensure
    name:'dev|workspace|1'
    label:'setup'
    cmd:'
        echo "Setting up development environment..."
        mkdir -p /tmp/dev_workspace
        cd /tmp/dev_workspace
        git clone https://github.com/example/repo.git
        cd repo
        npm install
        echo "Development environment ready!"
    '

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

#!/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.

Complete Declarative Example

#!/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

Example Scripts

Several example heroscripts are provided to demonstrate both paradigms:

1. Declarative Example (declarative_example.heroscript)

Pure declarative approach showing state-based configuration:

hero run examples/tmux/declarative_example.heroscript

2. Paradigm Comparison (imperative_vs_declarative.heroscript)

Side-by-side comparison of both approaches:

hero run examples/tmux/imperative_vs_declarative.heroscript

3. Setup and Cleanup Scripts

Traditional imperative scripts for environment management:

hero run examples/tmux/tmux_setup.heroscript      ##hero run examples/tmux/tmux_cleanup.heroscript    ##

fn new #

fn new(args TmuxNewArgs) !Tmux

return tmux instance

fn normalize_and_hash_command #

fn normalize_and_hash_command(cmd string) string

Generate MD5 hash for a command (normalized)

fn play #

fn play(mut plbook PlayBook) !

fn stop_all_ttyd #

fn stop_all_ttyd() !

Stop all ttyd processes (kills all ttyd processes system-wide)

fn (Session) cleanup_stale_command_states #

fn (mut s Session) cleanup_stale_command_states() !

Clean up stale command states (for maintenance)

fn (Session) create #

fn (mut s Session) create() !

fn (Session) get_all_command_states #

fn (mut s Session) get_all_command_states() !map[string]CommandState

Get all command states for a session (useful for debugging/monitoring)

fn (Session) kill_all_processes #

fn (mut s Session) kill_all_processes() !

Kill all processes in all windows and panes of this session

fn (Session) restart #

fn (mut s Session) restart() !

fn (Session) run_ttyd #

fn (mut s Session) run_ttyd(args TtydArgs) !

Run ttyd for this session so it can be accessed in the browser

fn (Session) run_ttyd_readonly #

fn (mut s Session) run_ttyd_readonly(port int) !

Backward compatibility method - runs ttyd in read-only mode

fn (Session) scan #

fn (mut s Session) scan() !

load info from reality

fn (Session) stats #

fn (mut s Session) stats() !ProcessStats

fn (Session) stop #

fn (mut s Session) stop() !

fn (Session) stop_ttyd #

fn (mut s Session) stop_ttyd(port int) !

Stop ttyd for this session by killing the process on the specified port

fn (Session) str #

fn (mut s Session) str() string

fn (Session) window_delete #

fn (mut s Session) window_delete(args_ WindowGetArgs) !

fn (Session) window_get #

fn (mut s Session) window_get(args_ WindowGetArgs) !&Window

fn (Session) window_list #

fn (mut s Session) window_list() []&Window

List windows in a session

fn (Session) window_names #

fn (mut s Session) window_names() []string

fn (Session) window_new #

fn (mut s Session) window_new(args WindowArgs) !&Window

window_name is the name of the window in session main (will always be called session main) cmd to execute e.g. bash file environment arguments to use reset, if reset it will create window even if it does already exist, will destroy it

struct WindowArgs {
pub mut:
    name    string
    cmd		string
    env		map[string]string	
    reset	bool
}

fn (Session) windows_get #

fn (mut s Session) windows_get() []&Window

get all windows as found in a session

fn (Window) scan #

fn (mut w Window) scan() !

fn (Window) stop #

fn (mut w Window) stop() !

fn (Window) create #

fn (mut w Window) create(cmd_ string) !

helper function TODO env variables are not inserted in pane

fn (Window) kill #

fn (mut w Window) kill() !

stop the window with comprehensive process cleanup

fn (Window) kill_all_processes #

fn (mut w Window) kill_all_processes() !

Kill all processes in all panes of this window

fn (Window) str #

fn (window Window) str() string

fn (Window) stats #

fn (mut w Window) stats() !ProcessStats

fn (Window) pane_list #

fn (mut w Window) pane_list() []&Pane

List panes in a window

fn (Window) pane_active #

fn (mut w Window) pane_active() ?&Pane

Get active pane in window

fn (Window) pane_split #

fn (mut w Window) pane_split(args PaneSplitArgs) !&Pane

Split the active pane horizontally or vertically

fn (Window) pane_split_horizontal #

fn (mut w Window) pane_split_horizontal(cmd string) !&Pane

Split pane horizontally (side by side)

fn (Window) pane_split_vertical #

fn (mut w Window) pane_split_vertical(cmd string) !&Pane

Split pane vertically (top and bottom)

fn (Window) resize_panes_equal #

fn (mut w Window) resize_panes_equal() !

Resize panes to equal dimensions dynamically based on pane count

fn (Window) run_ttyd #

fn (mut w Window) run_ttyd(args TtydArgs) !

Run ttyd for this window so it can be accessed in the browser

fn (Window) run_ttyd_readonly #

fn (mut w Window) run_ttyd_readonly(port int) !

Backward compatibility method - runs ttyd in read-only mode

fn (Window) stop_ttyd #

fn (mut w Window) stop_ttyd(port int) !

Stop ttyd for this window by killing the process on the specified port

fn (Window) pane_get #

fn (mut w Window) pane_get(id int) !&Pane

Get a pane by its ID

fn (Window) pane_new #

fn (mut w Window) pane_new() !&Pane

Create a new pane (just a split with default shell)

struct CommandState #

struct CommandState {
pub mut:
	cmd_md5    string // MD5 hash of the command
	cmd_text   string // Original command text
	status     string // running|finished|failed|unknown
	pid        int    // Process ID of the command
	started_at string // Timestamp when command started
	last_check string // Last time status was checked
	pane_id    int    // Pane ID for reference
}

Command state structure for Redis storage

struct LogsGetArgs #

struct LogsGetArgs {
pub mut:
	reset bool
}

struct Pane #

@[heap]
struct Pane {
pub mut:
	window             &Window @[str: skip]
	id                 int    // pane id (e.g., %1, %2)
	pid                int    // process id
	active             bool   // is this the active pane
	cmd                string // command running in pane
	env                map[string]string
	created_at         time.Time
	last_output_offset int // for tracking new logs
	// Logging fields
	log_enabled bool   // whether logging is enabled for this pane
	log_path    string // path where logs are stored
	logger_pid  int    // process id of the logger process
}

fn (Pane) clear #

fn (mut p Pane) clear() !

fn (Pane) clear_command_state #

fn (mut p Pane) clear_command_state() !

Clear command state from Redis (when pane is reset or command is removed)

fn (Pane) ensure_bash_parent #

fn (mut p Pane) ensure_bash_parent() !

Ensure bash is the first process in the pane

fn (Pane) exit_status #

fn (mut p Pane) exit_status() !ProcessStatus

fn (Pane) get_child_processes #

fn (mut p Pane) get_child_processes() ![]osal.ProcessInfo

Get all child processes of this pane's main process

fn (Pane) get_command_state #

fn (mut p Pane) get_command_state() ?CommandState

Retrieve command state from Redis

fn (Pane) get_height #

fn (p Pane) get_height() !int

Get current pane height

fn (Pane) get_state_key #

fn (p &Pane) get_state_key() string

Generate Redis key for command state tracking Pattern: herotmux:${session}:${window}|${pane}

fn (Pane) get_width #

fn (p Pane) get_width() !int

Get current pane width

fn (Pane) has_command_changed #

fn (mut p Pane) has_command_changed(new_cmd string) bool

Check if command has changed by comparing MD5 hashes

fn (Pane) is_at_clean_prompt #

fn (mut p Pane) is_at_clean_prompt() !bool

Check if pane is at a clean shell prompt

fn (Pane) is_pane_empty #

fn (mut p Pane) is_pane_empty() !bool

Check if pane is completely empty

fn (Pane) is_stored_command_running #

fn (mut p Pane) is_stored_command_running() bool

Check if stored command is currently running by verifying the PID

fn (Pane) kill #

fn (mut p Pane) kill() !

Kill this specific pane with comprehensive process cleanup

fn (Pane) kill_processes #

fn (mut p Pane) kill_processes() !

Kill all processes associated with this pane (main process and all children)

fn (Pane) kill_running_command #

fn (mut p Pane) kill_running_command() !

Kill the currently running command in this pane

fn (Pane) logging_disable #

fn (mut p Pane) logging_disable() !

Disable logging for this pane

fn (Pane) logging_enable #

fn (mut p Pane) logging_enable(args PaneLoggingEnableArgs) !

Enable logging for this pane

fn (Pane) logging_status #

fn (p Pane) logging_status() string

Get logging status for this pane

fn (Pane) logs_all #

fn (mut p Pane) logs_all() !string

fn (Pane) logs_get_new #

fn (mut p Pane) logs_get_new(args LogsGetArgs) ![]TMuxLogEntry

get new logs since last call

fn (Pane) output_wait #

fn (mut p Pane) output_wait(c_ string, timeoutsec int) !

Fix the output_wait method to use correct method name

fn (Pane) processinfo #

fn (mut p Pane) processinfo() !osal.ProcessMap

Get process information for this pane and all its children

fn (Pane) processinfo_main #

fn (mut p Pane) processinfo_main() !osal.ProcessInfo

Get process information for just this pane's main process

fn (Pane) reset_if_needed #

fn (mut p Pane) reset_if_needed() !

Reset pane if it appears empty or needs cleanup

fn (Pane) resize #

fn (mut p Pane) resize(args PaneResizeArgs) !

Resize this pane

fn (Pane) resize_down #

fn (mut p Pane) resize_down(cells int) !

fn (Pane) resize_left #

fn (mut p Pane) resize_left(cells int) !

fn (Pane) resize_right #

fn (mut p Pane) resize_right(cells int) !

fn (Pane) resize_up #

fn (mut p Pane) resize_up(cells int) !

Convenience methods for resizing

fn (Pane) select #

fn (mut p Pane) select() !

Select/activate this pane

fn (Pane) send_command #

fn (mut p Pane) send_command(command string) !

Send a command to this pane Supports both single-line and multi-line commands

fn (Pane) send_command_declarative #

fn (mut p Pane) send_command_declarative(command string) !

Send command with declarative mode logic (intelligent state management) This method implements the full declarative logic:1. Check if pane has previous command (Redis lookup)2. If previous command exists:a. Check if still running (process verification) b. Compare MD5 hashes c. If different command OR not running: proceed d. If same command AND running: skip3. If proceeding: kill existing processes, then start new command

fn (Pane) send_keys #

fn (mut p Pane) send_keys(keys string) !

Send raw keys to this pane (without Enter)

fn (Pane) send_reset #

fn (mut p Pane) send_reset() !

Send reset command to pane

fn (Pane) stats #

fn (mut p Pane) stats() !ProcessStats

fn (Pane) store_command_state #

fn (mut p Pane) store_command_state(cmd string, status string, pid int) !

Store command state in Redis

fn (Pane) update_command_status #

fn (mut p Pane) update_command_status(status string) !

Update command status in Redis

fn (Pane) verify_bash_parent #

fn (mut p Pane) verify_bash_parent() !bool

Verify that bash is the first process in this pane

fn (Pane) verify_command_hierarchy #

fn (mut p Pane) verify_command_hierarchy() !bool

Check if commands are running as children of bash

struct PaneLoggingEnableArgs #

@[params]
struct PaneLoggingEnableArgs {
pub mut:
	logpath  string // custom log path, if empty uses default
	logreset bool   // whether to reset/clear existing logs
}

struct PaneNewArgs #

@[params]
struct PaneNewArgs {
pub mut:
	name  string
	reset bool // means we reset the pane if it already exists
	cmd   string
	env   map[string]string
}

struct PaneResizeArgs #

@[params]
struct PaneResizeArgs {
pub mut:
	direction string = 'right' // 'up', 'down', 'left', 'right'
	cells     int    = 5       // number of cells to resize by
}

struct PaneSplitArgs #

@[params]
struct PaneSplitArgs {
pub mut:
	cmd        string            // command to run in new pane
	horizontal bool              // true for horizontal split, false for vertical
	env        map[string]string // environment variables
	// Logging parameters
	log      bool   // enable logging for this pane
	logreset bool   // reset/clear existing logs when enabling
	logpath  string // custom log path, if empty uses default
}

struct ProcessStats #

struct ProcessStats {
pub mut:
	cpu_percent    f64
	memory_bytes   u64
	memory_percent f64
}

struct SessionCreateArgs #

@[params]
struct SessionCreateArgs {
pub mut:
	name  string @[required]
	reset bool
}

struct TMuxLogEntry #

struct TMuxLogEntry {
pub mut:
	content   string
	timestamp time.Time
	offset    int
}

struct Tmux #

@[heap]
struct Tmux {
pub mut:
	sessions  []&Session
	sessionid string // unique link to job
	redis     &redisclient.Redis @[skip] // Redis client for command state tracking
}

fn (Tmux) is_running #

fn (mut t Tmux) is_running() !bool

checks whether tmux server is running

fn (Tmux) list_print #

fn (mut t Tmux) list_print()

print list of tmux sessions

fn (Tmux) scan #

fn (mut t Tmux) scan() !

scan the system to detect sessions . TODO needs to be done differently, here only find the sessions, then per session call the scan() which will find the windows, call scan() there as well ...

fn (Tmux) session_create #

fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session

create session, if reset will re-create

fn (Tmux) session_delete #

fn (mut t Tmux) session_delete(name_ string) !

fn (Tmux) session_exist #

fn (mut t Tmux) session_exist(name_ string) bool

fn (Tmux) session_get #

fn (mut t Tmux) session_get(name_ string) !&Session

get session (session has windows) . returns none if not found

fn (Tmux) start #

fn (mut t Tmux) start() !

fn (Tmux) stop #

fn (mut t Tmux) stop() !

fn (Tmux) str #

fn (mut t Tmux) str() string

fn (Tmux) window_new #

fn (mut t Tmux) window_new(args WindowNewArgs) !&Window

fn (Tmux) windows_get #

fn (mut t Tmux) windows_get() []&Window

get all windows as found in all sessions

struct TmuxNewArgs #

@[params]
struct TmuxNewArgs {
pub:
	sessionid string
}

struct TtydArgs #

@[params]
struct TtydArgs {
pub mut:
	port     int
	editable bool // if true, allows write access to the terminal
}

struct WindowArgs #

@[params]
struct WindowArgs {
pub mut:
	name  string
	cmd   string
	env   map[string]string
	reset bool
}

struct WindowGetArgs #

@[params]
struct WindowGetArgs {
pub mut:
	name string
	id   int
}

struct WindowNewArgs #

@[params]
struct WindowNewArgs {
pub mut:
	session_name string = 'main'
	name         string
	cmd          string
	env          map[string]string
	reset        bool
}