Merge pull request #117 from freeflowuniverse/development_tmux

Improve tmux API consistency and formatting
This commit is contained in:
Omdanii
2025-08-25 16:36:25 +03:00
committed by GitHub
18 changed files with 1587 additions and 83 deletions

View File

@@ -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!')
}

View File

@@ -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')

View File

@@ -0,0 +1,389 @@
#!/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'
const window_name = 'dashboard'
const python_port = 8000
const ttyd_port = 7890
// Command line argument handling
fn show_help() {
println('=== Tmux Server Dashboard ===')
println('Usage:')
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})')
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})')
println('')
println('ttyd modes:')
println(' Default: read-only access to terminal')
println(' -editable: allows writing/editing in the terminal')
}
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_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})')
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()!
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 4-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()!
}
// 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('\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')
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}')
}
}
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)
}
}
} else {
// No arguments - start the dashboard
start_dashboard_with_mode(ttyd_editable) or {
eprintln('Error starting dashboard: ${err}')
exit(1)
}
}
}

120
examples/tmux/tmux.vsh Executable file
View File

@@ -0,0 +1,120 @@
#!/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
const cpu_precision = 1
const 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}')
}

View File

@@ -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.')

170
examples/tmux/tmux_panes.vsh Executable file
View File

@@ -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')

View File

@@ -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()!

View File

@@ -0,0 +1,33 @@
### 1. **Network namespaces**
* Each namespace can have its own interfaces, routing table, firewall rules, etc.
* You can move the users 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 alices namespace
sudo -u alice ip netns exec alice bash
```
Now all processes run by `alice` will use only that veth → bridge → network.

View File

@@ -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}')
}

View File

@@ -96,3 +96,59 @@ pub fn (mut agent SSHAgent) verify_key_access(mut node builder.Node, key_name st
return result.contains('SSH key verification successful')
}
// 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}')
}

View File

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

82
lib/osal/sshagent/play.v Normal file
View File

@@ -0,0 +1,82 @@
module sshagent
import freeflowuniverse.herolib.core.playbook { PlayBook }
pub fn play(mut plbook PlayBook) ! {
if !plbook.exists(filter: 'sshagent.') {
return
}
// Get or create a single SSH agent instance
mut agent := new_single()!
// Process sshagent.check actions
mut check_actions := plbook.find(filter: 'sshagent.check')!
for mut action in check_actions {
agent_check(mut agent)!
action.done = true
}
// Process sshagent.sshkey_create actions
mut create_actions := plbook.find(filter: 'sshagent.sshkey_create')!
for mut action in create_actions {
mut p := action.params
name := p.get('name')!
passphrase := p.get_default('passphrase', '')!
sshkey_create(mut agent, name, passphrase)!
action.done = true
}
// Process sshagent.sshkey_delete actions
mut delete_actions := plbook.find(filter: 'sshagent.sshkey_delete')!
for mut action in delete_actions {
mut p := action.params
name := p.get('name')!
sshkey_delete(mut agent, name)!
action.done = true
}
// Process sshagent.sshkey_load actions
mut load_actions := plbook.find(filter: 'sshagent.sshkey_load')!
for mut action in load_actions {
mut p := action.params
name := p.get('name')!
sshkey_load(mut agent, name)!
action.done = true
}
// Process sshagent.sshkey_check actions
mut check_key_actions := plbook.find(filter: 'sshagent.sshkey_check')!
for mut action in check_key_actions {
mut p := action.params
name := p.get('name')!
sshkey_check(mut agent, name)!
action.done = true
}
// Process sshagent.remote_copy actions
mut remote_copy_actions := plbook.find(filter: 'sshagent.remote_copy')!
for mut action in remote_copy_actions {
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
}
// Process sshagent.remote_auth actions
mut remote_auth_actions := plbook.find(filter: 'sshagent.remote_auth')!
for mut action in remote_auth_actions {
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
}
}

View File

@@ -177,7 +177,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
}

View File

@@ -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
}
}
@@ -54,8 +150,14 @@ pub mut:
offset int
}
pub fn (mut p Pane) logs_get_new(reset bool) ![]TMuxLogEntry {
if reset {
pub struct LogsGetArgs {
pub mut:
reset bool
}
// 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
@@ -141,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)!
}

View File

@@ -5,9 +5,30 @@ 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('|')
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]
@@ -16,6 +37,11 @@ fn (mut t Tmux) scan_add(line string) !&Pane {
pane_pid := line_arr[5]
pane_start_command := line_arr[6] or { '' }
// Skip if window name is empty
if window_name.len == 0 {
return error('Window name is empty in line: ${line}')
}
wid := (window_id.replace('@', '')).int()
pid := (pane_id.replace('%', '')).int()
@@ -71,7 +97,7 @@ pub fn (mut t Tmux) scan() ! {
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}')

View File

@@ -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)
@@ -115,7 +118,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}')
}
@@ -127,7 +130,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{}
@@ -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
}
@@ -252,3 +258,29 @@ 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(args TtydArgs) ! {
target := '${s.name}'
// Add -W flag for write access if editable mode is enabled
mut ttyd_flags := '-p ${args.port}'
if args.editable {
ttyd_flags += ' -W'
}
cmd := 'nohup ttyd ${ttyd_flags} tmux attach -t ${target} >/dev/null 2>&1 &'
code := os.system(cmd)
if code != 0 {
return error('Failed to start ttyd on port ${args.port} for session ${s.name}')
}
mode_str := if args.editable { 'editable' } else { 'read-only' }
println('ttyd started for session ${s.name} at http://localhost:${args.port} (${mode_str} mode)')
}
// Backward compatibility method - runs ttyd in read-only mode
pub fn (mut s Session) run_ttyd_readonly(port int) ! {
s.run_ttyd(port: port, editable: false)!
}

View File

@@ -175,3 +175,117 @@ pub fn (mut w Window) pane_active() ?&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)
}
@[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(args TtydArgs) ! {
target := '${w.session.name}:@${w.id}'
// Add -W flag for write access if editable mode is enabled
mut ttyd_flags := '-p ${args.port}'
if args.editable {
ttyd_flags += ' -W'
}
cmd := 'nohup ttyd ${ttyd_flags} tmux attach -t ${target} >/dev/null 2>&1 &'
code := os.system(cmd)
if code != 0 {
return error('Failed to start ttyd on port ${args.port} for window ${w.name}')
}
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)!
}

92
lib/virt/podman/factory.v Normal file
View File

@@ -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<int>(mut range, 0)
if range.len == 0 {
return none
}
return range[0]
}