use std::collections::HashMap;
use std::io::{Read, Write};
use std::sync::{Arc, Mutex};
use base64::Engine;
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, State};
pub struct TerminalSession {
pub pty_master: Box<dyn MasterPty + Send>,
pub writer: Box<dyn Write + Send>,
pub child: Box<dyn Child + Send>,
pub target: String,
}
#[derive(Default, Clone)]
pub struct TerminalState {
pub sessions: Arc<Mutex<HashMap<String, TerminalSession>>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TerminalInfo {
pub session_id: String,
pub target: String,
}
#[tauri::command]
pub async fn start_terminal_session(
target: Option<String>,
cols: Option<u16>,
rows: Option<u16>,
app: AppHandle,
vault_state: State<'_, crate::vault::VaultState>,
term_state: State<'_, TerminalState>,
) -> Result<String, String> {
let target_name = target.unwrap_or_else(|| "local".to_string());
if target_name != "local" {
crate::vault::ensure_unlocked(&vault_state)?;
}
let pty_system = native_pty_system();
let pair = pty_system
.openpty(PtySize {
rows: rows.unwrap_or(24),
cols: cols.unwrap_or(80),
pixel_width: 0,
pixel_height: 0,
})
.map_err(|e| format!("Konnte PTY nicht öffnen: {}", e))?;
let cmd = if target_name == "local" {
#[cfg(windows)]
{
let mut c = CommandBuilder::new("powershell.exe");
c.args(["-NoLogo"]);
c
}
#[cfg(unix)]
{
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
CommandBuilder::new(shell)
}
} else {
let sb_ssh = which::which("sb-ssh")
.or_else(|_| {
dirs::home_dir()
.map(|h| {
h.join(".cargo").join("bin").join(if cfg!(windows) {
"sb-ssh.exe"
} else {
"sb-ssh"
})
})
.filter(|p| p.exists())
.ok_or("Nicht gefunden")
})
.map_err(|_| "sb-ssh Binary wurde nicht im System gefunden".to_string())?;
let mut c = CommandBuilder::new(sb_ssh);
c.args(["connect", &target_name]);
c
};
let child = pair
.slave
.spawn_command(cmd)
.map_err(|e| format!("Prozess-Start fehlgeschlagen: {}", e))?;
let mut reader = pair
.master
.try_clone_reader()
.map_err(|e| format!("Konnte PTY Reader nicht klonen: {}", e))?;
let writer = pair
.master
.take_writer()
.map_err(|e| format!("Konnte PTY Writer nicht entnehmen: {}", e))?;
let session_id = format!("term-{}", uuid_simple());
let app_handle = app.clone();
let s_id = session_id.clone();
std::thread::spawn(move || {
let mut buffer = [0u8; 4096];
loop {
match reader.read(&mut buffer) {
Ok(0) => break, Ok(n) => {
let b64 = base64::engine::general_purpose::STANDARD.encode(&buffer[..n]);
let _ = app_handle.emit(&format!("terminal-out-{}", s_id), b64);
}
Err(_) => break,
}
}
let _ = app_handle.emit(&format!("terminal-exit-{}", s_id), 0);
});
{
let mut sessions = term_state.sessions.lock().map_err(|e| e.to_string())?;
sessions.insert(
session_id.clone(),
TerminalSession {
pty_master: pair.master,
writer,
child,
target: target_name,
},
);
}
Ok(session_id)
}
#[tauri::command]
pub fn write_terminal_data(
session_id: String,
data: String,
term_state: State<'_, TerminalState>,
) -> Result<(), String> {
let mut sessions = term_state.sessions.lock().map_err(|e| e.to_string())?;
if let Some(session) = sessions.get_mut(&session_id) {
session
.writer
.write_all(data.as_bytes())
.map_err(|e| format!("Fehler beim Schreiben ins PTY: {}", e))?;
session
.writer
.flush()
.map_err(|e| format!("Flush fehlgeschlagen: {}", e))?;
Ok(())
} else {
Err(format!("Terminal-Sitzung '{}' existiert nicht", session_id))
}
}
#[tauri::command]
pub fn resize_terminal(
session_id: String,
cols: u16,
rows: u16,
term_state: State<'_, TerminalState>,
) -> Result<(), String> {
let mut sessions = term_state.sessions.lock().map_err(|e| e.to_string())?;
if let Some(session) = sessions.get_mut(&session_id) {
session
.pty_master
.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.map_err(|e| format!("Resize fehlgeschlagen: {}", e))?;
Ok(())
} else {
Err(format!("Terminal-Sitzung '{}' nicht gefunden", session_id))
}
}
#[tauri::command]
pub fn close_terminal_session(
session_id: String,
term_state: State<'_, TerminalState>,
) -> Result<(), String> {
let mut sessions = term_state.sessions.lock().map_err(|e| e.to_string())?;
if let Some(mut session) = sessions.remove(&session_id) {
let _ = session.child.kill();
Ok(())
} else {
Ok(())
}
}
fn uuid_simple() -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
let num: u64 = rng.gen();
format!("{:x}", num)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pty_system_available() {
let pty_system = native_pty_system();
let pair = pty_system.openpty(PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
});
assert!(pair.is_ok(), "PTY could not be initialized");
}
}