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
This commit is contained in:
Mahmoud-Emad
2025-08-24 16:31:04 +03:00
parent 810cbda176
commit 117c9ac67c
4 changed files with 199 additions and 214 deletions

View File

@@ -1,20 +1,20 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run #!/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.tmux
mut t := tmux.new()! 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 // if !t.is_running()! {
// t.window_new(session_name: 'main', name: 'test', cmd: 'mc', reset: true)! // 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) println(t)

View File

@@ -14,7 +14,6 @@ pub mut:
sessionid string // unique link to job sessionid string // unique link to job
} }
// get session (session has windows) . // get session (session has windows) .
// returns none if not found // returns none if not found
pub fn (mut t Tmux) session_get(name_ string) !&Session { pub fn (mut t Tmux) session_get(name_ string) !&Session {
@@ -56,8 +55,6 @@ pub mut:
reset bool reset bool
} }
// create session, if reset will re-create // create session, if reset will re-create
pub fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session { pub fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session {
name := texttools.name_fix(args.name) name := texttools.name_fix(args.name)
@@ -83,7 +80,6 @@ pub fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session {
return s return s
} }
@[params] @[params]
pub struct TmuxNewArgs { pub struct TmuxNewArgs {
sessionid string sessionid string
@@ -116,17 +112,16 @@ pub fn (mut t Tmux) window_new(args WindowNewArgs) !&Window {
} else { } else {
t.session_create(name: args.session_name)! t.session_create(name: args.session_name)!
} }
// Create window in session // Create window in session
return session.window_new( return session.window_new(
name: args.name name: args.name
cmd: args.cmd cmd: args.cmd
env: args.env env: args.env
reset: args.reset reset: args.reset
)! )!
} }
pub fn (mut t Tmux) stop() ! { pub fn (mut t Tmux) stop() ! {
$if debug { $if debug {
console.print_debug('Stopping tmux...') console.print_debug('Stopping tmux...')
@@ -156,7 +151,6 @@ pub fn (mut t Tmux) start() ! {
t.scan()! t.scan()!
} }
// print list of tmux sessions // print list of tmux sessions
pub fn (mut t Tmux) list_print() { pub fn (mut t Tmux) list_print() {
// os.log('TMUX - Start listing ....') // os.log('TMUX - Start listing ....')

View File

@@ -10,141 +10,140 @@ import freeflowuniverse.herolib.ui.console
@[heap] @[heap]
struct Pane { struct Pane {
pub mut: pub mut:
window &Window @[str: skip] window &Window @[str: skip]
id int // pane id (e.g., %1, %2) id int // pane id (e.g., %1, %2)
pid int // process id pid int // process id
active bool // is this the active pane active bool // is this the active pane
cmd string // command running in pane cmd string // command running in pane
env map[string]string env map[string]string
created_at time.Time created_at time.Time
last_output_offset int // for tracking new logs last_output_offset int // for tracking new logs
} }
pub fn (mut p Pane) stats() !ProcessStats { pub fn (mut p Pane) stats() !ProcessStats {
if p.pid == 0 { if p.pid == 0 {
return ProcessStats{} return ProcessStats{}
} }
// Use ps command to get CPU and memory stats // Use ps command to get CPU and memory stats
cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss --no-headers' cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss --no-headers'
result := osal.execute_silent(cmd) or { result := osal.execute_silent(cmd) or {
return error('Cannot get stats for PID ${p.pid}: ${err}') return error('Cannot get stats for PID ${p.pid}: ${err}')
} }
if result.trim_space() == '' { if result.trim_space() == '' {
return error('Process ${p.pid} not found') return error('Process ${p.pid} not found')
} }
parts := result.trim_space().split_any(' \t').filter(it != '') parts := result.trim_space().split_any(' \t').filter(it != '')
if parts.len < 3 { if parts.len < 3 {
return error('Invalid ps output: ${result}') return error('Invalid ps output: ${result}')
} }
return ProcessStats{ return ProcessStats{
cpu_percent: parts[0].f64() cpu_percent: parts[0].f64()
memory_percent: parts[1].f64() memory_percent: parts[1].f64()
memory_bytes: parts[2].u64() * 1024 // ps returns KB, convert to bytes memory_bytes: parts[2].u64() * 1024 // ps returns KB, convert to bytes
} }
} }
pub struct TMuxLogEntry { pub struct TMuxLogEntry {
pub mut: pub mut:
content string content string
timestamp time.Time timestamp time.Time
offset int offset int
} }
pub fn (mut p Pane) logs_get_new(reset bool) ![]TMuxLogEntry { pub struct LogsGetArgs {
pub mut:
reset bool
}
if reset{ // get new logs since last call
p.last_output_offset = 0 pub fn (mut p Pane) logs_get_new(args LogsGetArgs) ![]TMuxLogEntry {
} if args.reset {
// Capture pane content with line numbers p.last_output_offset = 0
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 { // Capture pane content with line numbers
return error('Cannot capture pane output: ${err}') 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() lines := result.split_into_lines()
mut entries := []TMuxLogEntry{} mut entries := []TMuxLogEntry{}
mut i:= 0 mut i := 0
for line in lines { for line in lines {
if line.trim_space() != '' { if line.trim_space() != '' {
entries << TMuxLogEntry{ entries << TMuxLogEntry{
content: line content: line
timestamp: time.now() timestamp: time.now()
offset: p.last_output_offset + i + 1 offset: p.last_output_offset + i + 1
} }
} }
} }
// Update offset to avoid duplicates next time // Update offset to avoid duplicates next time
if entries.len > 0 { if entries.len > 0 {
p.last_output_offset = entries.last().offset p.last_output_offset = entries.last().offset
} }
return entries return entries
} }
pub fn (mut p Pane) exit_status() !ProcessStatus { pub fn (mut p Pane) exit_status() !ProcessStatus {
// Get the last few lines to see if there's an exit status // Get the last few lines to see if there's an exit status
logs := p.logs_all()! logs := p.logs_all()!
lines := logs.split_into_lines() lines := logs.split_into_lines()
// Look for shell prompt indicating command finished // Look for shell prompt indicating command finished
for line in lines.reverse() { for line in lines.reverse() {
line_clean := line.trim_space() line_clean := line.trim_space()
if line_clean.contains('$') || line_clean.contains('#') || line_clean.contains('>') { if line_clean.contains('$') || line_clean.contains('#') || line_clean.contains('>') {
// Found shell prompt, command likely finished // Found shell prompt, command likely finished
// Could also check for specific exit codes in history // Could also check for specific exit codes in history
return .finished_ok return .finished_ok
} }
} }
return .finished_error return .finished_error
} }
pub fn (mut p Pane) logs_all() !string { 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' cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S -2000 -p'
return osal.execute_silent(cmd) or { return osal.execute_silent(cmd) or { error('Cannot capture pane output: ${err}') }
error('Cannot capture pane output: ${err}')
}
} }
// Fix the output_wait method to use correct method name // Fix the output_wait method to use correct method name
pub fn (mut p Pane) output_wait(c_ string, timeoutsec int) ! { pub fn (mut p Pane) output_wait(c_ string, timeoutsec int) ! {
mut t := ourtime.now() mut t := ourtime.now()
start := t.unix() start := t.unix()
c := c_.replace('\n', '') c := c_.replace('\n', '')
for i in 0 .. 2000 { for i in 0 .. 2000 {
entries := p.logs_get_new(reset: false)! entries := p.logs_get_new(reset: false)!
for entry in entries { for entry in entries {
if entry.content.replace('\n', '').contains(c) { if entry.content.replace('\n', '').contains(c) {
return return
} }
} }
mut t2 := ourtime.now() mut t2 := ourtime.now()
if t2.unix() > start + timeoutsec { if t2.unix() > start + timeoutsec {
return error('timeout on output wait for tmux.\n${p} .\nwaiting for:\n${c}') return error('timeout on output wait for tmux.\n${p} .\nwaiting for:\n${c}')
} }
time.sleep(100 * time.millisecond) time.sleep(100 * time.millisecond)
} }
} }
// Get process information for this pane and all its children // Get process information for this pane and all its children
pub fn (mut p Pane) processinfo() !osal.ProcessMap { pub fn (mut p Pane) processinfo() !osal.ProcessMap {
if p.pid == 0 { if p.pid == 0 {
return error('Pane has no associated process (pid is 0)') return error('Pane has no associated process (pid is 0)')
} }
return osal.processinfo_with_children(p.pid)! return osal.processinfo_with_children(p.pid)!
} }
// Get process information for just this pane's main process // Get process information for just this pane's main process
pub fn (mut p Pane) processinfo_main() !osal.ProcessInfo { pub fn (mut p Pane) processinfo_main() !osal.ProcessInfo {
if p.pid == 0 { if p.pid == 0 {
return error('Pane has no associated process (pid is 0)') return error('Pane has no associated process (pid is 0)')
} }
return osal.processinfo_get(p.pid)! return osal.processinfo_get(p.pid)!
} }

View File

@@ -21,87 +21,86 @@ pub mut:
env map[string]string env map[string]string
reset bool reset bool
} }
@[params] @[params]
pub struct WindowGetArgs { pub struct WindowGetArgs {
pub mut: pub mut:
name string name string
id int id int
} }
pub fn (mut s Session) create() ! { pub fn (mut s Session) create() ! {
// Check if session already exists // Check if session already exists
cmd_check := "tmux has-session -t ${s.name}" cmd_check := 'tmux has-session -t ${s.name}'
check_result := osal.exec(cmd: cmd_check, stdout: false, ignore_error: true) or { check_result := osal.exec(cmd: cmd_check, stdout: false, ignore_error: true) or {
// Session doesn't exist, this is expected // Session doesn't exist, this is expected
osal.Job{} osal.Job{}
} }
if check_result.exit_code == 0 { if check_result.exit_code == 0 {
return error('duplicate session: ${s.name}') return error('duplicate session: ${s.name}')
} }
// Create new session // Create new session
cmd := "tmux new-session -d -s ${s.name}" cmd := 'tmux new-session -d -s ${s.name}'
osal.exec(cmd: cmd, stdout: false, name: 'tmux_session_create') or { osal.exec(cmd: cmd, stdout: false, name: 'tmux_session_create') or {
return error("Can't create session ${s.name}: ${err}") return error("Can't create session ${s.name}: ${err}")
} }
} }
//load info from reality // load info from reality
pub fn (mut s Session) scan() ! { pub fn (mut s Session) scan() ! {
// Get current windows from tmux for this session // Get current windows from tmux for this session
cmd := "tmux list-windows -t ${s.name} -F '#{window_name}|#{window_id}|#{window_active}'" cmd := "tmux list-windows -t ${s.name} -F '#{window_name}|#{window_id}|#{window_active}'"
result := osal.execute_silent(cmd) or { result := osal.execute_silent(cmd) or {
if err.msg().contains('session not found') { if err.msg().contains('session not found') {
return // Session doesn't exist anymore return
} }
return error('Cannot list windows for session ${s.name}: ${err}') 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)
}
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) // window_name is the name of the window in session main (will always be called session main)
// cmd to execute e.g. bash file // cmd to execute e.g. bash file
@@ -116,7 +115,7 @@ pub fn (mut s Session) scan() ! {
// reset bool // reset bool
// } // }
// ``` // ```
pub fn (mut s Session) window_new(args WindowArgs) !Window { pub fn (mut s Session) window_new(args WindowArgs) !&Window {
$if debug { $if debug {
console.print_header(' start window: \n${args}') 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}') return error('cannot create new window it already exists, window ${namel} in session:${s.name}')
} }
} }
mut w := Window{ mut w := &Window{
session: &s session: &s
name: namel name: namel
panes: []&Pane{} panes: []&Pane{}
@@ -139,14 +138,10 @@ pub fn (mut s Session) window_new(args WindowArgs) !Window {
// Create the window with the specified command // Create the window with the specified command
w.create(args.cmd)! w.create(args.cmd)!
s.scan()! s.scan()!
return w return w
} }
// get all windows as found in a session // get all windows as found in a session
pub fn (mut s Session) windows_get() []&Window { pub fn (mut s Session) windows_get() []&Window {
mut res := []&Window{} mut res := []&Window{}
@@ -179,14 +174,14 @@ pub fn (mut s Session) str() string {
} }
pub fn (mut s Session) stats() !ProcessStats { pub fn (mut s Session) stats() !ProcessStats {
mut total := ProcessStats{} mut total := ProcessStats{}
for mut window in s.windows { for mut window in s.windows {
stats := window.stats() or { continue } stats := window.stats() or { continue }
total.cpu_percent += stats.cpu_percent total.cpu_percent += stats.cpu_percent
total.memory_bytes += stats.memory_bytes total.memory_bytes += stats.memory_bytes
total.memory_percent += stats.memory_percent total.memory_percent += stats.memory_percent
} }
return total return total
} }
// pub fn (mut s Session) activate()! { // 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 { fn (mut s Session) window_exist(args_ WindowGetArgs) bool {
mut args := args_ mut args := args_
s.window_get(args) or { return false } 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 s.windows.delete(i) // i is now the one in the list which needs to be removed
} }
pub fn (mut s Session) restart() ! { pub fn (mut s Session) restart() ! {
s.stop()! s.stop()!
s.create()! s.create()!
@@ -259,4 +251,4 @@ pub fn (mut s Session) stop() ! {
osal.execute_silent('tmux kill-session -t ${s.name}') or { 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}") return error("Can't delete session ${s.name} - This may happen when session is not found: ${err}")
} }
} }