Merge branch 'development' into development_tmux

* development:
  ...
  ...
  ...
  ...

# Conflicts:
#	examples/osal/sshagent.vsh
#	examples/osal/sshagent/sshagent_example.v
#	examples/osal/tmux.vsh
#	lib/osal/sshagent/agent.v
#	lib/osal/sshagent/builder_integration.v
#	lib/osal/tmux/tmux_pane.v
#	lib/osal/tmux/tmux_scan.v
#	lib/osal/tmux/tmux_session.v
#	lib/osal/tmux/tmux_window.v
This commit is contained in:
2025-08-25 06:34:03 +02:00
67 changed files with 902 additions and 19638 deletions

View File

@@ -1,86 +0,0 @@
module core
import freeflowuniverse.herolib.core.pathlib
import os
@[params]
pub struct SSHConfig {
pub:
directory string = os.join_path(os.home_dir(), '.ssh')
}
// Returns a specific SSH key with the given name from the default SSH directory (~/.ssh)
pub fn get_ssh_key(key_name string, config SSHConfig) ?SSHKey {
mut ssh_dir := pathlib.get_dir(path: config.directory) or { return none }
list := ssh_dir.list(files_only: true) or { return none }
for file in list.paths {
if file.name() == key_name {
return SSHKey{
name: file.name()
directory: ssh_dir.path
}
}
}
return none
}
// Lists SSH keys in the default SSH directory (~/.ssh) and returns an array of SSHKey structs
fn list_ssh_keys(config SSHConfig) ![]SSHKey {
mut ssh_dir := pathlib.get_dir(path: config.directory) or {
return error('Error getting ssh directory: ${err}')
}
mut keys := []SSHKey{}
list := ssh_dir.list(files_only: true) or {
return error('Failed to list files in SSH directory')
}
for file in list.paths {
if file.extension() == 'pub' || file.name().starts_with('id_') {
keys << SSHKey{
name: file.name()
directory: ssh_dir.path
}
}
}
return keys
}
// Creates a new SSH key pair to the specified directory
pub fn new_ssh_key(key_name string, config SSHConfig) !SSHKey {
ssh_dir := pathlib.get_dir(
path: config.directory
create: true
) or { return error('Error getting SSH directory: ${err}') }
// Paths for the private and public keys
priv_key_path := os.join_path(ssh_dir.path, key_name)
pub_key_path := '${priv_key_path}.pub'
// Check if the key already exists
if os.exists(priv_key_path) || os.exists(pub_key_path) {
return error("Key pair already exists with the name '${key_name}'")
}
panic('implement shhkeygen logic')
// Generate a random private key (for demonstration purposes)
// Replace this with actual key generation logic (e.g., calling `ssh-keygen` or similar)
// private_key_content := '-----BEGIN PRIVATE KEY-----\n${rand.string(64)}\n-----END PRIVATE KEY-----'
// public_key_content := 'ssh-rsa ${rand.string(64)} user@host'
// Save the keys to their respective files
// os.write_file(priv_key_path, private_key_content) or {
// return error("Failed to write private key: ${err}")
// }
// os.write_file(pub_key_path, public_key_content) or {
// return error("Failed to write public key: ${err}")
// }
return SSHKey{
name: key_name
directory: ssh_dir.path
}
}

View File

