This commit is contained in:
2025-08-25 06:28:42 +02:00
parent 5f683ec4a8
commit 836c87fbec
44 changed files with 891 additions and 942 deletions

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.osal.tmux
mut t := tmux.new()!

View File

@@ -3,13 +3,16 @@ module livekit
import freeflowuniverse.herolib.data.caching
import os
const CACHING_METHOD = caching.CachingMethod.once_per_process
// const CACHING_METHOD = caching.CachingMethod.once_per_process
fn _init() ! {
if caching.is_set(key: 'livekit_clients') {
return
}
caching.set[map[string]LivekitClient](key: 'livekit_clients', val: map[string]LivekitClient{}, CachingMethod.once_per_process)!
caching.set[map[string]LivekitClient](
key: 'livekit_clients'
val: map[string]LivekitClient{}
)!
}
fn _get() !map[string]LivekitClient {
@@ -25,7 +28,7 @@ pub fn get(name string) !LivekitClient {
pub fn set(client LivekitClient) ! {
mut clients := _get()!
clients[client.name] = client
caching.set[map[string]LivekitClient](key: 'livekit_clients', val: clients, CachingMethod.once_per_process)!
caching.set[map[string]LivekitClient](key: 'livekit_clients', val: clients)!
}
pub fn exists(name string) !bool {

View File

@@ -60,25 +60,36 @@ pub fn (mut c LivekitClient) start_web_egress(args StartWebEgressArgs) !EgressIn
}
pub fn (mut c LivekitClient) update_layout(egress_id string, layout string) !EgressInfo {
mut resp := c.post('twirp/livekit.Egress/UpdateLayout', {'egress_id': egress_id, 'layout': layout})!
mut resp := c.post('twirp/livekit.Egress/UpdateLayout', {
'egress_id': egress_id
'layout': layout
})!
egress_info := json.decode[EgressInfo](resp.body)!
return egress_info
}
pub fn (mut c LivekitClient) update_stream(egress_id string, args UpdateStreamArgs) !EgressInfo {
mut resp := c.post('twirp/livekit.Egress/UpdateStream', {'egress_id': egress_id, 'add_output_urls': args.add_output_urls, 'remove_output_urls': args.remove_output_urls})!
mut resp := c.post('twirp/livekit.Egress/UpdateStream', {
'egress_id': egress_id
'add_output_urls': args.add_output_urls
'remove_output_urls': args.remove_output_urls
})!
egress_info := json.decode[EgressInfo](resp.body)!
return egress_info
}
pub fn (mut c LivekitClient) list_egress(room_name string) ![]EgressInfo {
mut resp := c.post('twirp/livekit.Egress/ListEgress', {'room_name': room_name})!
mut resp := c.post('twirp/livekit.Egress/ListEgress', {
'room_name': room_name
})!
egress_infos := json.decode[[]EgressInfo](resp.body)!
return egress_infos
}
pub fn (mut c LivekitClient) stop_egress(egress_id string) !EgressInfo {
mut resp := c.post('twirp/livekit.Egress/StopEgress', {'egress_id': egress_id})!
mut resp := c.post('twirp/livekit.Egress/StopEgress', {
'egress_id': egress_id
})!
egress_info := json.decode[EgressInfo](resp.body)!
return egress_info
}

View File

@@ -93,8 +93,15 @@ pub mut:
video IngressVideoOptions
}
pub fn (mut c LivekitClient) create_ingress(args CreateIngressArgs) !IngressInfo {
mut resp := c.post('twirp/livekit.Ingress/CreateIngress', args)!
ingress_info := json.decode[IngressInfo](resp.body)!
return ingress_info
}
pub struct UpdateIngressArgs {
pub mut:
ingress_id string
name string
room_name string
participant_identity string
@@ -103,26 +110,32 @@ pub mut:
video IngressVideoOptions
}
pub fn (mut c LivekitClient) create_ingress(args CreateIngressArgs) !IngressInfo {
mut resp := c.post('twirp/livekit.Ingress/CreateIngress', args)!
ingress_info := json.decode[IngressInfo](resp.body)!
return ingress_info
}
pub fn (mut c LivekitClient) update_ingress(ingress_id string, args UpdateIngressArgs) !IngressInfo {
mut resp := c.post('twirp/livekit.Ingress/UpdateIngress', {'ingress_id': ingress_id, ...args})!
pub fn (mut c LivekitClient) update_ingress(args UpdateIngressArgs) !IngressInfo {
mut resp := c.post('twirp/livekit.Ingress/UpdateIngress', {
'ingress_id': args.ingress_id
'name': args.name
'room_name': args.room_name
'participant_identity': args.participant_identity
'participant_name': args.participant_name
'audio': args.audio
'video': args.video
})!
ingress_info := json.decode[IngressInfo](resp.body)!
return ingress_info
}
pub fn (mut c LivekitClient) list_ingress(room_name string) ![]IngressInfo {
mut resp := c.post('twirp/livekit.Ingress/ListIngress', {'room_name': room_name})!
mut resp := c.post('twirp/livekit.Ingress/ListIngress', {
'room_name': room_name
})!
ingress_infos := json.decode[[]IngressInfo](resp.body)!
return ingress_infos
}
pub fn (mut c LivekitClient) delete_ingress(ingress_id string) !IngressInfo {
mut resp := c.post('twirp/livekit.Ingress/DeleteIngress', {'ingress_id': ingress_id})!
mut resp := c.post('twirp/livekit.Ingress/DeleteIngress', {
'ingress_id': ingress_id
})!
ingress_info := json.decode[IngressInfo](resp.body)!
return ingress_info
}

View File

@@ -33,19 +33,27 @@ pub mut:
}
pub fn (mut c LivekitClient) list_participants(room_name string) ![]ParticipantInfo {
mut resp := c.post('twirp/livekit.RoomService/ListParticipants', {'room': room_name})!
mut resp := c.post('twirp/livekit.RoomService/ListParticipants', {
'room': room_name
})!
participants := json.decode[[]ParticipantInfo](resp.body)!
return participants
}
pub fn (mut c LivekitClient) get_participant(room_name string, identity string) !ParticipantInfo {
mut resp := c.post('twirp/livekit.RoomService/GetParticipant', {'room': room_name, 'identity': identity})!
mut resp := c.post('twirp/livekit.RoomService/GetParticipant', {
'room': room_name
'identity': identity
})!
participant := json.decode[ParticipantInfo](resp.body)!
return participant
}
pub fn (mut c LivekitClient) remove_participant(room_name string, identity string) ! {
_ = c.post('twirp/livekit.RoomService/RemoveParticipant', {'room': room_name, 'identity': identity})!
_ = c.post('twirp/livekit.RoomService/RemoveParticipant', {
'room': room_name
'identity': identity
})!
}
pub fn (mut c LivekitClient) update_participant(args UpdateParticipantArgs) ! {

View File

@@ -39,7 +39,9 @@ pub fn (mut c LivekitClient) create_room(args CreateRoomArgs) !Room {
}
pub fn (mut c LivekitClient) delete_room(room_name string) ! {
_ = c.post('twirp/livekit.RoomService/DeleteRoom', {'room': room_name})!
_ = c.post('twirp/livekit.RoomService/DeleteRoom', {
'room': room_name
})!
}
pub fn (mut c LivekitClient) update_room_metadata(args UpdateRoomMetadataArgs) ! {

View File

@@ -15,19 +15,19 @@ pub:
unix_micro string
}
pub struct Room {
pub:
active_recording bool
creation_time string
departure_timeout int
empty_timeout int
enabled_codecs []Codec
max_participants int
metadata string
name string
num_participants int
num_publishers int
sid string
turn_password string
version Version
}
// pub struct Room {
// pub:
// active_recording bool
// creation_time string
// departure_timeout int
// empty_timeout int
// enabled_codecs []Codec
// max_participants int
// metadata string
// name string
// num_participants int
// num_publishers int
// sid string
// turn_password string
// version Version
// }

View File

@@ -18,60 +18,60 @@ pub mut:
name string
}
// VideoGrant struct placeholder
pub struct VideoGrant {
pub mut:
room string
room_join bool @[json: 'roomJoin']
room_list bool @[json: 'roomList']
can_publish bool @[json: 'canPublish']
can_publish_data bool @[json: 'canPublishData']
can_subscribe bool @[json: 'canSubscribe']
}
// // VideoGrant struct placeholder
// pub struct VideoGrant {
// pub mut:
// room string
// room_join bool @[json: 'roomJoin']
// room_list bool @[json: 'roomList']
// can_publish bool @[json: 'canPublish']
// can_publish_data bool @[json: 'canPublishData']
// can_subscribe bool @[json: 'canSubscribe']
// }
// SIPGrant struct placeholder
struct SIPGrant {}
// AccessToken class
pub struct AccessToken {
mut:
api_key string
api_secret string
grants ClaimGrants
identity string
ttl int
}
// // AccessToken class
// pub struct AccessToken {
// mut:
// api_key string
// api_secret string
// grants ClaimGrants
// identity string
// ttl int
// }
// Method to add a video grant to the token
pub fn (mut token AccessToken) add_video_grant(grant VideoGrant) {
token.grants.video = grant
}
// pub fn (mut token AccessToken) add_video_grant(grant VideoGrant) {
// token.grants.video = grant
// }
// Method to generate a JWT token
pub fn (token AccessToken) to_jwt() !string {
// Create JWT payload
payload := json.encode(token.grants)
// // Method to generate a JWT token
// pub fn (token AccessToken) to_jwt() !string {
// // Create JWT payload
// payload := json.encode(token.grants)
println('payload: ${payload}')
// println('payload: ${payload}')
// Create JWT header
header := '{"alg":"HS256","typ":"JWT"}'
// // Create JWT header
// header := '{"alg":"HS256","typ":"JWT"}'
// Encode header and payload in base64
header_encoded := base64.url_encode_str(header)
payload_encoded := base64.url_encode_str(payload)
// // Encode header and payload in base64
// header_encoded := base64.url_encode_str(header)
// payload_encoded := base64.url_encode_str(payload)
// Create the unsigned token
unsigned_token := '${header_encoded}.${payload_encoded}'
// // Create the unsigned token
// unsigned_token := '${header_encoded}.${payload_encoded}'
// Create the HMAC-SHA256 signature
signature := hmac.new(token.api_secret.bytes(), unsigned_token.bytes(), sha256.sum,
sha256.block_size)
// // Create the HMAC-SHA256 signature
// signature := hmac.new(token.api_secret.bytes(), unsigned_token.bytes(), sha256.sum,
// sha256.block_size)
// Encode the signature in base64
signature_encoded := base64.url_encode(signature)
// // Encode the signature in base64
// signature_encoded := base64.url_encode(signature)
// Create the final JWT
jwt := '${unsigned_token}.${signature_encoded}'
return jwt
}
// // Create the final JWT
// jwt := '${unsigned_token}.${signature_encoded}'
// return jwt
// }

View File

@@ -40,12 +40,6 @@ pub fn (key SSHKey) private_key() !string {
return content
}
module core
import freeflowuniverse.herolib.core.pathlib
import os
@[params]
pub struct SSHConfig {
pub:

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

View File

@@ -96,9 +96,7 @@ fn sshkey_delete(mut agent SSHAgent, name string) ! {
fn sshkey_load(mut agent SSHAgent, name string) ! {
console.print_header('Loading SSH key: ${name}')
mut key := agent.get(name: name) or {
return error('SSH key "${name}" not found')
}
mut key := agent.get(name: name) or { return error('SSH key "${name}" not found') }
if key.loaded {
console.print_debug('SSH key "${name}" is already loaded')
@@ -113,18 +111,12 @@ fn sshkey_load(mut agent SSHAgent, name string) ! {
fn sshkey_check(mut agent SSHAgent, name string) ! {
console.print_header('Checking SSH key: ${name}')
mut key := agent.get(name: name) or {
return error('SSH key "${name}" not found')
}
mut key := agent.get(name: name) or { return error('SSH key "${name}" not found') }
// Check if key files exist
key_path := key.keypath() or {
return error('Private key file not found for "${name}"')
}
key_path := key.keypath() or { return error('Private key file not found for "${name}"') }
key_pub_path := key.keypath_pub() or {
return error('Public key file not found for "${name}"')
}
key_pub_path := key.keypath_pub() or { return error('Public key file not found for "${name}"') }
if !key_path.exists() {
return error('Private key file does not exist: ${key_path.path}')
@@ -157,9 +149,7 @@ 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')
}
mut key := agent.get(name: key_name) or { return error('SSH key "${key_name}" not found') }
// Create builder node
mut b := builder.new()!

View File

@@ -92,9 +92,7 @@ pub fn (mut agent SSHAgent) verify_key_access(mut node builder.Node, key_name st
}
// Test basic connectivity
result := node.exec_silent('echo "SSH key verification successful"') or {
return false
}
result := node.exec_silent('echo "SSH key verification successful"') or { return false }
return result.contains('SSH key verification successful')
}

View File

@@ -184,7 +184,12 @@ fn play_pane_kill(mut plbook PlayBook, mut tmux_instance Tmux) ! {
// 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
)!
}
}
}

View File

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

View File

@@ -20,7 +20,6 @@ pub mut:
last_output_offset int // for tracking new logs
}
pub fn (mut p Pane) stats() !ProcessStats {
if p.pid == 0 {
return ProcessStats{}
@@ -48,7 +47,6 @@ pub fn (mut p Pane) stats() !ProcessStats {
}
}
pub struct TMuxLogEntry {
pub mut:
content string
@@ -57,15 +55,12 @@ pub mut:
}
pub fn (mut p Pane) logs_get_new(reset bool) ![]TMuxLogEntry {
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}')
}
result := osal.execute_silent(cmd) or { return error('Cannot capture pane output: ${err}') }
lines := result.split_into_lines()
mut entries := []TMuxLogEntry{}
@@ -106,9 +101,7 @@ pub fn (mut p Pane) exit_status() !ProcessStatus {
pub fn (mut p Pane) logs_all() !string {
cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S -2000 -p'
return osal.execute_silent(cmd) or {
error('Cannot capture pane output: ${err}')
}
return osal.execute_silent(cmd) or { error('Cannot capture pane output: ${err}') }
}
// Fix the output_wait method to use correct method name

View File

@@ -1,7 +1,5 @@
module tmux
pub struct ProcessStats {
pub mut:
cpu_percent f64
@@ -9,13 +7,9 @@ pub mut:
memory_percent f64
}
enum ProcessStatus {
running
finished_ok
finished_error
not_found
}

View File

@@ -21,6 +21,7 @@ pub mut:
env map[string]string
reset bool
}
@[params]
pub struct WindowGetArgs {
pub mut:
@@ -28,10 +29,9 @@ pub mut:
id int
}
pub fn (mut s Session) create() ! {
// 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 {
// Session doesn't exist, this is expected
osal.Job{}
@@ -42,7 +42,7 @@ pub fn (mut s Session) create() ! {
}
// 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 {
return error("Can't create session ${s.name}: ${err}")
}
@@ -54,7 +54,7 @@ pub fn (mut s Session) scan() ! {
cmd := "tmux list-windows -t ${s.name} -F '#{window_name}|#{window_id}|#{window_active}'"
result := osal.execute_silent(cmd) or {
if err.msg().contains('session not found') {
return // Session doesn't exist anymore
return
}
return error('Cannot list windows for session ${s.name}: ${err}')
}
@@ -102,7 +102,6 @@ pub fn (mut s Session) scan() ! {
s.windows = s.windows.filter(current_windows[it.name] == true)
}
// window_name is the name of the window in session main (will always be called session main)
// cmd to execute e.g. bash file
// environment arguments to use
@@ -143,10 +142,6 @@ pub fn (mut s Session) window_new(args WindowArgs) !Window {
return w
}
// get all windows as found in a session
pub fn (mut s Session) windows_get() []&Window {
mut res := []&Window{}
@@ -208,8 +203,6 @@ pub fn (mut s Session) stats() !ProcessStats {
// }
// }
fn (mut s Session) window_exist(args_ WindowGetArgs) bool {
mut args := args_
s.window_get(args) or { return false }
@@ -249,7 +242,6 @@ pub fn (mut s Session) window_delete(args_ WindowGetArgs) ! {
s.windows.delete(i) // i is now the one in the list which needs to be removed
}
pub fn (mut s Session) restart() ! {
s.stop()!
s.create()!

View File

@@ -27,7 +27,6 @@ pub mut:
env map[string]string
}
pub fn (mut w Window) scan() ! {
// Get current panes for this window
cmd := "tmux list-panes -t ${w.session.name}:@${w.id} -F '#{pane_id}|#{pane_pid}|#{pane_active}|#{pane_start_command}'"
@@ -81,10 +80,10 @@ pub fn (mut w Window) scan() ! {
w.panes = w.panes.filter(current_panes[it.id] == true)
}
pub fn (mut w Window) stop() ! {
w.kill()!
}
// helper function
// TODO env variables are not inserted in pane
pub fn (mut w Window) create(cmd_ string) ! {
@@ -100,7 +99,7 @@ pub fn (mut w Window) create(cmd_ string) ! {
}
mut newcmd := '/bin/bash -c "${final_cmd}"'
if cmd_ == "" {
if cmd_ == '' {
newcmd = '/bin/bash'
}

View File

@@ -1,7 +1,7 @@
module datamodel
@[heap]
// I can bid for infra, and optionally get accepted
@[heap]
pub struct Bid {
pub mut:
id u32

View File

@@ -1,5 +1,7 @@
module datamodel
import freeflowuniverse.herolib.threefold.grid4.datamodel { Node }
pub struct NodeSim {
Node
pub mut: