This commit is contained in:
2025-08-24 14:41:12 +02:00
parent 9f39481cb4
commit e7a36f47e8
7 changed files with 360 additions and 374 deletions

View File

@@ -4,7 +4,11 @@
import freeflowuniverse.herolib.osal.tmux
mut t := tmux.new()!
t.session_delete('main')!
println(t)
if !t.is_running()! {
t.start()!
}
if t.session_exist('main') {
t.session_delete('main')!
}
t.window_new(name: 'test', cmd: 'mc', reset: true)!
println(t)

View File

@@ -13,19 +13,76 @@ pub mut:
sessionid string // unique link to job
}
@[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
// get session (session has windows) .
// returns none if not found
pub fn (mut t Tmux) session_get(name_ string) !&Session {
name := texttools.name_fix(name_)
for s in t.sessions {
if s.name == name {
return s
}
}
return error('Can not find session with name: \'${name_}\', out of loaded sessions.')
}
pub fn (mut t Tmux) session_exist(name_ string) bool {
name := texttools.name_fix(name_)
t.session_get(name) or { return false }
return true
}
pub fn (mut t Tmux) session_delete(name_ string) ! {
if !(t.session_exist(name_)) {
return
}
name := texttools.name_fix(name_)
mut i := 0
for mut s in t.sessions {
if s.name == name {
s.stop()!
break
}
i += 1
}
t.sessions.delete(i)
}
@[params]
pub struct SessionCreateArgs {
pub mut:
name string @[required]
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)
if !(t.session_exist(name)) {
$if debug {
console.print_header(' tmux - create session: ${args}')
}
mut s2 := Session{
tmux: t // reference back
name: name
}
s2.create()!
t.sessions << &s2
}
mut s := t.session_get(name)!
if args.reset {
$if debug {
console.print_header(' tmux - session ${name} will be restarted.')
}
s.restart()!
}
t.scan()!
return s
}
@[params]
pub struct TmuxNewArgs {
sessionid string
@@ -41,49 +98,6 @@ pub fn new(args TmuxNewArgs) !Tmux {
return t
}
// // loads tmux session, populate the object
// pub fn (mut tmux Tmux) load() ! {
// // isrunning := tmux.is_running()!
// // if !isrunning {
// // tmux.start()!
// // }
// // console.print_debug("SCAN")
// tmux.scan()!
// }
pub struct ProcessStats {
pub mut:
cpu_percent f64
memory_bytes u64
memory_percent f64
}
pub fn (mut p Pane) get_stats() !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}')
}
if result.trim() == '' {
return error('Process ${p.pid} not found')
}
parts := result.trim().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
}
}
pub fn (mut t Tmux) stop() ! {
$if debug {
@@ -128,73 +142,6 @@ pub mut:
offset int
}
pub fn (mut p Pane) get_new_logs() ![]LogEntry {
// 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}')
}
}
pub fn (mut p Pane) check_process_status() !ProcessStatus {
}
if result.trim() == '' {
// Process not found, check exit status from shell history or tmux
return p.check_exit_status() or { .finished_error }
}
return .running
}
fn (mut p Pane) check_exit_status() !ProcessStatus {
// Get the last few lines to see if there's an exit status
logs := p.get_all_logs()!
lines := logs.split_into_lines()
// Look for shell prompt indicating command finished
for line in lines.reverse() {
line_clean := line.trim()
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
}
lines := result.split_into_lines()
mut entries := []LogEntry{}
for i, line in lines {
if line.trim() != '' {
entries << LogEntry{
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) get_all_logs() !string {
cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S -1000 -p'
return osal.execute_silent(cmd) or {
error('Cannot capture pane output: ${err}')
}
}
// print list of tmux sessions
pub fn (mut t Tmux) list_print() {
// os.log('TMUX - Start listing ....')

137
lib/osal/tmux/tmux_pane.v Normal file
View File

@@ -0,0 +1,137 @@
module tmux
import freeflowuniverse.herolib.osal.core as osal
// import freeflowuniverse.herolib.session
import os
import time
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
}
pub fn (mut p Pane) stats() !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}')
}
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}')
}
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 LogEntry {
pub mut:
content string
timestamp time.Time
offset int
}
pub fn (mut p Pane) logs_get_new(reset bool) ![]LogEntry {
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}')
}
lines := result.split_into_lines()
mut entries := []LogEntry{}
mut i:= 0
for line in lines {
if line.trim_space() != '' {
entries << LogEntry{
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()
// 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_get_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}')
}
}
pub fn (mut w Pane) output_wait(c_ string, timeoutsec int) ! {
mut t := ourtime.now()
start := t.unix()
c := c_.replace('\n', '')
for i in 0 .. 2000 {
os := w.logs_get_new()!.map(it.content)
// console.print_debug(o)
// $if debug {
// console.print_debug(" - tmux ${w.name}: wait for: '${c}'")
// }
// need to replace \n because can be wrapped because of size of pane
for o in os{
if o.replace('\n', '').contains(c) {
return
}
}
mut t2 := ourtime.now()
if t2.unix() > start + timeoutsec {
return error('timeout on output wait for tmux.\n${w} .\nwaiting for:\n${c}')
}
time.sleep(100 * time.millisecond)
}
}

View File

@@ -0,0 +1,21 @@
module tmux
pub struct ProcessStats {
pub mut:
cpu_percent f64
memory_bytes u64
memory_percent f64
}
enum ProcessStatus {
running
finished_ok
finished_error
not_found
}

View File

@@ -64,6 +64,7 @@ fn (mut t Tmux) scan_add(line string) !&Pane {
}
// 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 ...
pub fn (mut t Tmux) scan() ! {
// os.log('TMUX - Scanning ....')

View File

@@ -13,72 +13,58 @@ pub mut:
name string
}
// get session (session has windows) .
// returns none if not found
pub fn (mut t Tmux) session_get(name_ string) !&Session {
name := texttools.name_fix(name_)
for s in t.sessions {
if s.name == name {
return s
}
}
return error('Can not find session with name: \'${name_}\', out of loaded sessions.')
}
pub fn (mut t Tmux) session_exist(name_ string) bool {
name := texttools.name_fix(name_)
t.session_get(name) or { return false }
return true
}
pub fn (mut t Tmux) session_delete(name_ string) ! {
if !(t.session_exist(name_)) {
return
}
name := texttools.name_fix(name_)
mut i := 0
for mut s in t.sessions {
if s.name == name {
s.stop()!
break
}
i += 1
}
t.sessions.delete(i)
}
@[params]
pub struct SessionCreateArgs {
pub struct WindowArgs {
pub mut:
name string @[required]
name string
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)
if !(t.session_exist(name)) {
$if debug {
console.print_header(' tmux - create session: ${args}')
}
mut s2 := Session{
tmux: t // reference back
name: name
}
s2.create()!
t.sessions << &s2
}
mut s := t.session_get(name)!
if args.reset {
$if debug {
console.print_header(' tmux - session ${name} will be restarted.')
}
s.restart()!
}
t.scan()!
return s
pub fn (mut w Session) scan() ! {
//TODO: here needs to be the code to check reality and update the windows
}
// window_name is the name of the window in session main (will always be called session main)
// cmd to execute e.g. bash file
// environment arguments to use
// reset, if reset it will create window even if it does already exist, will destroy it
// ```
// struct WindowArgs {
// pub mut:
// name string
// cmd string
// env map[string]string
// reset bool
// }
// ```
pub fn (mut s Session) window_new(args WindowArgs) !Window {
$if debug {
console.print_header(' start window: \n${args}')
}
namel := texttools.name_fix(args.name)
if s.window_exist(name: namel) {
if args.reset {
s.window_delete(name: namel)!
} else {
return error('cannot create new window it already exists, window ${namel} in session:${s.name}')
}
}
mut w := Window{
session: &s
name: namel
panes: []&Pane{}
env: args.env
}
s.windows << &w
w.create(args.cmd)!
// After creation, scan to populate panes
s.tmux.scan()!
return w
}
pub fn (mut s Session) create() ! {
res_opt := "-P -F '#\{window_id\}'"
cmd := "tmux new-session ${res_opt} -d -s ${s.name} 'sh'"
@@ -96,16 +82,6 @@ pub fn (mut s Session) create() ! {
}
}
pub fn (mut s Session) restart() ! {
s.stop()!
s.create()!
}
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}")
}
}
// get all windows as found in a session
pub fn (mut s Session) windows_get() []&Window {
@@ -118,11 +94,11 @@ pub fn (mut s Session) windows_get() []&Window {
}
// List windows in a session
pub fn (mut s Session) list_windows() []&Window {
pub fn (mut s Session) window_list() []&Window {
return s.windows
}
pub fn (mut s Session) windownames_get() []string {
pub fn (mut s Session) window_names() []string {
mut res := []string{}
for _, window in s.windows {
res << window.name
@@ -138,10 +114,10 @@ pub fn (mut s Session) str() string {
return out
}
pub fn (mut s Session) get_total_stats() !ProcessStats {
pub fn (mut s Session) stats() !ProcessStats {
mut total := ProcessStats{}
for mut window in s.windows {
stats := window.get_total_stats() or { continue }
stats := window.stats() or { continue }
total.cpu_percent += stats.cpu_percent
total.memory_bytes += stats.memory_bytes
total.memory_percent += stats.memory_percent
@@ -167,3 +143,56 @@ pub fn (mut s Session) get_total_stats() !ProcessStats {
// os.log('SESSION - Session: $s.name already activate ')
// }
// }
fn (mut s Session) window_exist(args_ WindowGetArgs) bool {
mut args := args_
s.window_get(args) or { return false }
return true
}
pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window {
mut args := args_
args.name = texttools.name_fix(args.name)
for w in s.windows {
if w.name == args.name {
if (args.id > 0 && w.id == args.id) || args.id == 0 {
return w
}
}
}
return error('Cannot find window ${args.name} in session:${s.name}')
}
pub fn (mut s Session) window_delete(args_ WindowGetArgs) ! {
// $if debug { console.print_debug(" - window delete: $args_")}
mut args := args_
args.name = texttools.name_fix(args.name)
if !(s.window_exist(args)) {
return
}
mut i := 0
for mut w in s.windows {
if w.name == args.name {
if (args.id > 0 && w.id == args.id) || args.id == 0 {
w.stop()!
break
}
}
i += 1
}
s.windows.delete(i) // i is now the one in the list which needs to be removed
}
pub fn (mut s Session) restart() ! {
s.stop()!
s.create()!
}
pub fn (mut s Session) stop() ! {
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}")
}
}

View File

@@ -18,127 +18,28 @@ pub mut:
env map[string]string
}
pub struct WindowArgs {
@[params]
pub struct PaneNewArgs {
pub mut:
name string
reset bool //means we reset the pane if it already exists
cmd string
env map[string]string
reset bool
env map[string]string
}
// window_name is the name of the window in session main (will always be called session main)
// cmd to execute e.g. bash file
// environment arguments to use
// reset, if reset it will create window even if it does already exist, will destroy it
// ```
// struct WindowArgs {
// pub mut:
// name string
// cmd string
// env map[string]string
// reset bool
// }
// ```
pub fn (mut t Tmux) window_new(args WindowArgs) !Window {
mut s := t.session_create(name: 'main', reset: false)!
mut w := s.window_new(args)!
return w
pub fn (mut w Window) scan() ! {
//TODO: here needs to be the code to check reality and update panes
}
// is always in the main tmux
pub fn (mut t Tmux) window_delete(args WindowGetArgs) ! {
mut s := t.session_create(name: 'main', reset: false)!
s.window_delete(name: args.name)!
}
// window_name is the name of the window in session main (will always be called session main)
// cmd to execute e.g. bash file
// environment arguments to use
// reset, if reset it will create window even if it does already exist, will destroy it
// ```
// struct WindowArgs {
// pub mut:
// name string
// cmd string
// env map[string]string
// reset bool
// }
// ```
pub fn (mut s Session) window_new(args WindowArgs) !Window {
$if debug {
console.print_header(' start window: \n${args}')
}
namel := texttools.name_fix(args.name)
if s.window_exist(name: namel) {
if args.reset {
s.window_delete(name: namel)!
} else {
return error('cannot create new window it already exists, window ${namel} in session:${s.name}')
}
}
mut w := Window{
session: &s
name: namel
panes: []&Pane{}
env: args.env
}
s.windows << &w
w.create(args.cmd)!
// After creation, scan to populate panes
s.tmux.scan()!
return w
}
pub struct WindowGetArgs {
pub mut:
name string
cmd string
id int
}
fn (mut s Session) window_exist(args_ WindowGetArgs) bool {
mut args := args_
s.window_get(args) or { return false }
return true
}
pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window {
mut args := args_
args.name = texttools.name_fix(args.name)
for w in s.windows {
if w.name == args.name {
if (args.id > 0 && w.id == args.id) || args.id == 0 {
return w
}
}
}
return error('Cannot find window ${args.name} in session:${s.name}')
}
pub fn (mut s Session) window_delete(args_ WindowGetArgs) ! {
// $if debug { console.print_debug(" - window delete: $args_")}
mut args := args_
args.name = texttools.name_fix(args.name)
if !(s.window_exist(args)) {
return
}
mut i := 0
for mut w in s.windows {
if w.name == args.name {
if (args.id > 0 && w.id == args.id) || args.id == 0 {
w.stop()!
break
}
}
i += 1
}
s.windows.delete(i) // i is now the one in the list which needs to be removed
}
pub fn (mut w Window) create(cmd_ string) ! {
//helper function
//TODO env variables are not inserted in pane
fn (mut w Window) pane_create(args_ PaneNewArgs) ! {
// tmux new-window -P -c /tmp -e good=1 -e bad=0 -n koekoe -t main bash
mut final_cmd := cmd_
if cmd_.contains('\n') {
mut args := args_
mut final_cmd := args.cmd
if args.cmd.contains('\n') {
// means is multiline need to write it
// scriptpath string // is the path where the script will be put which is executed
// scriptkeep bool // means we don't remove the script
@@ -151,8 +52,13 @@ pub fn (mut w Window) create(cmd_ string) ! {
final_cmd = cmd_new
}
mut newcmd:='/bin/bash -c ${final_cmd}'
if cmd_==""{
newcmd = '/bin/bash'
}
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} -t ${w.session.name} -n ${w.name} \'/bin/bash -c ${final_cmd}\''
cmd := 'tmux new-window ${res_opt} -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}")
@@ -166,20 +72,8 @@ pub fn (mut w Window) create(cmd_ string) ! {
}
}
// do some good checks if the window is still active
// not implemented yet
pub fn (mut w Window) check() ! {
panic('not implemented yet')
}
// restart the window
pub fn (mut w Window) restart() ! {
w.stop()!
w.create()!
}
// stop the window
pub fn (mut w Window) stop() ! {
pub fn (mut w Window) kill() ! {
osal.exec(
cmd: 'tmux kill-window -t @${w.id}'
stdout: false
@@ -197,10 +91,10 @@ pub fn (window Window) str() string {
return out
}
pub fn (mut w Window) get_total_stats() !ProcessStats {
pub fn (mut w Window) stats() !ProcessStats {
mut total := ProcessStats{}
for mut pane in w.panes {
stats := pane.get_stats() or { continue }
stats := pane.stats() or { continue }
total.cpu_percent += stats.cpu_percent
total.memory_bytes += stats.memory_bytes
total.memory_percent += stats.memory_percent
@@ -218,12 +112,12 @@ fn (mut w Window) activate() ! {
}
// List panes in a window
pub fn (mut w Window) list_panes() []&Pane {
return w.panes
pub fn (mut w Window) pane_list() []&Pane {
return w.panes
}
// Get active pane in window
pub fn (mut w Window) get_active_pane() ?&Pane {
pub fn (mut w Window) pane_active() ?&Pane {
for pane in w.panes {
if pane.active {
return pane
@@ -231,50 +125,3 @@ pub fn (mut w Window) get_active_pane() ?&Pane {
}
return none
}
// show the environment
pub fn (mut w Window) environment_print() ! {
// This function needs to be updated to target a specific pane, not the window directly.
// For now, I'll leave it as is, but it's a point for future refinement.
// It should probably take a pane ID or operate on the active pane.
return error('Window.environment_print() needs to be updated to target a specific pane.')
}
// capture the output
pub fn (mut w Window) output_print() ! {
o := w.output()!
console.print_debug(o)
}
// capture the output
pub fn (mut w Window) output() !string {
//-S is start, minus means go in history, otherwise its only the active output
// tmux capture-pane -t your-session-name:your-window-number -S -1000
cmd := 'tmux capture-pane -t ${w.session.name}:@${w.id} -S -1000 && tmux show-buffer'
res := osal.execute_silent(cmd) or {
return error('Couldnt show enviroment cmd: ${w.cmd} \n${err}')
}
return texttools.remove_empty_lines(res)
}
pub fn (mut w Window) output_wait(c_ string, timeoutsec int) ! {
mut t := ourtime.now()
start := t.unix()
c := c_.replace('\n', '')
for i in 0 .. 2000 {
o := w.output()!
// console.print_debug(o)
$if debug {
console.print_debug(" - tmux ${w.name}: wait for: '${c}'")
}
// need to replace \n because can be wrapped because of size of pane
if o.replace('\n', '').contains(c) {
return
}
mut t2 := ourtime.now()
if t2.unix() > start + timeoutsec {
return error('timeout on output wait for tmux.\n${w} .\nwaiting for:\n${c}')
}
time.sleep(100 * time.millisecond)
}
}