Merge branch 'development_decartive' into development_builders
* development_decartive: refactor: use osal.processinfo_get for process stats feat: add declarative tmux and ttyd management # Conflicts: # lib/osal/tmux/readme.md
This commit is contained in:
@@ -23,7 +23,12 @@ pub fn play(mut plbook PlayBook) ! {
|
||||
play_window_delete(mut plbook, mut tmux_instance)!
|
||||
play_pane_execute(mut plbook, mut tmux_instance)!
|
||||
play_pane_kill(mut plbook, mut tmux_instance)!
|
||||
// TODO: Implement pane_create, pane_delete, pane_split when pane API is extended
|
||||
play_pane_split(mut plbook, mut tmux_instance)!
|
||||
play_session_ttyd(mut plbook, mut tmux_instance)!
|
||||
play_window_ttyd(mut plbook, mut tmux_instance)!
|
||||
play_session_ttyd_stop(mut plbook, mut tmux_instance)!
|
||||
play_window_ttyd_stop(mut plbook, mut tmux_instance)!
|
||||
play_ttyd_stop_all(mut plbook, mut tmux_instance)!
|
||||
}
|
||||
|
||||
struct ParsedWindowName {
|
||||
@@ -43,8 +48,8 @@ fn parse_window_name(name string) !ParsedWindowName {
|
||||
return error('Window name must be in format "session|window", got: ${name}')
|
||||
}
|
||||
return ParsedWindowName{
|
||||
session: texttools.name_fix(parts[0])
|
||||
window: texttools.name_fix(parts[1])
|
||||
session: texttools.name_fix_token(parts[0])
|
||||
window: texttools.name_fix_token(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +59,9 @@ fn parse_pane_name(name string) !ParsedPaneName {
|
||||
return error('Pane name must be in format "session|window|pane", got: ${name}')
|
||||
}
|
||||
return ParsedPaneName{
|
||||
session: texttools.name_fix(parts[0])
|
||||
window: texttools.name_fix(parts[1])
|
||||
pane: texttools.name_fix(parts[2])
|
||||
session: texttools.name_fix_token(parts[0])
|
||||
window: texttools.name_fix_token(parts[1])
|
||||
pane: texttools.name_fix_token(parts[2])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,3 +202,138 @@ fn play_pane_kill(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
fn play_pane_split(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
mut actions := plbook.find(filter: 'tmux.pane_split')!
|
||||
for mut action in actions {
|
||||
mut p := action.params
|
||||
name := p.get('name')!
|
||||
cmd := p.get_default('cmd', '')!
|
||||
horizontal := p.get_default_false('horizontal')
|
||||
parsed := parse_window_name(name)!
|
||||
|
||||
// Parse environment variables if provided
|
||||
mut env := map[string]string{}
|
||||
if env_str := p.get_default('env', '') {
|
||||
env_pairs := env_str.split(',')
|
||||
for pair in env_pairs {
|
||||
kv := pair.split('=')
|
||||
if kv.len == 2 {
|
||||
env[kv[0].trim_space()] = kv[1].trim_space()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)!
|
||||
|
||||
// Split the pane
|
||||
window.pane_split(
|
||||
cmd: cmd
|
||||
horizontal: horizontal
|
||||
env: env
|
||||
)!
|
||||
}
|
||||
}
|
||||
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
fn play_session_ttyd(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
mut actions := plbook.find(filter: 'tmux.session_ttyd')!
|
||||
for mut action in actions {
|
||||
mut p := action.params
|
||||
session_name := p.get('name')!
|
||||
port := p.get_int('port')!
|
||||
editable := p.get_default_false('editable')
|
||||
|
||||
if tmux_instance.session_exist(session_name) {
|
||||
mut session := tmux_instance.session_get(session_name)!
|
||||
session.run_ttyd(
|
||||
port: port
|
||||
editable: editable
|
||||
)!
|
||||
}
|
||||
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
fn play_window_ttyd(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
mut actions := plbook.find(filter: 'tmux.window_ttyd')!
|
||||
for mut action in actions {
|
||||
mut p := action.params
|
||||
name := p.get('name')!
|
||||
port := p.get_int('port')!
|
||||
editable := p.get_default_false('editable')
|
||||
parsed := parse_window_name(name)!
|
||||
|
||||
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)!
|
||||
window.run_ttyd(
|
||||
port: port
|
||||
editable: editable
|
||||
)!
|
||||
}
|
||||
}
|
||||
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tmux.session_ttyd_stop actions
|
||||
fn play_session_ttyd_stop(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
for mut action in plbook.find(filter: 'tmux.session_ttyd_stop')! {
|
||||
if action.done {
|
||||
continue
|
||||
}
|
||||
|
||||
mut p := action.params
|
||||
session_name := p.get('name')!
|
||||
port := p.get_int('port')!
|
||||
|
||||
mut session := tmux_instance.session_get(session_name)!
|
||||
session.stop_ttyd(port)!
|
||||
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tmux.window_ttyd_stop actions
|
||||
fn play_window_ttyd_stop(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
for mut action in plbook.find(filter: 'tmux.window_ttyd_stop')! {
|
||||
if action.done {
|
||||
continue
|
||||
}
|
||||
|
||||
mut p := action.params
|
||||
name := p.get('name')!
|
||||
port := p.get_int('port')!
|
||||
|
||||
parsed := parse_window_name(name)!
|
||||
mut session := tmux_instance.session_get(parsed.session)!
|
||||
mut window := session.window_get(name: parsed.window)!
|
||||
window.stop_ttyd(port)!
|
||||
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tmux.ttyd_stop_all actions
|
||||
fn play_ttyd_stop_all(mut plbook PlayBook, mut tmux_instance Tmux) ! {
|
||||
for mut action in plbook.find(filter: 'tmux.ttyd_stop_all')! {
|
||||
if action.done {
|
||||
continue
|
||||
}
|
||||
|
||||
stop_all_ttyd()!
|
||||
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# TMUX
|
||||
|
||||
|
||||
TMUX is a very capable process manager.
|
||||
|
||||
> TODO: TTYD, need to integrate with TMUX for exposing TMUX over http
|
||||
@@ -11,7 +10,6 @@ TMUX is a very capable process manager.
|
||||
- session = is a set of windows, it has a name and groups windows
|
||||
- window = is typically one process running (you can have panes but in our implementation we skip this)
|
||||
|
||||
|
||||
## structure
|
||||
|
||||
tmux library provides functions for managing tmux sessions
|
||||
@@ -20,72 +18,156 @@ tmux library provides functions for managing tmux sessions
|
||||
- then windows (is where you see the app running)
|
||||
- then panes in windows (we don't support yet)
|
||||
|
||||
|
||||
## to attach to a tmux session
|
||||
|
||||
> TODO:
|
||||
>
|
||||
## HeroScript Declarative Support
|
||||
|
||||
The tmux module supports declarative configuration through heroscript, allowing you to define tmux sessions, windows, and panes in a structured way.
|
||||
|
||||
## HeroScript Usage Examples
|
||||
### Running HeroScript
|
||||
|
||||
### Imperative
|
||||
```bash
|
||||
hero run -p <heroscript_file>
|
||||
```
|
||||
|
||||
action after action builds the output
|
||||
### Supported Actions
|
||||
|
||||
#### Session Management
|
||||
|
||||
```heroscript
|
||||
!!tmux.session_create
|
||||
// Create a new session
|
||||
!!tmux.session_create
|
||||
name:'mysession'
|
||||
reset:true
|
||||
reset:true // Optional: delete existing session first
|
||||
|
||||
!!tmux.session_delete
|
||||
// Delete a session
|
||||
!!tmux.session_delete
|
||||
name:'mysession'
|
||||
```
|
||||
|
||||
!!tmux.window_create
|
||||
#### Window Management
|
||||
|
||||
```heroscript
|
||||
// Create a new window
|
||||
!!tmux.window_create
|
||||
name:"mysession|mywindow" // Format: session|window
|
||||
cmd:'htop' // Optional: command to run
|
||||
env:'VAR1=value1,VAR2=value2' // Optional: environment variables
|
||||
reset:true // Optional: recreate if exists
|
||||
|
||||
// Delete a window
|
||||
!!tmux.window_delete
|
||||
name:"mysession|mywindow"
|
||||
cmd:'htop'
|
||||
env:'VAR1=value1,VAR2=value2'
|
||||
reset:true
|
||||
```
|
||||
|
||||
!!tmux.window_delete
|
||||
name:"mysession|mywindow"
|
||||
#### Pane Management
|
||||
|
||||
!!tmux.pane_execute
|
||||
name:"mysession|mywindow|mypane"
|
||||
```heroscript
|
||||
// Execute command in a pane
|
||||
!!tmux.pane_execute
|
||||
name:"mysession|mywindow|mypane" // Format: session|window|pane
|
||||
cmd:'ls -la'
|
||||
|
||||
!!tmux.pane_kill
|
||||
// Kill a pane
|
||||
!!tmux.pane_kill
|
||||
name:"mysession|mywindow|mypane"
|
||||
```
|
||||
|
||||
### Declarative
|
||||
#### Ttyd Management
|
||||
|
||||
```heroscript
|
||||
!!tmux.session_ensure
|
||||
// Start ttyd for session access
|
||||
!!tmux.session_ttyd
|
||||
name:'mysession'
|
||||
port:8080
|
||||
editable:true // Optional: allows write access
|
||||
|
||||
!!tmux.window_ensure
|
||||
// Start ttyd for window access
|
||||
!!tmux.window_ttyd
|
||||
name:"mysession|mywindow"
|
||||
cat:"4pane" //we support 16pane, 12pane, 8pane, 6pane, 4pane, 2pane, 1pane
|
||||
port:8081
|
||||
editable:false // Optional: read-only access
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"mysession|mywindow|1"
|
||||
label:'ls'
|
||||
cmd:'ls -la ${HOME}'
|
||||
// Stop ttyd for session
|
||||
!!tmux.session_ttyd_stop
|
||||
name:'mysession'
|
||||
port:8080
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"mysession|mywindow|2"
|
||||
label:'ps'
|
||||
cmd:'ps aux'
|
||||
// Stop ttyd for window
|
||||
!!tmux.window_ttyd_stop
|
||||
name:"mysession|mywindow"
|
||||
port:8081
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"mysession|mywindow|3"
|
||||
label:'echo'
|
||||
env:'VAR1=value1,VAR2=value2'
|
||||
cmd:'echo $VAR1'
|
||||
// Stop all ttyd processes
|
||||
!!tmux.ttyd_stop_all
|
||||
```
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"mysession|mywindow|4"
|
||||
label:'htop'
|
||||
cmd:'htop'
|
||||
### Complete Example
|
||||
|
||||
```
|
||||
```heroscript
|
||||
#!/usr/bin/env hero
|
||||
|
||||
// Create development environment
|
||||
!!tmux.session_create
|
||||
name:'dev'
|
||||
reset:true
|
||||
|
||||
!!tmux.window_create
|
||||
name:"dev|editor"
|
||||
cmd:'vim'
|
||||
reset:true
|
||||
|
||||
!!tmux.window_create
|
||||
name:"dev|server"
|
||||
cmd:'python3 -m http.server 8000'
|
||||
env:'PORT=8000,DEBUG=true'
|
||||
reset:true
|
||||
|
||||
!!tmux.pane_execute
|
||||
name:"dev|editor|main"
|
||||
cmd:'echo "Welcome to development!"'
|
||||
```
|
||||
|
||||
### Naming Convention
|
||||
|
||||
- **Sessions**: Simple names like `dev`, `monitoring`, `main`
|
||||
- **Windows**: Use pipe separator: `session|window` (e.g., `dev|editor`)
|
||||
- **Panes**: Use pipe separator: `session|window|pane` (e.g., `dev|editor|main`)
|
||||
|
||||
Names are automatically normalized using `texttools.name_fix()` for consistency.
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Setup and Cleanup Scripts
|
||||
|
||||
Two example heroscripts are provided to demonstrate complete tmux environment management:
|
||||
|
||||
#### 1. Setup Script (`tmux_setup.heroscript`)
|
||||
|
||||
Creates a complete development environment with multiple sessions and windows:
|
||||
|
||||
```bash
|
||||
hero run examples/tmux/tmux_setup.heroscript
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
||||
- **dev session** with editor, server, logs, and a 4-pane services window
|
||||
- **monitoring session** with htop and network monitoring windows
|
||||
- **Web access** via ttyd on ports 8080, 8081, and 7681
|
||||
|
||||
#### 2. Cleanup Script (`tmux_cleanup.heroscript`)
|
||||
|
||||
Tears down all created tmux resources:
|
||||
|
||||
```bash
|
||||
hero run examples/tmux/tmux_cleanup.heroscript
|
||||
```
|
||||
|
||||
This removes:
|
||||
|
||||
- All windows from both sessions
|
||||
- Both dev and monitoring sessions
|
||||
- All associated panes
|
||||
- All ttyd web processes (ports 8080, 8081, 7681)
|
||||
|
||||
@@ -3,86 +3,6 @@ module tmux
|
||||
import freeflowuniverse.herolib.osal.core as osal
|
||||
import freeflowuniverse.herolib.data.ourtime
|
||||
import time
|
||||
// import freeflowuniverse.herolib.session
|
||||
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 {
|
||||
@@ -106,40 +26,15 @@ pub fn (mut p Pane) stats() !ProcessStats {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Use ps_tool to get process information
|
||||
process_info := osal.processinfo_get(p.pid) or {
|
||||
return error('Cannot get stats for PID ${p.pid}: ${err}')
|
||||
}
|
||||
|
||||
lines := result.split_into_lines()
|
||||
if lines.len < 2 {
|
||||
return error('Process ${p.pid} not found')
|
||||
}
|
||||
|
||||
// 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: cpu_percent
|
||||
memory_percent: memory_percent
|
||||
memory_bytes: memory_bytes
|
||||
cpu_percent: f64(process_info.cpu_perc)
|
||||
memory_percent: f64(process_info.mem_perc)
|
||||
memory_bytes: u64(process_info.rss * 1024) // rss is in KB, convert to bytes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,16 +61,42 @@ pub fn (mut s Session) scan() ! {
|
||||
|
||||
mut current_windows := map[string]bool{}
|
||||
for line in result.split_into_lines() {
|
||||
if line.contains('|') {
|
||||
parts := line.split('|')
|
||||
line_trimmed := line.trim_space()
|
||||
if line_trimmed.len == 0 {
|
||||
continue
|
||||
}
|
||||
if line_trimmed.contains('|') {
|
||||
parts := line_trimmed.split('|')
|
||||
if parts.len >= 3 && parts[0].len > 0 && parts[1].len > 0 {
|
||||
window_name := texttools.name_fix(parts[0])
|
||||
// Safely extract window name with additional validation
|
||||
raw_window_name := parts[0].trim_space()
|
||||
if raw_window_name.len == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use safer name processing instead of texttools.name_fix
|
||||
mut window_name := raw_window_name.to_lower().trim_space()
|
||||
// Replace problematic characters with underscores
|
||||
window_name = window_name.replace(' ', '_').replace('-', '_').replace('.',
|
||||
'_')
|
||||
|
||||
// Remove any non-ASCII characters safely
|
||||
mut safe_name := ''
|
||||
for c in window_name {
|
||||
if c.is_letter() || c.is_digit() || c == `_` {
|
||||
safe_name += c.ascii_str()
|
||||
}
|
||||
}
|
||||
window_name = safe_name
|
||||
|
||||
if window_name.len == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
window_id := parts[1].replace('@', '').int()
|
||||
window_active := parts[2] == '1'
|
||||
|
||||
// Safe map assignment
|
||||
current_windows[window_name] = true
|
||||
|
||||
// Update existing window or create new one
|
||||
@@ -102,7 +128,24 @@ pub fn (mut s Session) scan() ! {
|
||||
}
|
||||
|
||||
// Remove windows that no longer exist in tmux
|
||||
s.windows = s.windows.filter(it.name.len > 0 && current_windows[it.name] == true)
|
||||
mut valid_windows := []&Window{}
|
||||
for window in s.windows {
|
||||
// Safety check: ensure window.name is valid
|
||||
if window.name.len > 0 {
|
||||
// Avoid map access entirely - check if window still exists by comparing with current windows
|
||||
mut window_exists := false
|
||||
for current_name, _ in current_windows {
|
||||
if window.name == current_name {
|
||||
window_exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if window_exists {
|
||||
valid_windows << window
|
||||
}
|
||||
}
|
||||
}
|
||||
s.windows = valid_windows
|
||||
}
|
||||
|
||||
// window_name is the name of the window in session main (will always be called session main)
|
||||
@@ -217,7 +260,7 @@ pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window {
|
||||
if args.name.len == 0 {
|
||||
return error('Window name cannot be empty')
|
||||
}
|
||||
args.name = texttools.name_fix(args.name)
|
||||
args.name = texttools.name_fix_token(args.name)
|
||||
for w in s.windows {
|
||||
if w.name.len > 0 && w.name == args.name {
|
||||
if (args.id > 0 && w.id == args.id) || args.id == 0 {
|
||||
@@ -231,7 +274,7 @@ pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window {
|
||||
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)
|
||||
args.name = texttools.name_fix_token(args.name)
|
||||
if !(s.window_exist(args)) {
|
||||
return
|
||||
}
|
||||
@@ -284,3 +327,24 @@ pub fn (mut s Session) run_ttyd(args TtydArgs) ! {
|
||||
pub fn (mut s Session) run_ttyd_readonly(port int) ! {
|
||||
s.run_ttyd(port: port, editable: false)!
|
||||
}
|
||||
|
||||
// Stop ttyd for this session by killing the process on the specified port
|
||||
pub fn (mut s Session) stop_ttyd(port int) ! {
|
||||
// Kill any process running on the specified port
|
||||
cmd := 'lsof -ti:${port} | xargs kill -9'
|
||||
osal.execute_silent(cmd) or {
|
||||
// Ignore error if no process is found on the port
|
||||
// This is normal when no ttyd is running on that port
|
||||
}
|
||||
println('ttyd stopped for session ${s.name} on port ${port} (if it was running)')
|
||||
}
|
||||
|
||||
// Stop all ttyd processes (kills all ttyd processes system-wide)
|
||||
pub fn stop_all_ttyd() ! {
|
||||
cmd := 'pkill ttyd'
|
||||
osal.execute_silent(cmd) or {
|
||||
// Ignore error if no ttyd processes are found (exit code 1)
|
||||
// This is normal when no ttyd processes are running
|
||||
}
|
||||
println('All ttyd processes stopped (if any were running)')
|
||||
}
|
||||
|
||||
@@ -37,11 +37,27 @@ pub fn (mut w Window) scan() ! {
|
||||
|
||||
mut current_panes := map[int]bool{}
|
||||
for line in result.split_into_lines() {
|
||||
if line.contains('|') {
|
||||
parts := line.split('|')
|
||||
if parts.len >= 3 {
|
||||
pane_id := parts[0].replace('%', '').int()
|
||||
pane_pid := parts[1].int()
|
||||
line_trimmed := line.trim_space()
|
||||
if line_trimmed.len == 0 {
|
||||
continue
|
||||
}
|
||||
if line_trimmed.contains('|') {
|
||||
parts := line_trimmed.split('|')
|
||||
if parts.len >= 3 && parts[0].len > 0 && parts[1].len > 0 {
|
||||
// Safely parse pane ID
|
||||
pane_id_str := parts[0].replace('%', '').trim_space()
|
||||
if pane_id_str.len == 0 {
|
||||
continue
|
||||
}
|
||||
pane_id := pane_id_str.int()
|
||||
|
||||
// Safely parse PID
|
||||
pane_pid_str := parts[1].trim_space()
|
||||
if pane_pid_str.len == 0 {
|
||||
continue
|
||||
}
|
||||
pane_pid := pane_pid_str.int()
|
||||
|
||||
pane_active := parts[2] == '1'
|
||||
pane_cmd := if parts.len > 3 { parts[3] } else { '' }
|
||||
|
||||
@@ -77,7 +93,14 @@ pub fn (mut w Window) scan() ! {
|
||||
}
|
||||
|
||||
// Remove panes that no longer exist
|
||||
w.panes = w.panes.filter(current_panes[it.id] == true)
|
||||
mut valid_panes := []&Pane{}
|
||||
for pane in w.panes {
|
||||
// Use safe map access with 'in' operator first
|
||||
if pane.id in current_panes && current_panes[pane.id] == true {
|
||||
valid_panes << pane
|
||||
}
|
||||
}
|
||||
w.panes = valid_panes
|
||||
}
|
||||
|
||||
pub fn (mut w Window) stop() ! {
|
||||
@@ -237,14 +260,8 @@ pub fn (mut w Window) pane_split(args PaneSplitArgs) !&Pane {
|
||||
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}')
|
||||
// Return the new pane reference
|
||||
return &new_pane
|
||||
}
|
||||
|
||||
// Split pane horizontally (side by side)
|
||||
@@ -289,3 +306,14 @@ pub fn (mut w Window) run_ttyd(args TtydArgs) ! {
|
||||
pub fn (mut w Window) run_ttyd_readonly(port int) ! {
|
||||
w.run_ttyd(port: port, editable: false)!
|
||||
}
|
||||
|
||||
// Stop ttyd for this window by killing the process on the specified port
|
||||
pub fn (mut w Window) stop_ttyd(port int) ! {
|
||||
// Kill any process running on the specified port
|
||||
cmd := 'lsof -ti:${port} | xargs kill -9'
|
||||
osal.execute_silent(cmd) or {
|
||||
// Ignore error if no process is found on the port
|
||||
// This is normal when no ttyd is running on that port
|
||||
}
|
||||
println('ttyd stopped for window ${w.name} on port ${port} (if it was running)')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user