module tmux import incubaid.herolib.osal.core as osal import incubaid.herolib.core.texttools import os import incubaid.herolib.ui.console @[heap] struct Session { pub mut: tmux &Tmux @[str: skip] // reference back windows []&Window // session has windows name string } @[params] pub struct WindowArgs { pub mut: name string cmd string env map[string]string reset bool } @[params] pub struct WindowGetArgs { pub mut: name string id int } pub fn (mut s Session) create() ! { // Check if session already exists cmd_check := 'tmux has-session -t ${s.name}' check_result := osal.exec(cmd: cmd_check, stdout: false, ignore_error: true) or { // Session doesn't exist, this is expected osal.Job{} } if check_result.exit_code == 0 { return error('duplicate session: ${s.name}') } // Create new session cmd := 'tmux new-session -d -s ${s.name}' osal.exec(cmd: cmd, stdout: false, name: 'tmux_session_create') or { return error("Can't create session ${s.name}: ${err}") } } // load info from reality pub fn (mut s Session) scan() ! { // Get current windows from tmux for this session cmd := "tmux list-windows -t ${s.name} -F '#{window_name}|#{window_id}|#{window_active}'" result := osal.execute_silent(cmd) or { if err.msg().contains('session not found') { return } return error('Cannot list windows for session ${s.name}: ${err}') } mut current_windows := map[string]bool{} for line in result.split_into_lines() { line_trimmed := line.trim_space() 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 { // 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 { continue } window_id := parts[1].replace('@', '').int() window_active := parts[2] == '1' // Safe map assignment current_windows[window_name] = true // Update existing window or create new one mut found := false for mut w in s.windows { if w.name.len > 0 && window_name.len > 0 && w.name == window_name { w.id = window_id w.active = window_active w.scan()! // Scan panes for this window found = true break } } if !found { mut new_window := Window{ session: &s name: window_name id: window_id active: window_active panes: []&Pane{} env: map[string]string{} } new_window.scan()! // Scan panes for new window s.windows << &new_window } } } } // Remove windows that no longer exist in tmux 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) // 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 // } // ``` pub fn (mut s Session) window_new(args WindowArgs) !&Window { $if debug { console.print_header(' start window: \n${args}') } namel := texttools.name_fix(args.name) if s.window_exist(name: namel) { if args.reset { s.window_delete(name: namel)! } else { return error('cannot create new window it already exists, window ${namel} in session:${s.name}') } } mut w := &Window{ session: &s name: namel panes: []&Pane{} env: args.env } s.windows << &w // Create the window with the specified command w.create(args.cmd)! s.scan()! return w } // get all windows as found in a session pub fn (mut s Session) windows_get() []&Window { mut res := []&Window{} // os.log('TMUX - Start listing ....') for _, window in s.windows { res << window } return res } // List windows in a session pub fn (mut s Session) window_list() []&Window { return s.windows } pub fn (mut s Session) window_names() []string { mut res := []string{} for _, window in s.windows { res << window.name } return res } pub fn (mut s Session) str() string { mut out := '## Session: ${s.name}\n\n' for _, w in s.windows { out += '${*w}\n' } return out } pub fn (mut s Session) stats() !ProcessStats { mut total := ProcessStats{} for mut window in s.windows { stats := window.stats() or { continue } total.cpu_percent += stats.cpu_percent total.memory_bytes += stats.memory_bytes total.memory_percent += stats.memory_percent } return total } // pub fn (mut s Session) activate()! { // active_session := s.tmux.redis.get('tmux:active_session') or { 'No active session found' } // if active_session != 'No active session found' && s.name != active_session { // s.tmuxexecutor.db.exec('tmux attach-session -t $active_session') or { // return error('Fail to attach to current active session: $active_session \n$err') // } // s.tmuxexecutor.db.exec('tmux switch -t $s.name') or { // return error("Can't switch to session $s.name \n$err") // } // s.tmux.redis.set('tmux:active_session', s.name) or { panic('Failed to set tmux:active_session') } // os.log('SESSION - Session: $s.name activated ') // } else if active_session == 'No active session found' { // s.tmux.redis.set('tmux:active_session', s.name) or { panic('Failed to set tmux:active_session') } // os.log('SESSION - Session: $s.name activated ') // } else { // os.log('SESSION - Session: $s.name already activate ') // } // } fn (mut s Session) window_exist(args_ WindowGetArgs) bool { mut args := args_ s.window_get(args) or { return false } return true } pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window { mut args := args_ if args.name.len == 0 { return error('Window name cannot be empty') } args.name = texttools.name_fix_token(args.name) for w in s.windows { if w.name.len > 0 && w.name == args.name { if (args.id > 0 && w.id == args.id) || args.id == 0 { return w } } } return error('Cannot find window ${args.name} in session:${s.name}') } pub fn (mut s Session) window_delete(args_ WindowGetArgs) ! { // $if debug { console.print_debug(" - window delete: $args_")} mut args := args_ args.name = texttools.name_fix_token(args.name) if !(s.window_exist(args)) { return } mut i := 0 for mut w in s.windows { if w.name == args.name { if (args.id > 0 && w.id == args.id) || args.id == 0 { // Enhanced cleanup: kill all processes before stopping the window w.kill_all_processes() or { console.print_debug('Failed to kill processes in window ${w.name}: ${err}') } w.stop()! break } } i += 1 } s.windows.delete(i) // i is now the one in the list which needs to be removed } pub fn (mut s Session) restart() ! { s.stop()! s.create()! } pub fn (mut s Session) stop() ! { // First, kill all processes in all windows and panes of this session s.kill_all_processes()! // Then kill the tmux session itself osal.execute_silent('tmux kill-session -t ${s.name}') or { return } } // Kill all processes in all windows and panes of this session pub fn (mut s Session) kill_all_processes() ! { console.print_debug('Killing all processes in session ${s.name}') // Refresh session information to get current state s.scan()! // Kill processes in each window for mut window in s.windows { window.kill_all_processes() or { console.print_debug('Failed to kill processes in window ${window.name}: ${err}') // Continue with other windows even if one fails } } } // Run ttyd for this session so it can be accessed in the browser pub fn (mut s Session) run_ttyd(args TtydArgs) ! { // Check if the port is available before starting ttyd osal.port_check_available(args.port) or { return error('Cannot start ttyd for session ${s.name}: ${err}') } target := '${s.name}' // Add -W flag for write access if editable mode is enabled mut ttyd_flags := '-p ${args.port}' if args.editable { ttyd_flags += ' -W' } cmd := 'nohup ttyd ${ttyd_flags} tmux attach -t ${target} >/dev/null 2>&1 &' code := os.system(cmd) if code != 0 { return error('Failed to start ttyd on port ${args.port} for session ${s.name}') } mode_str := if args.editable { 'editable' } else { 'read-only' } println('ttyd started for session ${s.name} at http://localhost:${args.port} (${mode_str} mode)') } // Backward compatibility method - runs ttyd in read-only mode pub fn (mut s Session) run_ttyd_readonly(port int) ! { 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 { // Ignore error if no process is found on the port // This is normal when no ttyd is running on that port } println('ttyd stopped for session ${s.name} on port ${port} (if it was running)') } // Stop all ttyd processes (kills all ttyd processes system-wide) pub fn stop_all_ttyd() ! { cmd := 'pkill ttyd' osal.execute_silent(cmd) or { // Ignore error if no ttyd processes are found (exit code 1) // This is normal when no ttyd processes are running } println('All ttyd processes stopped (if any were running)') }