From 117c9ac67c1c5a249cc2ab83ed448bc4cc90fbd5 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 24 Aug 2025 16:31:04 +0300 Subject: [PATCH 01/10] refactor: Improve tmux API consistency and formatting - Refactor `logs_get_new` to use `LogsGetArgs` struct - Return window as reference from `window_new` - Standardize indentation and spacing - Remove excessive blank lines - Comment out initial example usage --- examples/osal/tmux.vsh | 24 ++--- lib/osal/tmux/tmux.v | 14 +-- lib/osal/tmux/tmux_pane.v | 203 +++++++++++++++++------------------ lib/osal/tmux/tmux_session.v | 172 ++++++++++++++--------------- 4 files changed, 199 insertions(+), 214 deletions(-) diff --git a/examples/osal/tmux.vsh b/examples/osal/tmux.vsh index dba1fc32..d7bddd7a 100755 --- a/examples/osal/tmux.vsh +++ b/examples/osal/tmux.vsh @@ -1,20 +1,20 @@ #!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run - import freeflowuniverse.herolib.osal.tmux mut t := tmux.new()! -if !t.is_running()! { - t.start()! -} -if t.session_exist('main') { - t.session_delete('main')! -} -// Create session first, then create window -mut session := t.session_create(name: 'main')! -session.window_new(name: 'test', cmd: 'mc', reset: true)! -// Or use the convenience method -// t.window_new(session_name: 'main', name: 'test', cmd: 'mc', reset: true)! +// if !t.is_running()! { +// t.start()! +// } +// if t.session_exist('main') { +// t.session_delete('main')! +// } +// // Create session first, then create window +// mut session := t.session_create(name: 'main')! +// session.window_new(name: 'test', cmd: 'mc', reset: true)! + +// // Or use the convenience method +// // t.window_new(session_name: 'main', name: 'test', cmd: 'mc', reset: true)! println(t) diff --git a/lib/osal/tmux/tmux.v b/lib/osal/tmux/tmux.v index 1d9b09d3..5060bf27 100644 --- a/lib/osal/tmux/tmux.v +++ b/lib/osal/tmux/tmux.v @@ -14,7 +14,6 @@ pub mut: sessionid string // unique link to job } - // get session (session has windows) . // returns none if not found pub fn (mut t Tmux) session_get(name_ string) !&Session { @@ -56,8 +55,6 @@ pub mut: reset bool } - - // create session, if reset will re-create pub fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session { name := texttools.name_fix(args.name) @@ -83,7 +80,6 @@ pub fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session { return s } - @[params] pub struct TmuxNewArgs { sessionid string @@ -116,17 +112,16 @@ pub fn (mut t Tmux) window_new(args WindowNewArgs) !&Window { } else { t.session_create(name: args.session_name)! } - + // Create window in session return session.window_new( - name: args.name - cmd: args.cmd - env: args.env + name: args.name + cmd: args.cmd + env: args.env reset: args.reset )! } - pub fn (mut t Tmux) stop() ! { $if debug { console.print_debug('Stopping tmux...') @@ -156,7 +151,6 @@ pub fn (mut t Tmux) start() ! { t.scan()! } - // print list of tmux sessions pub fn (mut t Tmux) list_print() { // os.log('TMUX - Start listing ....') diff --git a/lib/osal/tmux/tmux_pane.v b/lib/osal/tmux/tmux_pane.v index 1b786cc4..ade2f3d1 100644 --- a/lib/osal/tmux/tmux_pane.v +++ b/lib/osal/tmux/tmux_pane.v @@ -10,141 +10,140 @@ import freeflowuniverse.herolib.ui.console @[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 + 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 } - pub fn (mut p Pane) stats() !ProcessStats { - if p.pid == 0 { - return ProcessStats{} - } + if p.pid == 0 { + return ProcessStats{} + } - // Use ps command to get CPU and memory stats - cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss --no-headers' - result := osal.execute_silent(cmd) or { - return error('Cannot get stats for PID ${p.pid}: ${err}') - } + // Use ps command to get CPU and memory stats + cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss --no-headers' + result := osal.execute_silent(cmd) or { + return error('Cannot get stats for PID ${p.pid}: ${err}') + } - if result.trim_space() == '' { - return error('Process ${p.pid} not found') - } + if result.trim_space() == '' { + return error('Process ${p.pid} not found') + } - parts := result.trim_space().split_any(' \t').filter(it != '') - if parts.len < 3 { - return error('Invalid ps output: ${result}') - } + parts := result.trim_space().split_any(' \t').filter(it != '') + if parts.len < 3 { + return error('Invalid ps output: ${result}') + } - return ProcessStats{ - cpu_percent: parts[0].f64() - memory_percent: parts[1].f64() - memory_bytes: parts[2].u64() * 1024 // ps returns KB, convert to bytes - } + return ProcessStats{ + cpu_percent: parts[0].f64() + memory_percent: parts[1].f64() + memory_bytes: parts[2].u64() * 1024 // ps returns KB, convert to bytes + } } - pub struct TMuxLogEntry { pub mut: - content string - timestamp time.Time - offset int + content string + timestamp time.Time + offset int } -pub fn (mut p Pane) logs_get_new(reset bool) ![]TMuxLogEntry { +pub struct LogsGetArgs { +pub mut: + reset bool +} - if reset{ - p.last_output_offset = 0 - } - // Capture pane content with line numbers - cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S ${p.last_output_offset} -p' - result := osal.execute_silent(cmd) or { - return error('Cannot capture pane output: ${err}') - } +// get new logs since last call +pub fn (mut p Pane) logs_get_new(args LogsGetArgs) ![]TMuxLogEntry { + if args.reset { + p.last_output_offset = 0 + } + // Capture pane content with line numbers + cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S ${p.last_output_offset} -p' + result := osal.execute_silent(cmd) or { return error('Cannot capture pane output: ${err}') } - lines := result.split_into_lines() - mut entries := []TMuxLogEntry{} + lines := result.split_into_lines() + mut entries := []TMuxLogEntry{} - mut i:= 0 - for line in lines { - if line.trim_space() != '' { - entries << TMuxLogEntry{ - content: line - timestamp: time.now() - offset: p.last_output_offset + i + 1 - } - } - } - // Update offset to avoid duplicates next time - if entries.len > 0 { - p.last_output_offset = entries.last().offset - } - return entries + mut i := 0 + for line in lines { + if line.trim_space() != '' { + entries << TMuxLogEntry{ + content: line + timestamp: time.now() + offset: p.last_output_offset + i + 1 + } + } + } + // Update offset to avoid duplicates next time + if entries.len > 0 { + p.last_output_offset = entries.last().offset + } + return entries } pub fn (mut p Pane) exit_status() !ProcessStatus { - // Get the last few lines to see if there's an exit status - logs := p.logs_all()! - lines := logs.split_into_lines() + // Get the last few lines to see if there's an exit status + logs := p.logs_all()! + lines := logs.split_into_lines() - // Look for shell prompt indicating command finished - for line in lines.reverse() { - line_clean := line.trim_space() - if line_clean.contains('$') || line_clean.contains('#') || line_clean.contains('>') { - // Found shell prompt, command likely finished - // Could also check for specific exit codes in history - return .finished_ok - } - } - return .finished_error + // Look for shell prompt indicating command finished + for line in lines.reverse() { + line_clean := line.trim_space() + if line_clean.contains('$') || line_clean.contains('#') || line_clean.contains('>') { + // Found shell prompt, command likely finished + // Could also check for specific exit codes in history + return .finished_ok + } + } + return .finished_error } pub fn (mut p Pane) logs_all() !string { - cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S -2000 -p' - return osal.execute_silent(cmd) or { - error('Cannot capture pane output: ${err}') - } + cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S -2000 -p' + return osal.execute_silent(cmd) or { error('Cannot capture pane output: ${err}') } } // Fix the output_wait method to use correct method name pub fn (mut p Pane) output_wait(c_ string, timeoutsec int) ! { - mut t := ourtime.now() - start := t.unix() - c := c_.replace('\n', '') - for i in 0 .. 2000 { - entries := p.logs_get_new(reset: false)! - for entry in entries { - if entry.content.replace('\n', '').contains(c) { - return - } - } - mut t2 := ourtime.now() - if t2.unix() > start + timeoutsec { - return error('timeout on output wait for tmux.\n${p} .\nwaiting for:\n${c}') - } - time.sleep(100 * time.millisecond) - } + mut t := ourtime.now() + start := t.unix() + c := c_.replace('\n', '') + for i in 0 .. 2000 { + entries := p.logs_get_new(reset: false)! + for entry in entries { + if entry.content.replace('\n', '').contains(c) { + return + } + } + mut t2 := ourtime.now() + if t2.unix() > start + timeoutsec { + return error('timeout on output wait for tmux.\n${p} .\nwaiting for:\n${c}') + } + time.sleep(100 * time.millisecond) + } } // Get process information for this pane and all its children pub fn (mut p Pane) processinfo() !osal.ProcessMap { - if p.pid == 0 { - return error('Pane has no associated process (pid is 0)') - } - - return osal.processinfo_with_children(p.pid)! + if p.pid == 0 { + return error('Pane has no associated process (pid is 0)') + } + + return osal.processinfo_with_children(p.pid)! } // Get process information for just this pane's main process pub fn (mut p Pane) processinfo_main() !osal.ProcessInfo { - if p.pid == 0 { - return error('Pane has no associated process (pid is 0)') - } - - return osal.processinfo_get(p.pid)! + if p.pid == 0 { + return error('Pane has no associated process (pid is 0)') + } + + return osal.processinfo_get(p.pid)! } diff --git a/lib/osal/tmux/tmux_session.v b/lib/osal/tmux/tmux_session.v index 76d73f73..9c4eb9f8 100644 --- a/lib/osal/tmux/tmux_session.v +++ b/lib/osal/tmux/tmux_session.v @@ -21,87 +21,86 @@ pub mut: env map[string]string reset bool } + @[params] pub struct WindowGetArgs { pub mut: - name string - id int + 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}") - } + // 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 +// 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 // Session doesn't exist anymore - } - return error('Cannot list windows for session ${s.name}: ${err}') - } - - mut current_windows := map[string]bool{} - for line in result.split_into_lines() { - if line.contains('|') { - parts := line.split('|') - if parts.len >= 2 { - window_name := texttools.name_fix(parts[0]) - window_id := parts[1].replace('@', '').int() - window_active := parts[2] == '1' - - current_windows[window_name] = true - - // Update existing window or create new one - mut found := false - for mut w in s.windows { - if 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 - s.windows = s.windows.filter(current_windows[it.name] == true) -} + // 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() { + if line.contains('|') { + parts := line.split('|') + if parts.len >= 2 { + window_name := texttools.name_fix(parts[0]) + window_id := parts[1].replace('@', '').int() + window_active := parts[2] == '1' + + current_windows[window_name] = true + + // Update existing window or create new one + mut found := false + for mut w in s.windows { + if 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 + s.windows = s.windows.filter(current_windows[it.name] == true) +} // window_name is the name of the window in session main (will always be called session main) // cmd to execute e.g. bash file @@ -116,7 +115,7 @@ pub fn (mut s Session) scan() ! { // reset bool // } // ``` -pub fn (mut s Session) window_new(args WindowArgs) !Window { +pub fn (mut s Session) window_new(args WindowArgs) !&Window { $if debug { console.print_header(' start window: \n${args}') } @@ -128,7 +127,7 @@ pub fn (mut s Session) window_new(args WindowArgs) !Window { return error('cannot create new window it already exists, window ${namel} in session:${s.name}') } } - mut w := Window{ + mut w := &Window{ session: &s name: namel panes: []&Pane{} @@ -139,14 +138,10 @@ pub fn (mut s Session) window_new(args WindowArgs) !Window { // 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{} @@ -179,14 +174,14 @@ pub fn (mut s Session) str() string { } 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 + 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()! { @@ -208,8 +203,6 @@ pub fn (mut s Session) stats() !ProcessStats { // } // } - - fn (mut s Session) window_exist(args_ WindowGetArgs) bool { mut args := args_ s.window_get(args) or { return false } @@ -249,7 +242,6 @@ pub fn (mut s Session) window_delete(args_ WindowGetArgs) ! { 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()! @@ -259,4 +251,4 @@ pub fn (mut s Session) stop() ! { osal.execute_silent('tmux kill-session -t ${s.name}') or { return error("Can't delete session ${s.name} - This may happen when session is not found: ${err}") } -} \ No newline at end of file +} From b26893bf4576647674d8491c02cf274a81a1274e Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 24 Aug 2025 17:58:09 +0300 Subject: [PATCH 02/10] wip: pushing the code to sync in other branch --- examples/osal/tmux.vsh | 20 -- examples/tmux/tmux.vsh | 122 +++++++++++++ examples/tmux/tmux_pane_resize.vsh | 143 +++++++++++++++ examples/tmux/tmux_panes.vsh | 170 +++++++++++++++++ lib/osal/tmux/tmux.v | 20 +- lib/osal/tmux/tmux_pane.v | 179 +++++++++++++++++- lib/osal/tmux/tmux_scan.v | 128 +++++++------ lib/osal/tmux/tmux_session.v | 14 +- lib/osal/tmux/tmux_window.v | 284 ++++++++++++++++++----------- 9 files changed, 892 insertions(+), 188 deletions(-) delete mode 100755 examples/osal/tmux.vsh create mode 100755 examples/tmux/tmux.vsh create mode 100755 examples/tmux/tmux_pane_resize.vsh create mode 100755 examples/tmux/tmux_panes.vsh diff --git a/examples/osal/tmux.vsh b/examples/osal/tmux.vsh deleted file mode 100755 index d7bddd7a..00000000 --- a/examples/osal/tmux.vsh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run - -import freeflowuniverse.herolib.osal.tmux - -mut t := tmux.new()! - -// if !t.is_running()! { -// t.start()! -// } -// if t.session_exist('main') { -// t.session_delete('main')! -// } -// // Create session first, then create window -// mut session := t.session_create(name: 'main')! -// session.window_new(name: 'test', cmd: 'mc', reset: true)! - -// // Or use the convenience method -// // t.window_new(session_name: 'main', name: 'test', cmd: 'mc', reset: true)! - -println(t) diff --git a/examples/tmux/tmux.vsh b/examples/tmux/tmux.vsh new file mode 100755 index 00000000..89f2cbe4 --- /dev/null +++ b/examples/tmux/tmux.vsh @@ -0,0 +1,122 @@ +#!/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 + cpu_precision = 1 + 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}') +} diff --git a/examples/tmux/tmux_pane_resize.vsh b/examples/tmux/tmux_pane_resize.vsh new file mode 100755 index 00000000..aa738d9c --- /dev/null +++ b/examples/tmux/tmux_pane_resize.vsh @@ -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.') diff --git a/examples/tmux/tmux_panes.vsh b/examples/tmux/tmux_panes.vsh new file mode 100755 index 00000000..31d09225 --- /dev/null +++ b/examples/tmux/tmux_panes.vsh @@ -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') diff --git a/lib/osal/tmux/tmux.v b/lib/osal/tmux/tmux.v index 5060bf27..b63b5a35 100644 --- a/lib/osal/tmux/tmux.v +++ b/lib/osal/tmux/tmux.v @@ -7,6 +7,24 @@ import os import time import freeflowuniverse.herolib.ui.console +// Check if error message indicates tmux server is not running +fn is_tmux_server_not_running_error(error_msg string) bool { + // Common tmux server not running error patterns + tmux_not_running_patterns := [ + 'no server running', + 'error connecting to', + 'No such file or directory', // when socket doesn't exist + ] + + error_lower := error_msg.to_lower() + for pattern in tmux_not_running_patterns { + if error_lower.contains(pattern.to_lower()) { + return true + } + } + return false +} + @[heap] pub struct Tmux { pub mut: @@ -177,7 +195,7 @@ pub fn (mut t Tmux) windows_get() []&Window { pub fn (mut t Tmux) is_running() !bool { res := os.execute('tmux info') if res.exit_code != 0 { - if res.output.contains('no server running') { + if is_tmux_server_not_running_error(res.output) { // console.print_debug(" TMUX NOT RUNNING") return false } diff --git a/lib/osal/tmux/tmux_pane.v b/lib/osal/tmux/tmux_pane.v index ade2f3d1..b86c7ec2 100644 --- a/lib/osal/tmux/tmux_pane.v +++ b/lib/osal/tmux/tmux_pane.v @@ -7,6 +7,83 @@ import time import os import freeflowuniverse.herolib.ui.console +// Constants for memory calculations +const kb_to_bytes_factor = 1024 +const memory_display_precision = 3 +const memory_cache_ttl_seconds = 300 // Cache system memory for 5 minutes + +// Global cache for system memory to avoid repeated syscalls +struct MemoryCache { +mut: + total_bytes u64 + cached_at time.Time +} + +__global ( + memory_cache MemoryCache +) + +// Platform-specific memory detection +fn get_total_system_memory() !u64 { + $if macos { + result := osal.execute_silent('sysctl -n hw.memsize') or { + return error('Failed to get system memory on macOS: ${err}') + } + return result.trim_space().u64() + } $else $if linux { + // Read from /proc/meminfo + content := os.read_file('/proc/meminfo') or { + return error('Failed to read /proc/meminfo on Linux: ${err}') + } + for line in content.split_into_lines() { + if line.starts_with('MemTotal:') { + parts := line.split_any(' \t').filter(it.len > 0) + if parts.len >= 2 { + kb_value := parts[1].u64() + return kb_value * kb_to_bytes_factor + } + } + } + return error('Could not parse MemTotal from /proc/meminfo') + } $else { + return error('Unsupported platform for memory detection') + } +} + +// Get cached or fresh system memory +fn get_system_memory_cached() u64 { + now := time.now() + + // Check if cache is valid + if memory_cache.total_bytes > 0 + && now.unix() - memory_cache.cached_at.unix() < memory_cache_ttl_seconds { + return memory_cache.total_bytes + } + + // Refresh cache + total_memory := get_total_system_memory() or { + console.print_debug('Failed to get system memory: ${err}') + return 0 + } + + memory_cache.total_bytes = total_memory + memory_cache.cached_at = now + + return total_memory +} + +// Calculate accurate memory percentage +fn calculate_memory_percentage(memory_bytes u64, ps_fallback_percent f64) f64 { + total_memory := get_system_memory_cached() + + if total_memory > 0 { + return (f64(memory_bytes) / f64(total_memory)) * 100.0 + } + + // Fallback to ps value if system memory detection fails + return ps_fallback_percent +} + @[heap] struct Pane { pub mut: @@ -22,28 +99,47 @@ pub mut: pub fn (mut p Pane) stats() !ProcessStats { if p.pid == 0 { - return ProcessStats{} + return ProcessStats{ + cpu_percent: 0.0 + memory_percent: 0.0 + memory_bytes: 0 + } } - // Use ps command to get CPU and memory stats - cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss --no-headers' + // Use ps command to get CPU and memory stats (cross-platform compatible) + cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss' result := osal.execute_silent(cmd) or { return error('Cannot get stats for PID ${p.pid}: ${err}') } - if result.trim_space() == '' { + lines := result.split_into_lines() + if lines.len < 2 { return error('Process ${p.pid} not found') } - parts := result.trim_space().split_any(' \t').filter(it != '') - if parts.len < 3 { - return error('Invalid ps output: ${result}') + // Skip header line, get data line + data_line := lines[1].trim_space() + if data_line == '' { + return error('Process ${p.pid} not found') } + parts := data_line.split_any(' \t').filter(it != '') + if parts.len < 3 { + return error('Invalid ps output: ${data_line}') + } + + // Parse values from ps output + cpu_percent := parts[0].f64() + ps_memory_percent := parts[1].f64() + memory_bytes := parts[2].u64() * kb_to_bytes_factor + + // Calculate accurate memory percentage using cached system memory + memory_percent := calculate_memory_percentage(memory_bytes, ps_memory_percent) + return ProcessStats{ - cpu_percent: parts[0].f64() - memory_percent: parts[1].f64() - memory_bytes: parts[2].u64() * 1024 // ps returns KB, convert to bytes + cpu_percent: cpu_percent + memory_percent: memory_percent + memory_bytes: memory_bytes } } @@ -147,3 +243,66 @@ pub fn (mut p Pane) processinfo_main() !osal.ProcessInfo { return osal.processinfo_get(p.pid)! } + +// Send a command to this pane +pub fn (mut p Pane) send_command(command string) ! { + cmd := 'tmux send-keys -t ${p.window.session.name}:@${p.window.id}.%${p.id} "${command}" Enter' + osal.execute_silent(cmd) or { return error('Cannot send command to pane %${p.id}: ${err}') } +} + +// Send raw keys to this pane (without Enter) +pub fn (mut p Pane) send_keys(keys string) ! { + cmd := 'tmux send-keys -t ${p.window.session.name}:@${p.window.id}.%${p.id} "${keys}"' + osal.execute_silent(cmd) or { return error('Cannot send keys to pane %${p.id}: ${err}') } +} + +// Kill this specific pane +pub fn (mut p Pane) kill() ! { + cmd := 'tmux kill-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id}' + osal.execute_silent(cmd) or { return error('Cannot kill pane %${p.id}: ${err}') } +} + +// Select/activate this pane +pub fn (mut p Pane) select() ! { + cmd := 'tmux select-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id}' + osal.execute_silent(cmd) or { return error('Cannot select pane %${p.id}: ${err}') } + p.active = true +} + +@[params] +pub struct PaneResizeArgs { +pub mut: + direction string = 'right' // 'up', 'down', 'left', 'right' + cells int = 5 // number of cells to resize by +} + +// Resize this pane +pub fn (mut p Pane) resize(args PaneResizeArgs) ! { + direction_flag := match args.direction.to_lower() { + 'up', 'u' { '-U' } + 'down', 'd' { '-D' } + 'left', 'l' { '-L' } + 'right', 'r' { '-R' } + else { return error('Invalid resize direction: ${args.direction}. Use up, down, left, or right') } + } + + cmd := 'tmux resize-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} ${direction_flag} ${args.cells}' + osal.execute_silent(cmd) or { return error('Cannot resize pane %${p.id}: ${err}') } +} + +// Convenience methods for resizing +pub fn (mut p Pane) resize_up(cells int) ! { + p.resize(direction: 'up', cells: cells)! +} + +pub fn (mut p Pane) resize_down(cells int) ! { + p.resize(direction: 'down', cells: cells)! +} + +pub fn (mut p Pane) resize_left(cells int) ! { + p.resize(direction: 'left', cells: cells)! +} + +pub fn (mut p Pane) resize_right(cells int) ! { + p.resize(direction: 'right', cells: cells)! +} diff --git a/lib/osal/tmux/tmux_scan.v b/lib/osal/tmux/tmux_scan.v index 65274a6a..c8e3819a 100644 --- a/lib/osal/tmux/tmux_scan.v +++ b/lib/osal/tmux/tmux_scan.v @@ -5,73 +5,99 @@ import freeflowuniverse.herolib.core.texttools import freeflowuniverse.herolib.ui.console import time +// Check if error message indicates tmux server is not running +fn is_tmux_server_not_running_error(error_msg string) bool { + // Common tmux server not running error patterns + tmux_not_running_patterns := [ + 'no server running', + 'error connecting to', + 'No such file or directory', // when socket doesn't exist + ] + + error_lower := error_msg.to_lower() + for pattern in tmux_not_running_patterns { + if error_lower.contains(pattern.to_lower()) { + return true + } + } + return false +} + fn (mut t Tmux) scan_add(line string) !&Pane { - // Parse the line to get session, window, and pane info - line_arr := line.split('|') - session_name := line_arr[0] - window_name := line_arr[1] - window_id := line_arr[2] - pane_active := line_arr[3] - pane_id := line_arr[4] - pane_pid := line_arr[5] - pane_start_command := line_arr[6] or { '' } + // Parse the line to get session, window, and pane info + line_arr := line.split('|') + if line_arr.len < 6 { + return error('Invalid tmux pane line format: ${line}') + } + session_name := line_arr[0] + window_name := line_arr[1] + window_id := line_arr[2] + pane_active := line_arr[3] + pane_id := line_arr[4] + pane_pid := line_arr[5] + pane_start_command := line_arr[6] or { '' } - wid := (window_id.replace('@', '')).int() - pid := (pane_id.replace('%', '')).int() + // Skip if window name is empty + if window_name.len == 0 { + return error('Window name is empty in line: ${line}') + } - mut s := t.session_get(session_name)! + wid := (window_id.replace('@', '')).int() + pid := (pane_id.replace('%', '')).int() - // Get or create window - mut w := if s.window_exist(name: window_name, id: wid) { - s.window_get(name: window_name, id: wid)! - } else { - mut new_w := Window{ - session: s - name: texttools.name_fix(window_name) - id: wid - panes: []&Pane{} - } - s.windows << &new_w - &new_w - } + mut s := t.session_get(session_name)! - // Create or update pane - mut p := Pane{ - window: w - id: pid - pid: pane_pid.int() - active: pane_active == '1' - cmd: pane_start_command - created_at: time.now() - } + // Get or create window + mut w := if s.window_exist(name: window_name, id: wid) { + s.window_get(name: window_name, id: wid)! + } else { + mut new_w := Window{ + session: s + name: texttools.name_fix(window_name) + id: wid + panes: []&Pane{} + } + s.windows << &new_w + &new_w + } - // Check if pane already exists - mut found := false - for mut existing_pane in w.panes { - if existing_pane.id == pid { - existing_pane.pid = p.pid - existing_pane.active = p.active - existing_pane.cmd = p.cmd - found = true - break - } - } + // Create or update pane + mut p := Pane{ + window: w + id: pid + pid: pane_pid.int() + active: pane_active == '1' + cmd: pane_start_command + created_at: time.now() + } - if !found { - w.panes << &p - } + // Check if pane already exists + mut found := false + for mut existing_pane in w.panes { + if existing_pane.id == pid { + existing_pane.pid = p.pid + existing_pane.active = p.active + existing_pane.cmd = p.cmd + found = true + break + } + } - return &p + if !found { + w.panes << &p + } + + return &p } // 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 ... +// 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 ... pub fn (mut t Tmux) scan() ! { // os.log('TMUX - Scanning ....') cmd_list_session := "tmux list-sessions -F '#{session_name}'" exec_list := osal.exec(cmd: cmd_list_session, stdout: false, name: 'tmux_list') or { - if err.msg().contains('no server running') { + if is_tmux_server_not_running_error(err.msg()) { return } return error('could not execute list sessions.\n${err}') diff --git a/lib/osal/tmux/tmux_session.v b/lib/osal/tmux/tmux_session.v index 9c4eb9f8..32044d53 100644 --- a/lib/osal/tmux/tmux_session.v +++ b/lib/osal/tmux/tmux_session.v @@ -63,8 +63,11 @@ pub fn (mut s Session) scan() ! { for line in result.split_into_lines() { if line.contains('|') { parts := line.split('|') - if parts.len >= 2 { + if parts.len >= 3 && parts[0].len > 0 && parts[1].len > 0 { window_name := texttools.name_fix(parts[0]) + if window_name.len == 0 { + continue + } window_id := parts[1].replace('@', '').int() window_active := parts[2] == '1' @@ -73,7 +76,7 @@ pub fn (mut s Session) scan() ! { // Update existing window or create new one mut found := false for mut w in s.windows { - if w.name == window_name { + 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 @@ -99,7 +102,7 @@ pub fn (mut s Session) scan() ! { } // Remove windows that no longer exist in tmux - s.windows = s.windows.filter(current_windows[it.name] == true) + s.windows = s.windows.filter(it.name.len > 0 && current_windows[it.name] == true) } // window_name is the name of the window in session main (will always be called session main) @@ -211,9 +214,12 @@ fn (mut s Session) window_exist(args_ WindowGetArgs) bool { 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(args.name) for w in s.windows { - if w.name == args.name { + if w.name.len > 0 && w.name == args.name { if (args.id > 0 && w.id == args.id) || args.id == 0 { return w } diff --git a/lib/osal/tmux/tmux_window.v b/lib/osal/tmux/tmux_window.v index 6412aac7..8163713e 100644 --- a/lib/osal/tmux/tmux_window.v +++ b/lib/osal/tmux/tmux_window.v @@ -13,7 +13,7 @@ pub mut: session &Session @[skip] name string id int - panes []&Pane // windows contain multiple panes + panes []&Pane // windows contain multiple panes active bool env map[string]string } @@ -22,105 +22,104 @@ pub mut: pub struct PaneNewArgs { pub mut: name string - reset bool //means we reset the pane if it already exists + reset bool // means we reset the pane if it already exists cmd string - env map[string]string + env map[string]string } - pub fn (mut w Window) scan() ! { - // Get current panes for this window - cmd := "tmux list-panes -t ${w.session.name}:@${w.id} -F '#{pane_id}|#{pane_pid}|#{pane_active}|#{pane_start_command}'" - result := osal.execute_silent(cmd) or { - // Window might not exist anymore - return - } - - mut current_panes := map[int]bool{} - for line in result.split_into_lines() { - if line.contains('|') { - parts := line.split('|') - if parts.len >= 3 { - pane_id := parts[0].replace('%', '').int() - pane_pid := parts[1].int() - pane_active := parts[2] == '1' - pane_cmd := if parts.len > 3 { parts[3] } else { '' } - - current_panes[pane_id] = true - - // Update existing pane or create new one - mut found := false - for mut p in w.panes { - if p.id == pane_id { - p.pid = pane_pid - p.active = pane_active - p.cmd = pane_cmd - found = true - break - } - } - - if !found { - mut new_pane := Pane{ - window: &w - id: pane_id - pid: pane_pid - active: pane_active - cmd: pane_cmd - env: map[string]string{} - created_at: time.now() - last_output_offset: 0 - } - w.panes << &new_pane - } - } - } - } - - // Remove panes that no longer exist - w.panes = w.panes.filter(current_panes[it.id] == true) -} + // Get current panes for this window + cmd := "tmux list-panes -t ${w.session.name}:@${w.id} -F '#{pane_id}|#{pane_pid}|#{pane_active}|#{pane_start_command}'" + result := osal.execute_silent(cmd) or { + // Window might not exist anymore + return + } + mut current_panes := map[int]bool{} + for line in result.split_into_lines() { + if line.contains('|') { + parts := line.split('|') + if parts.len >= 3 { + pane_id := parts[0].replace('%', '').int() + pane_pid := parts[1].int() + pane_active := parts[2] == '1' + pane_cmd := if parts.len > 3 { parts[3] } else { '' } + + current_panes[pane_id] = true + + // Update existing pane or create new one + mut found := false + for mut p in w.panes { + if p.id == pane_id { + p.pid = pane_pid + p.active = pane_active + p.cmd = pane_cmd + found = true + break + } + } + + if !found { + mut new_pane := Pane{ + window: &w + id: pane_id + pid: pane_pid + active: pane_active + cmd: pane_cmd + env: map[string]string{} + created_at: time.now() + last_output_offset: 0 + } + w.panes << &new_pane + } + } + } + } + + // Remove panes that no longer exist + w.panes = w.panes.filter(current_panes[it.id] == true) +} pub fn (mut w Window) stop() ! { - w.kill()! + w.kill()! } -//helper function -//TODO env variables are not inserted in pane + +// helper function +// TODO env variables are not inserted in pane pub fn (mut w Window) create(cmd_ string) ! { - mut final_cmd := cmd_ - if cmd_.contains('\n') { - os.mkdir_all('/tmp/tmux/${w.session.name}')! - // Fix: osal.exec_string doesn't exist, use file writing instead - script_path := '/tmp/tmux/${w.session.name}/${w.name}.sh' - script_content := '#!/bin/bash\n' + cmd_ - os.write_file(script_path, script_content)! - os.chmod(script_path, 0o755)! - final_cmd = script_path - } + mut final_cmd := cmd_ + if cmd_.contains('\n') { + os.mkdir_all('/tmp/tmux/${w.session.name}')! + // Fix: osal.exec_string doesn't exist, use file writing instead + script_path := '/tmp/tmux/${w.session.name}/${w.name}.sh' + script_content := '#!/bin/bash\n' + cmd_ + os.write_file(script_path, script_content)! + os.chmod(script_path, 0o755)! + final_cmd = script_path + } - mut newcmd := '/bin/bash -c "${final_cmd}"' - if cmd_ == "" { - newcmd = '/bin/bash' - } + mut newcmd := '/bin/bash -c "${final_cmd}"' + if cmd_ == '' { + newcmd = '/bin/bash' + } - // Build environment arguments - mut env_args := '' - for key, value in w.env { - env_args += ' -e ${key}="${value}"' - } + // Build environment arguments + mut env_args := '' + for key, value in w.env { + env_args += ' -e ${key}="${value}"' + } - res_opt := "-P -F '#{session_name}|#{window_name}|#{window_id}|#{pane_active}|#{pane_id}|#{pane_pid}|#{pane_start_command}'" - cmd := 'tmux new-window ${res_opt}${env_args} -t ${w.session.name} -n ${w.name} \'${newcmd}\'' - console.print_debug(cmd) - - res := osal.exec(cmd: cmd, stdout: false, name: 'tmux_window_create') or { - return error("Can't create new window ${w.name} \n${cmd}\n${err}") - } - - line_arr := res.output.split('|') - wid := line_arr[2] or { return error('cannot split line for window create.\n${line_arr}') } - w.id = wid.replace('@', '').int() + res_opt := "-P -F '#{session_name}|#{window_name}|#{window_id}|#{pane_active}|#{pane_id}|#{pane_pid}|#{pane_start_command}'" + cmd := 'tmux new-window ${res_opt}${env_args} -t ${w.session.name} -n ${w.name} \'${newcmd}\'' + console.print_debug(cmd) + + res := osal.exec(cmd: cmd, stdout: false, name: 'tmux_window_create') or { + return error("Can't create new window ${w.name} \n${cmd}\n${err}") + } + + line_arr := res.output.split('|') + wid := line_arr[2] or { return error('cannot split line for window create.\n${line_arr}') } + w.id = wid.replace('@', '').int() } // stop the window @@ -143,14 +142,14 @@ pub fn (window Window) str() string { } pub fn (mut w Window) stats() !ProcessStats { - mut total := ProcessStats{} - for mut pane in w.panes { - stats := pane.stats() or { continue } - total.cpu_percent += stats.cpu_percent - total.memory_bytes += stats.memory_bytes - total.memory_percent += stats.memory_percent - } - return total + mut total := ProcessStats{} + for mut pane in w.panes { + stats := pane.stats() or { continue } + total.cpu_percent += stats.cpu_percent + total.memory_bytes += stats.memory_bytes + total.memory_percent += stats.memory_percent + } + return total } // will select the current window so with tmux a we can go there . @@ -169,10 +168,91 @@ pub fn (mut w Window) pane_list() []&Pane { // Get active pane in window pub fn (mut w Window) pane_active() ?&Pane { - for pane in w.panes { - if pane.active { - return pane - } - } - return none + for pane in w.panes { + if pane.active { + return pane + } + } + return none +} + +@[params] +pub 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 +} + +// Split the active pane horizontally or vertically +pub fn (mut w Window) pane_split(args PaneSplitArgs) !&Pane { + mut cmd_to_run := args.cmd + if cmd_to_run == '' { + cmd_to_run = '/bin/bash' + } + + // Build environment arguments + mut env_args := '' + for key, value in args.env { + env_args += ' -e ${key}="${value}"' + } + + // Choose split direction + split_flag := if args.horizontal { '-h' } else { '-v' } + + // Execute tmux split-window command + res_opt := "-P -F '#{session_name}|#{window_name}|#{window_id}|#{pane_active}|#{pane_id}|#{pane_pid}|#{pane_start_command}'" + cmd := 'tmux split-window ${split_flag} ${res_opt}${env_args} -t ${w.session.name}:@${w.id} \'${cmd_to_run}\'' + + console.print_debug('Splitting pane: ${cmd}') + + res := osal.exec(cmd: cmd, stdout: false, name: 'tmux_pane_split') or { + return error("Can't split pane in window ${w.name}: ${err}") + } + + // Parse the result to get new pane info + line_arr := res.output.split('|') + if line_arr.len < 7 { + return error('Invalid tmux split-window output: ${res.output}') + } + + pane_id := line_arr[4].replace('%', '').int() + pane_pid := line_arr[5].int() + pane_active := line_arr[3] == '1' + pane_cmd := line_arr[6] or { '' } + + // Create new pane object + mut new_pane := Pane{ + window: &w + id: pane_id + pid: pane_pid + active: pane_active + cmd: pane_cmd + env: args.env + created_at: time.now() + last_output_offset: 0 + } + + // Add to window's panes and rescan to get current state + w.panes << &new_pane + w.scan()! + + // Return reference to the new pane + for mut pane in w.panes { + 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) +pub fn (mut w Window) pane_split_horizontal(cmd string) !&Pane { + return w.pane_split(cmd: cmd, horizontal: true) +} + +// Split pane vertically (top and bottom) +pub fn (mut w Window) pane_split_vertical(cmd string) !&Pane { + return w.pane_split(cmd: cmd, horizontal: false) } From 426a53a50dc08a58e3c8a3105aceac5fb55f157b Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 24 Aug 2025 18:10:30 +0300 Subject: [PATCH 03/10] refactor: Remove is_tmux_server_not_running_error function --- lib/osal/tmux/tmux.v | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/osal/tmux/tmux.v b/lib/osal/tmux/tmux.v index b63b5a35..7d451f8b 100644 --- a/lib/osal/tmux/tmux.v +++ b/lib/osal/tmux/tmux.v @@ -7,24 +7,6 @@ import os import time import freeflowuniverse.herolib.ui.console -// Check if error message indicates tmux server is not running -fn is_tmux_server_not_running_error(error_msg string) bool { - // Common tmux server not running error patterns - tmux_not_running_patterns := [ - 'no server running', - 'error connecting to', - 'No such file or directory', // when socket doesn't exist - ] - - error_lower := error_msg.to_lower() - for pattern in tmux_not_running_patterns { - if error_lower.contains(pattern.to_lower()) { - return true - } - } - return false -} - @[heap] pub struct Tmux { pub mut: From 25327053b99d926845284d340bdf0b41666e1d0b Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 24 Aug 2025 23:12:32 +0300 Subject: [PATCH 04/10] feat: add tmux dashboard with ttyd integration - Create script for 3-pane tmux dashboard - Run Python HTTP server, counter, and htop in panes - Add `run_ttyd` function to `Session` struct - Add `run_ttyd` function to `Window` struct - Expose tmux session and window via ttyd --- examples/tmux/server_dashboard.vsh | 167 +++++++++++++++++++++++++++++ lib/osal/tmux/tmux_session.v | 13 +++ lib/osal/tmux/tmux_window.v | 13 +++ 3 files changed, 193 insertions(+) create mode 100755 examples/tmux/server_dashboard.vsh diff --git a/examples/tmux/server_dashboard.vsh b/examples/tmux/server_dashboard.vsh new file mode 100755 index 00000000..13224c16 --- /dev/null +++ b/examples/tmux/server_dashboard.vsh @@ -0,0 +1,167 @@ +#!/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' + window_name = 'dashboard' + python_port = 8000 + ttyd_port = 7890 +) + +println('=== Server Dashboard with 3 Panes ===') +println('Setting up tmux session with:') +println(' 1. Python HTTP Server (port ${python_port})') +println(' 2. Counter (updating every 5 seconds)') +println(' 3. 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 3-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()! +} + +println('3. Layout complete! We now have 3 panes.') + +// 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 < 3 { + eprintln('Expected 3 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 +println('Starting Counter in pane 2...') +mut pane2 := window.panes[1] +pane2.select()! +pane2.send_command('echo "=== Counter 1 to 10000 every 5 seconds ==="')! +// Start simple counter using a loop instead of watch +pane2.send_command('while true; do echo "Count: $(date)"; sleep 5; done')! + +time.sleep(500 * time.millisecond) + +// Pane 3 (right): CPU Monitor +println('Starting CPU Monitor in pane 3...') +mut pane3 := window.panes[2] +pane3.select()! +pane3.send_command('echo "=== CPU Monitor ==="')! +pane3.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' } + 2 { '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' } + 2 { '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') + +window.run_ttyd(ttyd_port) or { println('Failed to start ttyd: ${err}') } diff --git a/lib/osal/tmux/tmux_session.v b/lib/osal/tmux/tmux_session.v index 32044d53..9c964076 100644 --- a/lib/osal/tmux/tmux_session.v +++ b/lib/osal/tmux/tmux_session.v @@ -258,3 +258,16 @@ pub fn (mut s Session) stop() ! { return error("Can't delete session ${s.name} - This may happen when session is not found: ${err}") } } + +// Run ttyd for this session so it can be accessed in the browser +pub fn (mut s Session) run_ttyd(port int) ! { + target := '${s.name}' + cmd := 'nohup ttyd -p ${port} tmux attach -t ${target} >/dev/null 2>&1 &' + + code := os.system(cmd) + if code != 0 { + return error('Failed to start ttyd on port ${port} for session ${s.name}') + } + + println('ttyd started for session ${s.name} at http://localhost:${port}') +} diff --git a/lib/osal/tmux/tmux_window.v b/lib/osal/tmux/tmux_window.v index 8163713e..acc9edbb 100644 --- a/lib/osal/tmux/tmux_window.v +++ b/lib/osal/tmux/tmux_window.v @@ -256,3 +256,16 @@ pub fn (mut w Window) pane_split_horizontal(cmd string) !&Pane { pub fn (mut w Window) pane_split_vertical(cmd string) !&Pane { return w.pane_split(cmd: cmd, horizontal: false) } + +// Run ttyd for this window so it can be accessed in the browser +pub fn (mut w Window) run_ttyd(port int) ! { + target := '${w.session.name}:@${w.id}' + cmd := 'nohup ttyd -p ${port} tmux attach -t ${target} >/dev/null 2>&1 &' + + code := os.system(cmd) + if code != 0 { + return error('Failed to start ttyd on port ${port} for window ${w.name}') + } + + println('ttyd started for window ${w.name} at http://localhost:${port}') +} From a37dbd243800f4db600d2fbf7dbc5f1500b80b66 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 24 Aug 2025 23:54:57 +0300 Subject: [PATCH 05/10] refactor: update SSH agent examples and module structure - Refactor `gittools` to remove `sshagent` import - Update `sshagent.loaded()` to use `ssh-add -l` command - Relocate and expose `remote_copy` and `remote_auth` functions - Improve SSH agent examples and remove Linux tests - Optimize `sshagent` module and `play` function imports --- examples/osal/sshagent.vsh | 65 --------- examples/osal/sshagent/sshagent_example.v | 51 ------- examples/osal/sshagent/sshagent_example.vsh | 55 ++++++++ examples/osal/sshagent/sshagent_example2.vsh | 85 ++++++++++++ lib/develop/gittools/repository_utils.v | 5 +- lib/osal/sshagent/agent.v | 133 ++++++------------- lib/osal/sshagent/builder_integration.v | 96 ++++++++++--- lib/osal/sshagent/factory.v | 4 +- lib/osal/sshagent/play.v | 14 +- 9 files changed, 263 insertions(+), 245 deletions(-) delete mode 100644 examples/osal/sshagent.vsh delete mode 100644 examples/osal/sshagent/sshagent_example.v create mode 100755 examples/osal/sshagent/sshagent_example.vsh create mode 100755 examples/osal/sshagent/sshagent_example2.vsh diff --git a/examples/osal/sshagent.vsh b/examples/osal/sshagent.vsh deleted file mode 100644 index 5bcc6bb9..00000000 --- a/examples/osal/sshagent.vsh +++ /dev/null @@ -1,65 +0,0 @@ -#!/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.builder -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: Push key to remote node (uncomment and modify for actual use) -/* -console.print_header('Testing remote node key deployment...') -mut b := builder.new()! - -// Create connection to remote node -mut node := b.node_new( - ipaddr: 'root@192.168.1.100:22' // Replace with actual remote host - name: 'test_node' -)! - -if agent.keys.len > 0 { - key_name := agent.keys[0].name - console.print_debug('Pushing key "${key_name}" to remote node...') - - // Push the key - agent.push_key_to_node(mut node, key_name)! - - // Verify access - if agent.verify_key_access(mut node, key_name)! { - console.print_debug('✓ SSH key access verified') - } else { - console.print_debug('✗ SSH key access verification failed') - } - - // Optional: Remove key from remote (for testing) - // agent.remove_key_from_node(mut node, key_name)! - // console.print_debug('Key removed from remote node') -} -*/ - -console.print_header('SSH Agent example completed successfully') \ No newline at end of file diff --git a/examples/osal/sshagent/sshagent_example.v b/examples/osal/sshagent/sshagent_example.v deleted file mode 100644 index fc20a9df..00000000 --- a/examples/osal/sshagent/sshagent_example.v +++ /dev/null @@ -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) } -} diff --git a/examples/osal/sshagent/sshagent_example.vsh b/examples/osal/sshagent/sshagent_example.vsh new file mode 100755 index 00000000..e19a1b8b --- /dev/null +++ b/examples/osal/sshagent/sshagent_example.vsh @@ -0,0 +1,55 @@ +#!/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 + +fn do_sshagent_example() ! { + console.print_header('SSH Agent Basic Example') + + mut agent := sshagent.new()! + console.print_debug('SSH Agent created') + println(agent) + + // Generate a test key if no keys exist + if agent.keys.len == 0 { + console.print_debug('No keys found, generating test key...') + mut test_key := agent.generate('test_example_key', '')! + test_key.load()! + console.print_debug('Test key generated and loaded') + } + + // Try to get a specific key (this will fail if key doesn't exist) + console.print_debug('Looking for existing keys...') + + if agent.keys.len > 0 { + // Work with the first available key + mut first_key := agent.keys[0] + console.print_debug('Found key: ${first_key.name}') + + if !first_key.loaded { + console.print_debug('Loading key...') + first_key.load()! + console.print_debug('Key loaded') + } + + console.print_debug('Key details:') + println(first_key) + + // Show agent status after loading + console.print_debug('Agent status after loading:') + println(agent) + + // Note: We don't call forget() in this example to avoid removing keys + // first_key.forget()! + } else { + console.print_debug('No keys available in agent') + } +} + +fn main() { + do_sshagent_example() or { + console.print_debug('Error: ${err}') + panic(err) + } + console.print_header('SSH Agent example completed successfully!') +} diff --git a/examples/osal/sshagent/sshagent_example2.vsh b/examples/osal/sshagent/sshagent_example2.vsh new file mode 100755 index 00000000..116cd691 --- /dev/null +++ b/examples/osal/sshagent/sshagent_example2.vsh @@ -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') \ No newline at end of file diff --git a/lib/develop/gittools/repository_utils.v b/lib/develop/gittools/repository_utils.v index 5debb06f..dd30f56d 100644 --- a/lib/develop/gittools/repository_utils.v +++ b/lib/develop/gittools/repository_utils.v @@ -1,6 +1,5 @@ module gittools -import freeflowuniverse.herolib.osal.sshagent import freeflowuniverse.herolib.core.pathlib import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.develop.vscode @@ -106,7 +105,9 @@ fn (self GitRepo) get_repo_url_for_clone() !string { // return url // } - if sshagent.loaded() { + // Check if SSH agent is loaded (avoid importing sshagent to prevent circular dependency) + ssh_check := os.execute('ssh-add -l') + if ssh_check.exit_code == 0 { return self.get_ssh_url()! } else { return self.get_http_url()! diff --git a/lib/osal/sshagent/agent.v b/lib/osal/sshagent/agent.v index 6f27a057..549e6edd 100644 --- a/lib/osal/sshagent/agent.v +++ b/lib/osal/sshagent/agent.v @@ -1,29 +1,31 @@ module sshagent +import freeflowuniverse.herolib.ui.console + // Check if SSH agent is properly configured and all is good fn agent_check(mut agent SSHAgent) ! { console.print_header('SSH Agent Check') - + // Ensure single agent is running agent.ensure_single_agent()! - + // Get diagnostics diag := agent.diagnostics() - + for key, value in diag { console.print_item('${key}: ${value}') } - + // Verify agent is responsive if !agent.is_agent_responsive() { return error('SSH agent is not responsive') } - + // Load all existing keys from ~/.ssh that aren't loaded yet agent.init()! - + console.print_green('✓ SSH Agent is properly configured and running') - + // Show loaded keys loaded_keys := agent.keys_loaded()! console.print_item('Loaded keys: ${loaded_keys.len}') @@ -35,17 +37,17 @@ fn agent_check(mut agent SSHAgent) ! { // Create a new SSH key fn sshkey_create(mut agent SSHAgent, name string, passphrase string) ! { console.print_header('Creating SSH key: ${name}') - + // Check if key already exists if agent.exists(name: name) { console.print_debug('SSH key "${name}" already exists') return } - + // Generate new key mut key := agent.generate(name, passphrase)! console.print_green('✓ SSH key "${name}" created successfully') - + // Automatically load the key key.load()! console.print_green('✓ SSH key "${name}" loaded into agent') @@ -54,28 +56,28 @@ fn sshkey_create(mut agent SSHAgent, name string, passphrase string) ! { // Delete an SSH key fn sshkey_delete(mut agent SSHAgent, name string) ! { console.print_header('Deleting SSH key: ${name}') - + // Check if key exists mut key := agent.get(name: name) or { console.print_debug('SSH key "${name}" does not exist') return } - + // Get key paths before deletion - key_path := key.keypath() or { + mut key_path := key.keypath() or { console.print_debug('Private key path not available for "${name}"') key.keypath_pub() or { return } // Just to trigger the path lookup } - key_pub_path := key.keypath_pub() or { + mut key_pub_path := key.keypath_pub() or { console.print_debug('Public key path not available for "${name}"') return } - + // Remove from agent if loaded (temporarily disabled due to reset_ssh panic) // if key.loaded { // key.forget()! // } - + // Delete key files if key_path.exists() { key_path.delete()! @@ -85,26 +87,24 @@ fn sshkey_delete(mut agent SSHAgent, name string) ! { key_pub_path.delete()! console.print_debug('Deleted public key: ${key_pub_path.path}') } - + // Reinitialize agent to update key list agent.init()! - + console.print_green('✓ SSH key "${name}" deleted successfully') } // Load SSH key into agent fn sshkey_load(mut agent SSHAgent, name string) ! { console.print_header('Loading SSH key: ${name}') - - mut key := agent.get(name: name) or { - return error('SSH key "${name}" not found') - } - + + mut key := agent.get(name: name) or { return error('SSH key "${name}" not found') } + if key.loaded { console.print_debug('SSH key "${name}" is already loaded') return } - + key.load()! console.print_green('✓ SSH key "${name}" loaded into agent') } @@ -112,28 +112,24 @@ fn sshkey_load(mut agent SSHAgent, name string) ! { // Check if SSH key is valid fn sshkey_check(mut agent SSHAgent, name string) ! { console.print_header('Checking SSH key: ${name}') - - mut key := agent.get(name: name) or { - return error('SSH key "${name}" not found') - } - + + mut key := agent.get(name: name) or { return error('SSH key "${name}" not found') } + // Check if key files exist - key_path := key.keypath() or { - return error('Private key file not found for "${name}"') - } - - key_pub_path := key.keypath_pub() or { + mut key_path := key.keypath() or { return error('Private key file not found for "${name}"') } + + mut key_pub_path := key.keypath_pub() or { return error('Public key file not found for "${name}"') } - + if !key_path.exists() { return error('Private key file does not exist: ${key_path.path}') } - + if !key_pub_path.exists() { return error('Public key file does not exist: ${key_pub_path.path}') } - + // Verify key can be loaded (if not already loaded) if !key.loaded { // Test load without actually loading (since forget is disabled) @@ -142,70 +138,15 @@ fn sshkey_check(mut agent SSHAgent, name string) ! { return error('Invalid private key format in "${name}"') } } - + console.print_item('Key type: ${key.cat}') console.print_item('Loaded: ${key.loaded}') console.print_item('Email: ${key.email}') console.print_item('Private key: ${key_path.path}') console.print_item('Public key: ${key_pub_path.path}') - + console.print_green('✓ SSH key "${name}" is valid') } -// Copy private key to remote node -fn remote_copy(mut agent SSHAgent, node_addr string, key_name string) ! { - console.print_header('Copying SSH key "${key_name}" to ${node_addr}') - - // Get the key - mut key := agent.get(name: key_name) or { - return error('SSH key "${key_name}" not found') - } - - // Create builder node - mut b := builder.new()! - mut node := b.node_new(ipaddr: node_addr)! - - // Get private key content - key_path := key.keypath()! - if !key_path.exists() { - return error('Private key file not found: ${key_path.path}') - } - - private_key_content := key_path.read()! - - // Get home directory on remote - home_dir := node.environ_get()!['HOME'] or { - return error('Could not determine HOME directory on remote node') - } - - remote_ssh_dir := '${home_dir}/.ssh' - remote_key_path := '${remote_ssh_dir}/${key_name}' - - // Ensure .ssh directory exists with correct permissions - node.exec_silent('mkdir -p ${remote_ssh_dir}')! - node.exec_silent('chmod 700 ${remote_ssh_dir}')! - - // Copy private key to remote - node.file_write(remote_key_path, private_key_content)! - node.exec_silent('chmod 600 ${remote_key_path}')! - - // Generate public key on remote - node.exec_silent('ssh-keygen -y -f ${remote_key_path} > ${remote_key_path}.pub')! - node.exec_silent('chmod 644 ${remote_key_path}.pub')! - - console.print_green('✓ SSH key "${key_name}" copied to ${node_addr}') -} - -// Add public key to authorized_keys on remote node -fn remote_auth(mut agent SSHAgent, node_addr string, key_name string) ! { - console.print_header('Adding SSH key "${key_name}" to authorized_keys on ${node_addr}') - - // Create builder node - mut b := builder.new()! - mut node := b.node_new(ipaddr: node_addr)! - - // Use existing builder integration - agent.push_key_to_node(mut node, key_name)! - - console.print_green('✓ SSH key "${key_name}" added to authorized_keys on ${node_addr}') -} \ No newline at end of file +// Note: remote_copy and remote_auth functions moved to builder_integration.v +// to avoid circular dependencies diff --git a/lib/osal/sshagent/builder_integration.v b/lib/osal/sshagent/builder_integration.v index f85bf3d9..2eff438a 100644 --- a/lib/osal/sshagent/builder_integration.v +++ b/lib/osal/sshagent/builder_integration.v @@ -10,27 +10,27 @@ pub fn (mut agent SSHAgent) push_key_to_node(mut node builder.Node, key_name str if node_info['category'] != 'ssh' { return error('Can only push keys to SSH nodes, got: ${node_info['category']}') } - + // Find the key mut key := agent.get(name: key_name) or { return error('SSH key "${key_name}" not found in agent') } - + // Get public key content pubkey_content := key.keypub()! - + // Check if authorized_keys file exists on remote home_dir := node.environ_get()!['HOME'] or { return error('Could not determine HOME directory on remote node') } - + ssh_dir := '${home_dir}/.ssh' authorized_keys_path := '${ssh_dir}/authorized_keys' - + // Ensure .ssh directory exists with correct permissions node.exec_silent('mkdir -p ${ssh_dir}')! node.exec_silent('chmod 700 ${ssh_dir}')! - + // Check if key already exists if node.file_exists(authorized_keys_path) { existing_keys := node.file_read(authorized_keys_path)! @@ -39,11 +39,11 @@ pub fn (mut agent SSHAgent) push_key_to_node(mut node builder.Node, key_name str return } } - + // Add key to authorized_keys node.exec_silent('echo "${pubkey_content}" >> ${authorized_keys_path}')! node.exec_silent('chmod 600 ${authorized_keys_path}')! - + console.print_debug('SSH key "${key_name}" successfully pushed to node') } @@ -54,31 +54,31 @@ pub fn (mut agent SSHAgent) remove_key_from_node(mut node builder.Node, key_name if node_info['category'] != 'ssh' { return error('Can only remove keys from SSH nodes, got: ${node_info['category']}') } - + // Find the key mut key := agent.get(name: key_name) or { return error('SSH key "${key_name}" not found in agent') } - + // Get public key content pubkey_content := key.keypub()! - + // Get authorized_keys path home_dir := node.environ_get()!['HOME'] or { return error('Could not determine HOME directory on remote node') } - + authorized_keys_path := '${home_dir}/.ssh/authorized_keys' - + if !node.file_exists(authorized_keys_path) { console.print_debug('authorized_keys file does not exist on remote node') return } - + // Remove the key line from authorized_keys escaped_key := pubkey_content.replace('/', '\\/') node.exec_silent('sed -i "\\|${escaped_key}|d" ${authorized_keys_path}')! - + console.print_debug('SSH key "${key_name}" removed from remote node') } @@ -90,11 +90,65 @@ pub fn (mut agent SSHAgent) verify_key_access(mut node builder.Node, key_name st if node_info['category'] != 'ssh' { return error('Can only verify access to SSH nodes') } - + // Test basic connectivity - result := node.exec_silent('echo "SSH key verification successful"') or { - return false - } - + result := node.exec_silent('echo "SSH key verification successful"') or { return false } + return result.contains('SSH key verification successful') -} \ No newline at end of file +} + +// Copy private key to remote node +pub fn remote_copy(mut agent SSHAgent, node_addr string, key_name string) ! { + console.print_header('Copying SSH key "${key_name}" to ${node_addr}') + + // Get the key + mut key := agent.get(name: key_name) or { return error('SSH key "${key_name}" not found') } + + // Create builder node + mut b := builder.new()! + mut node := b.node_new(ipaddr: node_addr)! + + // Get private key content + mut key_path := key.keypath()! + if !key_path.exists() { + return error('Private key file not found: ${key_path.path}') + } + + private_key_content := key_path.read()! + + // Get home directory on remote + home_dir := node.environ_get()!['HOME'] or { + return error('Could not determine HOME directory on remote node') + } + + remote_ssh_dir := '${home_dir}/.ssh' + remote_key_path := '${remote_ssh_dir}/${key_name}' + + // Ensure .ssh directory exists with correct permissions + node.exec_silent('mkdir -p ${remote_ssh_dir}')! + node.exec_silent('chmod 700 ${remote_ssh_dir}')! + + // Copy private key to remote + node.file_write(remote_key_path, private_key_content)! + node.exec_silent('chmod 600 ${remote_key_path}')! + + // Generate public key on remote + node.exec_silent('ssh-keygen -y -f ${remote_key_path} > ${remote_key_path}.pub')! + node.exec_silent('chmod 644 ${remote_key_path}.pub')! + + console.print_green('✓ SSH key "${key_name}" copied to ${node_addr}') +} + +// Add public key to authorized_keys on remote node +pub fn remote_auth(mut agent SSHAgent, node_addr string, key_name string) ! { + console.print_header('Adding SSH key "${key_name}" to authorized_keys on ${node_addr}') + + // Create builder node + mut b := builder.new()! + mut node := b.node_new(ipaddr: node_addr)! + + // Use existing builder integration + agent.push_key_to_node(mut node, key_name)! + + console.print_green('✓ SSH key "${key_name}" added to authorized_keys on ${node_addr}') +} diff --git a/lib/osal/sshagent/factory.v b/lib/osal/sshagent/factory.v index 71c9c33e..a9639a7b 100644 --- a/lib/osal/sshagent/factory.v +++ b/lib/osal/sshagent/factory.v @@ -27,8 +27,8 @@ pub fn new(args_ SSHAgentNewArgs) !SSHAgent { } pub fn loaded() bool { - mut agent := new() or { panic(err) } - return agent.active + res := os.execute('ssh-add -l') + return res.exit_code == 0 } // create new SSH agent with single instance guarantee diff --git a/lib/osal/sshagent/play.v b/lib/osal/sshagent/play.v index 1ef6f5cb..ff9cbeee 100644 --- a/lib/osal/sshagent/play.v +++ b/lib/osal/sshagent/play.v @@ -1,8 +1,6 @@ module sshagent import freeflowuniverse.herolib.core.playbook { PlayBook } -import freeflowuniverse.herolib.ui.console -import freeflowuniverse.herolib.builder pub fn play(mut plbook PlayBook) ! { if !plbook.exists(filter: 'sshagent.') { @@ -25,7 +23,7 @@ pub fn play(mut plbook PlayBook) ! { mut p := action.params name := p.get('name')! passphrase := p.get_default('passphrase', '')! - + sshkey_create(mut agent, name, passphrase)! action.done = true } @@ -35,7 +33,7 @@ pub fn play(mut plbook PlayBook) ! { for mut action in delete_actions { mut p := action.params name := p.get('name')! - + sshkey_delete(mut agent, name)! action.done = true } @@ -45,7 +43,7 @@ pub fn play(mut plbook PlayBook) ! { for mut action in load_actions { mut p := action.params name := p.get('name')! - + sshkey_load(mut agent, name)! action.done = true } @@ -55,7 +53,7 @@ pub fn play(mut plbook PlayBook) ! { for mut action in check_key_actions { mut p := action.params name := p.get('name')! - + sshkey_check(mut agent, name)! action.done = true } @@ -66,7 +64,7 @@ pub fn play(mut plbook PlayBook) ! { mut p := action.params node_addr := p.get('node')! key_name := p.get('name')! - + remote_copy(mut agent, node_addr, key_name)! action.done = true } @@ -77,7 +75,7 @@ pub fn play(mut plbook PlayBook) ! { mut p := action.params node_addr := p.get('node')! key_name := p.get('name')! - + remote_auth(mut agent, node_addr, key_name)! action.done = true } From 5f75c542df74e0d8eb4a324da55bb8ac45248f85 Mon Sep 17 00:00:00 2001 From: despiegk Date: Mon, 25 Aug 2025 06:34:39 +0200 Subject: [PATCH 06/10] ... --- examples/osal/sshagent/sshagent_example2.vsh | 6 +++--- examples/tmux/server_dashboard.vsh | 14 ++++++-------- examples/tmux/tmux.vsh | 8 +++----- examples/tmux/tmux_pane_resize.vsh | 12 ++++++------ 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/examples/osal/sshagent/sshagent_example2.vsh b/examples/osal/sshagent/sshagent_example2.vsh index 116cd691..9933cbee 100755 --- a/examples/osal/sshagent/sshagent_example2.vsh +++ b/examples/osal/sshagent/sshagent_example2.vsh @@ -24,7 +24,7 @@ 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') @@ -35,7 +35,7 @@ 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('Key ${i + 1}: ${key.name}') console.print_debug(' Type: ${key.cat}') console.print_debug(' Loaded: ${key.loaded}') console.print_debug(' Email: ${key.email}') @@ -82,4 +82,4 @@ console.print_debug('Test key exists: ${existing_key.name}') console.print_header('Final SSH Agent Status:') println(agent) -console.print_header('SSH Agent example completed successfully') \ No newline at end of file +console.print_header('SSH Agent example completed successfully') diff --git a/examples/tmux/server_dashboard.vsh b/examples/tmux/server_dashboard.vsh index 13224c16..00161c26 100755 --- a/examples/tmux/server_dashboard.vsh +++ b/examples/tmux/server_dashboard.vsh @@ -6,12 +6,10 @@ import time import os // Configuration -const ( - session_name = 'server_dashboard' - window_name = 'dashboard' - python_port = 8000 - ttyd_port = 7890 -) +const session_name = 'server_dashboard' +const window_name = 'dashboard' +const python_port = 8000 +const ttyd_port = 7890 println('=== Server Dashboard with 3 Panes ===') println('Setting up tmux session with:') @@ -123,7 +121,7 @@ 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)') + println(' Pane ${i + 1}: ID=%${pane.id}, PID=${pane.pid} (stats unavailable)') continue } memory_mb := f64(stats.memory_bytes) / (1024.0 * 1024.0) @@ -133,7 +131,7 @@ for i, mut pane in window.panes { 2 { 'CPU Monitor' } else { 'Unknown' } } - println(' Pane ${i+1} (${service_name}): ID=%${pane.id}, CPU=${stats.cpu_percent:.1f}%, Memory=${memory_mb:.1f}MB') + println(' Pane ${i + 1} (${service_name}): ID=%${pane.id}, CPU=${stats.cpu_percent:.1f}%, Memory=${memory_mb:.1f}MB') } println('\n=== Access Information ===') diff --git a/examples/tmux/tmux.vsh b/examples/tmux/tmux.vsh index 89f2cbe4..3fbc4d29 100755 --- a/examples/tmux/tmux.vsh +++ b/examples/tmux/tmux.vsh @@ -5,11 +5,9 @@ import freeflowuniverse.herolib.osal.core as osal import time // Constants for display formatting -const ( - bytes_to_mb = 1024.0 * 1024.0 - cpu_precision = 1 - memory_precision = 3 -) +const bytes_to_mb = 1024.0 * 1024.0 +const cpu_precision = 1 +const memory_precision = 3 println('=== Tmux Pane Example ===') diff --git a/examples/tmux/tmux_pane_resize.vsh b/examples/tmux/tmux_pane_resize.vsh index aa738d9c..be443ca9 100755 --- a/examples/tmux/tmux_pane_resize.vsh +++ b/examples/tmux/tmux_pane_resize.vsh @@ -68,9 +68,9 @@ 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 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...') @@ -116,7 +116,7 @@ if window.panes.len >= 4 { bash_pane.send_command('pwd')! time.sleep(500 * time.millisecond) - // Send command to top pane + // Send command to top pane mut top_pane := window.panes[2] top_pane.send_command('echo "=== Top Pane ==="')! time.sleep(500 * time.millisecond) @@ -127,12 +127,12 @@ 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(' ${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_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') From 750e34cbe476913c74532b6dd06b9648a657e99d Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Mon, 25 Aug 2025 09:43:37 +0300 Subject: [PATCH 07/10] fix: Fix build --- lib/osal/sshagent/agent.v | 59 --------------------------------------- 1 file changed, 59 deletions(-) diff --git a/lib/osal/sshagent/agent.v b/lib/osal/sshagent/agent.v index 94ec84ea..70741bb8 100644 --- a/lib/osal/sshagent/agent.v +++ b/lib/osal/sshagent/agent.v @@ -148,62 +148,3 @@ pub fn sshkey_check(mut agent SSHAgent, name string) ! { console.print_green('✓ SSH key "${name}" is valid') } - -// Copy private key to remote node -pub fn remote_copy(mut agent SSHAgent, node_addr string, key_name string) ! { - console.print_header('Copying SSH key "${key_name}" to ${node_addr}') - - // Get the key - mut key := agent.get(name: key_name) or { return error('SSH key "${key_name}" not found') } - - // Create builder node - mut b := builder.new() or { return error('Failed to create builder') } - mut node := b.node_new(ipaddr: node_addr) or { return error('Failed to create node') } - - // Get private key content - mut key_path := key.keypath()! - if !key_path.exists() { - return error('Private key file not found: ${key_path.path}') - } - - private_key_content := key_path.read()! - - // Get home directory on remote - home_dir_map := node.environ_get() or { - return error('Could not get environment on remote node') - } - home_dir := home_dir_map['HOME'] or { - return error('Could not determine HOME directory on remote node') - } - - remote_ssh_dir := '${home_dir}/.ssh' - remote_key_path := '${remote_ssh_dir}/${key_name}' - - // Ensure .ssh directory exists with correct permissions - node.exec_silent('mkdir -p ${remote_ssh_dir}')! - node.exec_silent('chmod 700 ${remote_ssh_dir}')! - - // Copy private key to remote - node.file_write(remote_key_path, private_key_content)! - node.exec_silent('chmod 600 ${remote_key_path}')! - - // Generate public key on remote - node.exec_silent('ssh-keygen -y -f ${remote_key_path} > ${remote_key_path}.pub')! - node.exec_silent('chmod 644 ${remote_key_path}.pub')! - - console.print_green('✓ SSH key "${key_name}" copied to ${node_addr}') -} - -// Add public key to authorized_keys on remote node -pub fn remote_auth(mut agent SSHAgent, node_addr string, key_name string) ! { - console.print_header('Adding SSH key "${key_name}" to authorized_keys on ${node_addr}') - - // Create builder node - mut b := builder.new() or { return error('Failed to create builder') } - mut node := b.node_new(ipaddr: node_addr) or { return error('Failed to create node') } - - // Use existing builder integration - agent.push_key_to_node(mut node, key_name)! - - console.print_green('✓ SSH key "${key_name}" added to authorized_keys on ${node_addr}') -} From dcb971459961d9b5e63f1e019fdbecb8d4c87141 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Mon, 25 Aug 2025 10:16:10 +0300 Subject: [PATCH 08/10] feat: add CLI for dashboard management and 4-pane layout - Implement CLI for start, stop, status, restart - Refactor dashboard setup into `start_dashboard` function - Add `stop_dashboard` and `show_status` functions - Expand tmux dashboard layout to 4 panes (2x2 grid) - Integrate "Hero Web" service into dashboard panes --- examples/tmux/server_dashboard.vsh | 255 +++++++++++++++++++++++++---- 1 file changed, 227 insertions(+), 28 deletions(-) diff --git a/examples/tmux/server_dashboard.vsh b/examples/tmux/server_dashboard.vsh index 00161c26..c5a5fafd 100755 --- a/examples/tmux/server_dashboard.vsh +++ b/examples/tmux/server_dashboard.vsh @@ -11,12 +11,142 @@ const window_name = 'dashboard' const python_port = 8000 const ttyd_port = 7890 -println('=== Server Dashboard with 3 Panes ===') -println('Setting up tmux session with:') -println(' 1. Python HTTP Server (port ${python_port})') -println(' 2. Counter (updating every 5 seconds)') -println(' 3. CPU Monitor (htop)') -println('') +// Command line argument handling +fn show_help() { + println('=== Tmux Server Dashboard ===') + println('Usage:') + println(' ${os.args[0]} # Start the dashboard') + 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})') +} + +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() ! { + 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()! @@ -44,7 +174,7 @@ mut window := session.window_new(name: window_name, cmd: 'bash', reset: true)! time.sleep(500 * time.millisecond) t.scan()! -println('\n=== Setting up 3-pane layout ===') +println('\n=== Setting up 4-pane layout ===') // Get the main window window = session.window_get(name: window_name)! @@ -67,7 +197,26 @@ if window.panes.len >= 2 { window.scan()! } -println('3. Layout complete! We now have 3 panes.') +// 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()! @@ -76,8 +225,8 @@ for i, pane in window.panes { println(' Pane ${i}: ID=%${pane.id}, PID=${pane.pid}') } -if window.panes.len < 3 { - eprintln('Expected 3 panes, but got ${window.panes.len}') +if window.panes.len < 4 { + eprintln('Expected 4 panes, but got ${window.panes.len}') exit(1) } @@ -92,22 +241,30 @@ pane1.send_command('cd /tmp && python3 -m http.server ${python_port}')! time.sleep(500 * time.millisecond) -// Pane 2 (bottom-left): Counter -println('Starting Counter in pane 2...') +// 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 1 to 10000 every 5 seconds ==="')! -// Start simple counter using a loop instead of watch +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 (right): CPU Monitor -println('Starting CPU Monitor in pane 3...') +// Pane 3 (top-right): Hero Web +println('Starting Hero Web in pane 3...') mut pane3 := window.panes[2] pane3.select()! -pane3.send_command('echo "=== CPU Monitor ==="')! -pane3.send_command('htop')! +pane3.send_command('echo "=== Hero Web Server ==="')! +pane3.send_command('./cli/compile.vsh && /Users/mahmoud/hero/bin/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! ===') @@ -127,8 +284,9 @@ for i, mut pane in window.panes { memory_mb := f64(stats.memory_bytes) / (1024.0 * 1024.0) service_name := match i { 0 { 'Python Server' } - 1 { 'Counter' } - 2 { 'CPU Monitor' } + 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') @@ -145,8 +303,9 @@ println(' Or programmatically:') for i, pane in window.panes { service_name := match i { 0 { 'Python Server' } - 1 { 'Counter' } - 2 { 'CPU Monitor' } + 1 { 'Counter Service' } + 2 { 'Hero Web' } + 3 { 'CPU Monitor' } else { 'Unknown' } } println(' # Resize ${service_name} pane:') @@ -156,10 +315,50 @@ for i, pane in window.panes { 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') + 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') -window.run_ttyd(ttyd_port) or { println('Failed to start ttyd: ${err}') } + window.run_ttyd(ttyd_port) or { println('Failed to start ttyd: ${err}') } +} + +// Main execution with argument handling +if os.args.len > 1 { + command := os.args[1] + match command { + '-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() or { + eprintln('Error starting dashboard: ${err}') + exit(1) + } +} From a62147d7ccfd81c1fa03c96d74ea62c63a24348d Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Mon, 25 Aug 2025 11:52:13 +0300 Subject: [PATCH 09/10] feat: add editable ttyd dashboard mode - Implement `-editable` CLI argument - Configure ttyd for read/write access - Introduce `TtydArgs` struct for ttyd parameters - Update help message with ttyd modes - Streamline Hero Web startup command --- examples/tmux/server_dashboard.vsh | 439 +++++++++++++++-------------- lib/osal/tmux/tmux_session.v | 21 +- lib/osal/tmux/tmux_window.v | 28 +- 3 files changed, 273 insertions(+), 215 deletions(-) diff --git a/examples/tmux/server_dashboard.vsh b/examples/tmux/server_dashboard.vsh index c5a5fafd..d0a9d5e9 100755 --- a/examples/tmux/server_dashboard.vsh +++ b/examples/tmux/server_dashboard.vsh @@ -15,11 +15,12 @@ const ttyd_port = 7890 fn show_help() { println('=== Tmux Server Dashboard ===') println('Usage:') - println(' ${os.args[0]} # Start the dashboard') - 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(' ${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})') @@ -27,6 +28,10 @@ fn show_help() { 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() ! { @@ -87,25 +92,25 @@ fn show_status() ! { } 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') + // 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') } @@ -139,7 +144,7 @@ fn restart_dashboard() ! { start_dashboard()! } -fn 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})') @@ -148,172 +153,172 @@ fn start_dashboard() ! { println(' 4. CPU Monitor (htop)') println('') -// Initialize tmux -mut t := tmux.new()! + // Initialize tmux + mut t := tmux.new()! -if !t.is_running()! { - println('Starting tmux server...') - t.start()! -} + 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)! -} + // 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 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)! + // 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()! + // Wait for initial setup + time.sleep(500 * time.millisecond) + t.scan()! -println('\n=== Setting up 4-pane layout ===') + println('\n=== Setting up 4-pane layout ===') -// Get the main window -window = session.window_get(name: window_name)! + // 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')! + // 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 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) + // Split left pane vertically (top-left and bottom-left) + println('2. Splitting left pane vertically...') 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('./cli/compile.vsh && /Users/mahmoud/hero/bin/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 + 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()! } - 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' } + // 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(' # 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}') @@ -321,44 +326,64 @@ for i, pane in window.panes { println('To stop all services: tmux kill-session -t ${session_name}') println('Running the browser-based dashboard: TTYD') - window.run_ttyd(ttyd_port) or { println('Failed to start ttyd: ${err}') } + 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}') + } } -// Main execution with argument handling -if os.args.len > 1 { - command := os.args[1] - match command { - '-down' { - stop_dashboard() or { - eprintln('Error stopping dashboard: ${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) } } - '-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() + } else { + // No arguments - start the dashboard + start_dashboard_with_mode(ttyd_editable) or { + eprintln('Error starting dashboard: ${err}') exit(1) } } -} else { - // No arguments - start the dashboard - start_dashboard() or { - eprintln('Error starting dashboard: ${err}') - exit(1) - } } diff --git a/lib/osal/tmux/tmux_session.v b/lib/osal/tmux/tmux_session.v index 9c964076..2e7a8fde 100644 --- a/lib/osal/tmux/tmux_session.v +++ b/lib/osal/tmux/tmux_session.v @@ -260,14 +260,27 @@ pub fn (mut s Session) stop() ! { } // Run ttyd for this session so it can be accessed in the browser -pub fn (mut s Session) run_ttyd(port int) ! { +pub fn (mut s Session) run_ttyd(args TtydArgs) ! { target := '${s.name}' - cmd := 'nohup ttyd -p ${port} tmux attach -t ${target} >/dev/null 2>&1 &' + + // 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 ${port} for session ${s.name}') + return error('Failed to start ttyd on port ${args.port} for session ${s.name}') } - println('ttyd started for session ${s.name} at http://localhost:${port}') + 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)! } diff --git a/lib/osal/tmux/tmux_window.v b/lib/osal/tmux/tmux_window.v index acc9edbb..6119ca93 100644 --- a/lib/osal/tmux/tmux_window.v +++ b/lib/osal/tmux/tmux_window.v @@ -257,15 +257,35 @@ pub fn (mut w Window) pane_split_vertical(cmd string) !&Pane { return w.pane_split(cmd: cmd, horizontal: false) } +@[params] +pub struct TtydArgs { +pub mut: + port int + editable bool // if true, allows write access to the terminal +} + // Run ttyd for this window so it can be accessed in the browser -pub fn (mut w Window) run_ttyd(port int) ! { +pub fn (mut w Window) run_ttyd(args TtydArgs) ! { target := '${w.session.name}:@${w.id}' - cmd := 'nohup ttyd -p ${port} tmux attach -t ${target} >/dev/null 2>&1 &' + + // 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 ${port} for window ${w.name}') + return error('Failed to start ttyd on port ${args.port} for window ${w.name}') } - println('ttyd started for window ${w.name} at http://localhost:${port}') + mode_str := if args.editable { 'editable' } else { 'read-only' } + println('ttyd started for window ${w.name} at http://localhost:${args.port} (${mode_str} mode)') +} + +// Backward compatibility method - runs ttyd in read-only mode +pub fn (mut w Window) run_ttyd_readonly(port int) ! { + w.run_ttyd(port: port, editable: false)! } From ffa5447e6f03f7f05a787094f25bc04a0edeb677 Mon Sep 17 00:00:00 2001 From: despiegk Date: Mon, 25 Aug 2025 12:04:40 +0200 Subject: [PATCH 10/10] ... --- lib/osal/netns/instructions.md | 33 ++++++++++++ lib/virt/podman/factory.v | 92 ++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 lib/osal/netns/instructions.md create mode 100644 lib/virt/podman/factory.v diff --git a/lib/osal/netns/instructions.md b/lib/osal/netns/instructions.md new file mode 100644 index 00000000..bd1712c0 --- /dev/null +++ b/lib/osal/netns/instructions.md @@ -0,0 +1,33 @@ +### 1. **Network namespaces** + +* Each namespace can have its own interfaces, routing table, firewall rules, etc. +* You can move the user’s processes into a network namespace that only has access to a given bridge. + +Example: + +```bash +# Create a new netns for user "alice" +ip netns add alice + +# Add a veth pair +ip link add veth-alice type veth peer name veth-alice-br + +# Attach one side to the bridge +ip link set veth-alice-br master br0 +ip link set veth-alice-br up + +# Move the other side into the netns +ip link set veth-alice netns alice + +# Configure inside namespace +ip netns exec alice ip addr add 192.168.100.2/24 dev veth-alice +ip netns exec alice ip link set veth-alice up +ip netns exec alice ip route add default via 192.168.100.1 + +# Now run a shell in alice’s namespace +sudo -u alice ip netns exec alice bash +``` + +Now all processes run by `alice` will use only that veth → bridge → network. + + diff --git a/lib/virt/podman/factory.v b/lib/virt/podman/factory.v new file mode 100644 index 00000000..c41c9db8 --- /dev/null +++ b/lib/virt/podman/factory.v @@ -0,0 +1,92 @@ +module herocontainers + +import freeflowuniverse.herolib.osal.core as osal { exec } +import freeflowuniverse.herolib.core +import freeflowuniverse.herolib.installers.virt.podman as podman_installer + +@[heap] +pub struct PodmanFactory { +pub mut: + // sshkeys_allowed []string // all keys here have access over ssh into the machine, when ssh enabled + images []Image + containers []Container + buildpath string + // cache bool = true + // push bool + // platform []BuildPlatformType // used to build + // registries []BAHRegistry // one or more supported BAHRegistries + prefix string +} + + +@[params] +pub struct NewArgs { +pub mut: + install bool = true + reset bool + herocompile bool +} + + + if args.install { + mut podman_installer0 := podman_installer.get()! + podman_installer0.install()! + } + + +fn (mut e PodmanFactory) init() ! { + if e.buildpath == '' { + e.buildpath = '/tmp/builder' + exec(cmd: 'mkdir -p ${e.buildpath}', stdout: false)! + } + e.load()! +} + +// reload the state from system +pub fn (mut e PodmanFactory) load() ! { + e.builders_load()! + e.images_load()! + e.containers_load()! +} + +// reset all images & containers, CAREFUL! +pub fn (mut e PodmanFactory) reset_all() ! { + e.load()! + for mut container in e.containers.clone() { + container.delete()! + } + for mut image in e.images.clone() { + image.delete(true)! + } + exec(cmd: 'podman rm -a -f', stdout: false)! + exec(cmd: 'podman rmi -a -f', stdout: false)! + e.builders_delete_all()! + osal.done_reset()! + if core.platform()! == core.PlatformType.arch { + exec(cmd: 'systemctl status podman.socket', stdout: false)! + } + e.load()! +} + +// Get free port +pub fn (mut e PodmanFactory) get_free_port() ?int { + mut used_ports := []int{} + mut range := []int{} + + for c in e.containers { + for p in c.forwarded_ports { + used_ports << p.split(':')[0].int() + } + } + + for i in 20000 .. 40000 { + if i !in used_ports { + range << i + } + } + // arrays.shuffle(mut range, 0) + if range.len == 0 { + return none + } + return range[0] +}