@@ -39,3 +39,85 @@ pub fn (key SSHKey) private_key() !string {
content := path.read()!
return content
}
@[params]
pub struct SSHConfig {
pub:
directory string = os.join_path(os.home_dir(), '.ssh')
}
// Returns a specific SSH key with the given name from the default SSH directory (~/.ssh)
pub fn get_ssh_key(key_name string, config SSHConfig) ?SSHKey {
mut ssh_dir := pathlib.get_dir(path: config.directory) or { return none }
list := ssh_dir.list(files_only: true) or { return none }
for file in list.paths {
if file.name() == key_name {
return SSHKey{
name: file.name()
directory: ssh_dir.path
}
}
}
return none
}
// Lists SSH keys in the default SSH directory (~/.ssh) and returns an array of SSHKey structs
fn list_ssh_keys(config SSHConfig) ![]SSHKey {
mut ssh_dir := pathlib.get_dir(path: config.directory) or {
return error('Error getting ssh directory: ${err}')
}
mut keys := []SSHKey{}
list := ssh_dir.list(files_only: true) or {
return error('Failed to list files in SSH directory')
}
for file in list.paths {
if file.extension() == 'pub' || file.name().starts_with('id_') {
keys << SSHKey{
name: file.name()
directory: ssh_dir.path
}
}
}
return keys
}
// Creates a new SSH key pair to the specified directory
pub fn new_ssh_key(key_name string, config SSHConfig) !SSHKey {
ssh_dir := pathlib.get_dir(
path: config.directory
create: true
) or { return error('Error getting SSH directory: ${err}') }
// Paths for the private and public keys
priv_key_path := os.join_path(ssh_dir.path, key_name)
pub_key_path := '${priv_key_path}.pub'
// Check if the key already exists
if os.exists(priv_key_path) || os.exists(pub_key_path) {
return error("Key pair already exists with the name '${key_name}'")
}
panic('implement shhkeygen logic')
// Generate a random private key (for demonstration purposes)
// Replace this with actual key generation logic (e.g., calling `ssh-keygen` or similar)
// private_key_content := '-----BEGIN PRIVATE KEY-----\n${rand.string(64)}\n-----END PRIVATE KEY-----'
// public_key_content := 'ssh-rsa ${rand.string(64)} user@host'
// Save the keys to their respective files
// os.write_file(priv_key_path, private_key_content) or {
// return error("Failed to write private key: ${err}")
// }
// os.write_file(pub_key_path, public_key_content) or {
// return error("Failed to write public key: ${err}")
// }
return SSHKey{
name: key_name
directory: ssh_dir.path
}
}

View File

@@ -24,6 +24,6 @@ pub:
pub fn new(args LinuxNewArgs) !LinuxFactory {
mut t := LinuxFactory{
username: args.username
}
}
return t
}

View File

@@ -11,34 +11,34 @@ pub fn play(mut plbook PlayBook) ! {
// Process user_create actions
play_user_create(mut plbook, mut lf)!
// Process user_delete actions
play_user_delete(mut plbook, mut lf)!
// Process sshkey_create actions
play_sshkey_create(mut plbook, mut lf)!
// Process sshkey_delete actions
play_sshkey_delete(mut plbook, mut lf)!
}
fn play_user_create(mut plbook PlayBook, mut lf LinuxFactory) ! {
mut actions := plbook.find(filter: 'usermgmt.user_create')!
for mut action in actions {
mut p := action.params
mut args := UserCreateArgs{
name: p.get('name')!
giteakey: p.get_default('giteakey', '')!
giteaurl: p.get_default('giteaurl', '')!
passwd: p.get_default('passwd', '')!
name: p.get('name')!
giteakey: p.get_default('giteakey', '')!
giteaurl: p.get_default('giteaurl', '')!
passwd: p.get_default('passwd', '')!
description: p.get_default('description', '')!
email: p.get_default('email', '')!
tel: p.get_default('tel', '')!
sshkey: p.get_default('sshkey', '')! // SSH public key
email: p.get_default('email', '')!
tel: p.get_default('tel', '')!
sshkey: p.get_default('sshkey', '')! // SSH public key
}
lf.user_create(args)!
action.done = true
}
@@ -46,14 +46,14 @@ fn play_user_create(mut plbook PlayBook, mut lf LinuxFactory) ! {
fn play_user_delete(mut plbook PlayBook, mut lf LinuxFactory) ! {
mut actions := plbook.find(filter: 'usermgmt.user_delete')!
for mut action in actions {
mut p := action.params
mut args := UserDeleteArgs{
name: p.get('name')!
}
lf.user_delete(args)!
action.done = true
}
@@ -61,17 +61,17 @@ fn play_user_delete(mut plbook PlayBook, mut lf LinuxFactory) ! {
fn play_sshkey_create(mut plbook PlayBook, mut lf LinuxFactory) ! {
mut actions := plbook.find(filter: 'usermgmt.sshkey_create')!
for mut action in actions {
mut p := action.params
mut args := SSHKeyCreateArgs{
username: p.get('username')!
username: p.get('username')!
sshkey_name: p.get('sshkey_name')!
sshkey_pub: p.get_default('sshkey_pub', '')!
sshkey_pub: p.get_default('sshkey_pub', '')!
sshkey_priv: p.get_default('sshkey_priv', '')!
}
lf.sshkey_create(args)!
action.done = true
}
@@ -79,16 +79,16 @@ fn play_sshkey_create(mut plbook PlayBook, mut lf LinuxFactory) ! {
fn play_sshkey_delete(mut plbook PlayBook, mut lf LinuxFactory) ! {
mut actions := plbook.find(filter: 'usermgmt.sshkey_delete')!
for mut action in actions {
mut p := action.params
mut args := SSHKeyDeleteArgs{
username: p.get('username')!
username: p.get('username')!
sshkey_name: p.get('sshkey_name')!
}
lf.sshkey_delete(args)!
action.done = true
}
}
}

View File

@@ -0,0 +1,12 @@
# Auto-start ssh-agent if not running
SSH_AGENT_PID_FILE="$HOME/.ssh/agent.pid"
SSH_AUTH_SOCK_FILE="$HOME/.ssh/agent.sock"
chown "$NEWUSER":"$NEWUSER" "$PROFILE_SCRIPT"
chmod 644 "$PROFILE_SCRIPT"
# --- source it on login ---
#TODO should be done in vcode
if ! grep -q ".profile_sshagent" "$USERHOME/.bashrc"; then
echo "[ -f ~/.profile_sshagent ] && source ~/.profile_sshagent" >> "$USERHOME/.bashrc"
fi

View File

@@ -57,19 +57,4 @@ chown root:ourworld /code
chmod 2775 /code # rwx for user+group, SGID bit so new files inherit group
echo "✅ /code prepared (group=ourworld, rwx for group, SGID bit set)"
# --- create login helper script for ssh-agent ---
PROFILE_SCRIPT="$USERHOME/.profile_sshagent"
cat > "$PROFILE_SCRIPT" <<'EOF'
# Auto-start ssh-agent if not running
SSH_AGENT_PID_FILE="$HOME/.ssh/agent.pid"
SSH_AUTH_SOCK_FILE="$HOME/.ssh/agent.sock"
chown "$NEWUSER":"$NEWUSER" "$PROFILE_SCRIPT"
chmod 644 "$PROFILE_SCRIPT"
# --- source it on login ---
if ! grep -q ".profile_sshagent" "$USERHOME/.bashrc"; then
echo "[ -f ~/.profile_sshagent ] && source ~/.profile_sshagent" >> "$USERHOME/.bashrc"
fi
echo "🎉 Setup complete for user $NEWUSER"

View File

@@ -119,7 +119,9 @@ pub fn (mut lf LinuxFactory) sshkey_create(args SSHKeyCreateArgs) ! {
} else {
// Generate new SSH key (modern ed25519)
key_path := '${ssh_dir}/${args.sshkey_name}'
osal.exec(cmd: 'ssh-keygen -t ed25519 -f ${key_path} -N "" -C "${args.username}@$(hostname)"')!
osal.exec(
cmd: 'ssh-keygen -t ed25519 -f ${key_path} -N "" -C "${args.username}@$(hostname)"'
)!
console.print_green(' New SSH key generated for ${args.username}')
}
@@ -175,12 +177,12 @@ fn (mut lf LinuxFactory) save_user_config(args UserCreateArgs) ! {
}
new_config := UserConfig{
name: args.name
giteakey: args.giteakey
giteaurl: args.giteaurl
email: args.email
name: args.name
giteakey: args.giteakey
giteaurl: args.giteaurl
email: args.email
description: args.description
tel: args.tel
tel: args.tel
}
if found_idx >= 0 {
@@ -201,7 +203,7 @@ fn (mut lf LinuxFactory) remove_user_config(username string) ! {
config_path := '${config_dir}/myconfig.json'
if !os.exists(config_path) {
return // Nothing to remove
return
}
content := osal.file_read(config_path)!
@@ -243,7 +245,9 @@ fn (mut lf LinuxFactory) create_user_system(args UserCreateArgs) ! {
// Ensure ourworld group exists
group_check := osal.exec(cmd: 'getent group ourworld', raise_error: false) or {
osal.Job{ exit_code: 1 }
osal.Job{
exit_code: 1
}
}
if group_check.exit_code != 0 {
console.print_item(' Creating group ourworld')
@@ -284,58 +288,9 @@ fn (mut lf LinuxFactory) create_ssh_agent_profile(username string) ! {
user_home := '/home/${username}'
profile_script := '${user_home}/.profile_sshagent'
script_content := '# Auto-start ssh-agent if not running
SSH_AGENT_PID_FILE="$HOME/.ssh/agent.pid"
SSH_AUTH_SOCK_FILE="$HOME/.ssh/agent.sock"
// script_content := ''
# Function to start ssh-agent
start_ssh_agent() {
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
# Start ssh-agent and save connection info
ssh-agent -s > "$SSH_AGENT_PID_FILE"
source "$SSH_AGENT_PID_FILE"
# Save socket path for future sessions
echo "$SSH_AUTH_SOCK" > "$SSH_AUTH_SOCK_FILE"
# Load all private keys found in ~/.ssh
if [ -d "$HOME/.ssh" ]; then
for KEY in "$HOME"/.ssh/*; do
if [ -f "$KEY" ] && [ ! "${KEY##*.}" = "pub" ] && grep -q "PRIVATE KEY" "$KEY" 2>/dev/null; then
'ssh-' + 'add "$KEY" >/dev/null 2>&1 && echo "🔑 Loaded key: $(basename $KEY)"'
fi
done
fi
}
# Check if ssh-agent is running
if [ -f "$SSH_AGENT_PID_FILE" ]; then
source "$SSH_AGENT_PID_FILE" >/dev/null 2>&1
# Test if agent is responsive
if ! ('ssh-' + 'add -l >/dev/null 2>&1'); then
start_ssh_agent
else
# Agent is running, restore socket path
if [ -f "$SSH_AUTH_SOCK_FILE" ]; then
export SSH_AUTH_SOCK=$(cat "$SSH_AUTH_SOCK_FILE")
fi
fi
else
start_ssh_agent
fi
# For interactive shells
if [[ $- == *i* ]]; then
echo "🔑 SSH Agent ready at $SSH_AUTH_SOCK"
# Show loaded keys
KEY_COUNT=$('ssh-' + 'add -l 2>/dev/null | wc -l')
if [ "$KEY_COUNT" -gt 0 ]; then
echo "🔑 $KEY_COUNT SSH key(s) loaded"
fi
fi
'
panic('implement')
osal.file_write(profile_script, script_content)!
osal.exec(cmd: 'chown ${username}:${username} ${profile_script}')!
@@ -351,4 +306,4 @@ fi
}
console.print_green(' SSH agent profile created for ${username}')
}
}

View File

@@ -16,19 +16,19 @@ pub mut:
pub fn (mut agent SSHAgent) ensure_single_agent() ! {
user := os.getenv('USER')
socket_path := get_agent_socket_path(user)
// Check if we have a valid agent already
if agent.is_agent_responsive() {
console.print_debug('SSH agent already running and responsive')
return
}
// Kill any orphaned agents
agent.cleanup_orphaned_agents()!
// Start new agent with consistent socket
agent.start_agent_with_socket(socket_path)!
// Set environment variables
os.setenv('SSH_AUTH_SOCK', socket_path, true)
agent.active = true
@@ -44,7 +44,7 @@ pub fn (mut agent SSHAgent) is_agent_responsive() bool {
if os.getenv('SSH_AUTH_SOCK') == '' {
return false
}
res := os.execute('ssh-add -l 2>/dev/null')
return res.exit_code == 0 || res.exit_code == 1 // 1 means no keys, but agent is running
}
@@ -52,12 +52,12 @@ pub fn (mut agent SSHAgent) is_agent_responsive() bool {
// cleanup orphaned ssh-agent processes
pub fn (mut agent SSHAgent) cleanup_orphaned_agents() ! {
user := os.getenv('USER')
// Find ssh-agent processes for current user
res := os.execute('pgrep -u ${user} ssh-agent')
if res.exit_code == 0 && res.output.len > 0 {
pids := res.output.trim_space().split('\n')
for pid in pids {
if pid.trim_space() != '' {
// Check if this agent has a valid socket
@@ -77,7 +77,7 @@ fn (mut agent SSHAgent) is_agent_pid_valid(pid int) bool {
if res.exit_code != 0 {
return false
}
for socket_path in res.output.split('\n') {
if socket_path.trim_space() != '' {
// Test if this socket responds
@@ -85,7 +85,7 @@ fn (mut agent SSHAgent) is_agent_pid_valid(pid int) bool {
os.setenv('SSH_AUTH_SOCK', socket_path, true)
test_res := os.execute('ssh-add -l 2>/dev/null')
os.setenv('SSH_AUTH_SOCK', old_sock, true)
if test_res.exit_code == 0 || test_res.exit_code == 1 {
return true
}
@@ -100,45 +100,45 @@ pub fn (mut agent SSHAgent) start_agent_with_socket(socket_path string) ! {
if os.exists(socket_path) {
os.rm(socket_path)!
}
// Start ssh-agent with specific socket
cmd := 'ssh-agent -a ${socket_path}'
res := os.execute(cmd)
if res.exit_code != 0 {
return error('Failed to start ssh-agent: ${res.output}')
}
// Verify socket was created
if !os.exists(socket_path) {
return error('SSH agent socket was not created at ${socket_path}')
}
// Set environment variable
os.setenv('SSH_AUTH_SOCK', socket_path, true)
// Verify agent is responsive
if !agent.is_agent_responsive() {
return error('SSH agent started but is not responsive')
}
console.print_debug('SSH agent started with socket: ${socket_path}')
}
// get agent status and diagnostics
pub fn (mut agent SSHAgent) diagnostics() map[string]string {
mut diag := map[string]string{}
diag['socket_path'] = os.getenv('SSH_AUTH_SOCK')
diag['socket_exists'] = os.exists(diag['socket_path']).str()
diag['agent_responsive'] = agent.is_agent_responsive().str()
diag['loaded_keys_count'] = agent.keys.filter(it.loaded).len.str()
diag['total_keys_count'] = agent.keys.len.str()
// Count running ssh-agent processes
user := os.getenv('USER')
res := os.execute('pgrep -u ${user} ssh-agent | wc -l')
diag['agent_processes'] = if res.exit_code == 0 { res.output.trim_space() } else { '0' }
return diag
}

View File

@@ -11,7 +11,7 @@ pub fn play(mut plbook PlayBook) ! {
// Create tmux instance
mut tmux_instance := new()!
// Start tmux if not running
if !tmux_instance.is_running()! {
tmux_instance.start()!
@@ -44,7 +44,7 @@ fn parse_window_name(name string) !ParsedWindowName {
}
return ParsedWindowName{
session: texttools.name_fix(parts[0])
window: texttools.name_fix(parts[1])
window: texttools.name_fix(parts[1])
}
}
@@ -55,8 +55,8 @@ fn parse_pane_name(name string) !ParsedPaneName {
}
return ParsedPaneName{
session: texttools.name_fix(parts[0])
window: texttools.name_fix(parts[1])
pane: texttools.name_fix(parts[2])
window: texttools.name_fix(parts[1])
pane: texttools.name_fix(parts[2])
}
}
@@ -66,12 +66,12 @@ fn play_session_create(mut plbook PlayBook, mut tmux_instance Tmux) ! {
mut p := action.params
session_name := p.get('name')!
reset := p.get_default_false('reset')
tmux_instance.session_create(
name: session_name
name: session_name
reset: reset
)!
action.done = true
}
}
@@ -81,9 +81,9 @@ fn play_session_delete(mut plbook PlayBook, mut tmux_instance Tmux) ! {
for mut action in actions {
mut p := action.params
session_name := p.get('name')!
tmux_instance.session_delete(session_name)!
action.done = true
}
}
@@ -96,7 +96,7 @@ fn play_window_create(mut plbook PlayBook, mut tmux_instance Tmux) ! {
parsed := parse_window_name(name)!
cmd := p.get_default('cmd', '')!
reset := p.get_default_false('reset')
// Parse environment variables if provided
mut env := map[string]string{}
if env_str := p.get_default('env', '') {
@@ -109,21 +109,21 @@ fn play_window_create(mut plbook PlayBook, mut tmux_instance Tmux) ! {
}
}
}
// Get or create session
mut session := if tmux_instance.session_exist(parsed.session) {
tmux_instance.session_get(parsed.session)!
} else {
tmux_instance.session_create(name: parsed.session)!
}
session.window_new(
name: parsed.window
cmd: cmd
env: env
name: parsed.window
cmd: cmd
env: env
reset: reset
)!
action.done = true
}
}
@@ -134,12 +134,12 @@ fn play_window_delete(mut plbook PlayBook, mut tmux_instance Tmux) ! {
mut p := action.params
name := p.get('name')!
parsed := parse_window_name(name)!
if tmux_instance.session_exist(parsed.session) {
mut session := tmux_instance.session_get(parsed.session)!
session.window_delete(name: parsed.window)!
}
action.done = true
}
}
@@ -151,19 +151,19 @@ fn play_pane_execute(mut plbook PlayBook, mut tmux_instance Tmux) ! {
name := p.get('name')!
cmd := p.get('cmd')!
parsed := parse_pane_name(name)!
// Find the session and window
if tmux_instance.session_exist(parsed.session) {
mut session := tmux_instance.session_get(parsed.session)!
if session.window_exist(name: parsed.window) {
mut window := session.window_get(name: parsed.window)!
// Send command to the window (goes to active pane by default)
tmux_cmd := 'tmux send-keys -t ${session.name}:@${window.id} "${cmd}" Enter'
osal.exec(cmd: tmux_cmd, stdout: false, name: 'tmux_pane_execute')!
}
}
action.done = true
}
}
@@ -174,21 +174,26 @@ fn play_pane_kill(mut plbook PlayBook, mut tmux_instance Tmux) ! {
mut p := action.params
name := p.get('name')!
parsed := parse_pane_name(name)!
// Find the session and window, then kill the active pane
if tmux_instance.session_exist(parsed.session) {
mut session := tmux_instance.session_get(parsed.session)!
if session.window_exist(name: parsed.window) {
mut window := session.window_get(name: parsed.window)!
// Kill the active pane in the window
if pane := window.pane_active() {
tmux_cmd := 'tmux kill-pane -t ${session.name}:@${window.id}.%${pane.id}'
osal.exec(cmd: tmux_cmd, stdout: false, name: 'tmux_pane_kill', ignore_error: true)!
osal.exec(
cmd: tmux_cmd
stdout: false
name: 'tmux_pane_kill'
ignore_error: true
)!
}
}
}
action.done = true
}
}
}

View File

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

View File

@@ -45,37 +45,37 @@ fn test_stop() ! {
}
fn test_windows_get() ! {
mut tmux := new()!
tmux.start()!
// After start, scan to get the initial session
tmux.scan()!
windows := tmux.windows_get()
assert windows.len >= 0 // At least the default session should exist
tmux.stop()!
mut tmux := new()!
tmux.start()!
// After start, scan to get the initial session
tmux.scan()!
windows := tmux.windows_get()
assert windows.len >= 0 // At least the default session should exist
tmux.stop()!
}
fn test_scan() ! {
console.print_debug('-----Testing scan------')
mut tmux := new()!
tmux.start()!
console.print_debug('-----Testing scan------')
mut tmux := new()!
tmux.start()!
// Test initial scan
tmux.scan()!
sessions_before := tmux.sessions.len
// Create a test session
mut session := tmux.session_create(name: 'test_scan')!
// Scan again
tmux.scan()!
sessions_after := tmux.sessions.len
assert sessions_after >= sessions_before
tmux.stop()!
// Test initial scan
tmux.scan()!
sessions_before := tmux.sessions.len
// Create a test session
mut session := tmux.session_create(name: 'test_scan')!
// Scan again
tmux.scan()!
sessions_after := tmux.sessions.len
assert sessions_after >= sessions_before
tmux.stop()!
}
// //TODO: fix test

View File

@@ -24,23 +24,23 @@ fn testsuite_end() {
}
fn test_window_new() ! {
mut tmux := new()!
tmux.start()!
mut tmux := new()!
tmux.start()!
// Create session first
mut session := tmux.session_create(name: 'main')!
// Test window creation
mut window := session.window_new(
name: 'TestWindow'
cmd: 'bash'
reset: true
)!
assert window.name == 'testwindow' // name_fix converts to lowercase
assert session.window_exist(name: 'testwindow')
tmux.stop()!
// Create session first
mut session := tmux.session_create(name: 'main')!
// Test window creation
mut window := session.window_new(
name: 'TestWindow'
cmd: 'bash'
reset: true
)!
assert window.name == 'testwindow' // name_fix converts to lowercase
assert session.window_exist(name: 'testwindow')
tmux.stop()!
}
// tests creating duplicate windows