refactor: table-style display with live spinner (🔄→✅/❌)
- Remove \t-based alignment (fixes emoji spacing inconsistencies)
- New display.rs module: table rows with dynamic terminal-width columns
- Columns: action_emoji | name (14ch) | payload (55%) | status_emoji | result (45%)
- cmd_start() prints 🔄 spinner, cmd_done() overwrites line in-place via ANSI cursor-up
- Payload and result truncated to column width with ellipsis
- Consistent 2-space gaps after every emoji (no tab stops)
- Add terminal_size crate for dynamic width (fallback: 120)
This commit is contained in:
parent
7c0341a5f3
commit
959a00ff8a
3 changed files with 212 additions and 154 deletions
|
|
@ -24,6 +24,7 @@ base64 = "0.22"
|
|||
png = "0.17"
|
||||
futures-util = "0.3"
|
||||
colored = "2"
|
||||
terminal_size = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1"
|
||||
|
|
|
|||
105
crates/client/src/display.rs
Normal file
105
crates/client/src/display.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/// Terminal display helpers — table-style command rows with live spinner.
|
||||
///
|
||||
/// Layout (no borders, aligned columns):
|
||||
/// {2 spaces}{action_emoji}{2 spaces}{name:<NAME_W}{2 spaces}{payload:<payload_w}{2 spaces}{status_emoji}{2 spaces}{result}
|
||||
use std::io::Write;
|
||||
|
||||
use colored::Colorize;
|
||||
|
||||
// Fixed column widths (in terminal display columns, ASCII-only content assumed for name/payload/result)
|
||||
const NAME_W: usize = 14;
|
||||
const MIN_PAYLOAD_W: usize = 10;
|
||||
const MIN_RESULT_W: usize = 10;
|
||||
// Overhead: 2 (indent) + 2 (action emoji) + 2 (gap) + NAME_W + 2 (gap) + 2 (gap) + 2 (status emoji) + 2 (gap)
|
||||
const FIXED_OVERHEAD: usize = 2 + 2 + 2 + NAME_W + 2 + 2 + 2 + 2;
|
||||
|
||||
pub fn terminal_width() -> usize {
|
||||
terminal_size::terminal_size()
|
||||
.map(|(w, _)| w.0 as usize)
|
||||
.unwrap_or(120)
|
||||
.max(60)
|
||||
}
|
||||
|
||||
/// Split remaining space between payload and result columns.
|
||||
/// payload gets ~55%, result gets the rest; both have minimums.
|
||||
fn col_widths() -> (usize, usize) {
|
||||
let tw = terminal_width();
|
||||
let remaining = tw.saturating_sub(FIXED_OVERHEAD).max(MIN_PAYLOAD_W + MIN_RESULT_W);
|
||||
let payload_w = (remaining * 55 / 100).max(MIN_PAYLOAD_W);
|
||||
let result_w = remaining.saturating_sub(payload_w).max(MIN_RESULT_W);
|
||||
(payload_w, result_w)
|
||||
}
|
||||
|
||||
/// Truncate a string to at most `max` Unicode chars, appending `…` if cut.
|
||||
pub fn trunc(s: &str, max: usize) -> String {
|
||||
if max == 0 {
|
||||
return String::new();
|
||||
}
|
||||
let mut chars = s.chars();
|
||||
let truncated: String = chars.by_ref().take(max).collect();
|
||||
if chars.next().is_some() {
|
||||
// Replace last char with ellipsis
|
||||
let mut t: String = truncated.chars().take(max.saturating_sub(1)).collect();
|
||||
t.push('…');
|
||||
t
|
||||
} else {
|
||||
truncated
|
||||
}
|
||||
}
|
||||
|
||||
/// Format one table row into a String (no trailing newline).
|
||||
fn format_row(action: &str, name: &str, payload: &str, status: &str, result: &str) -> String {
|
||||
let (payload_w, result_w) = col_widths();
|
||||
let p = trunc(payload, payload_w);
|
||||
let r = trunc(result, result_w);
|
||||
format!(
|
||||
" {} {:<name_w$} {:<payload_w$} {} {}",
|
||||
action,
|
||||
name,
|
||||
p,
|
||||
status,
|
||||
r,
|
||||
name_w = NAME_W,
|
||||
payload_w = payload_w,
|
||||
)
|
||||
}
|
||||
|
||||
/// Print the "running" row (🔄 spinner) without extra newline logic.
|
||||
/// Returns the row string so callers can reuse it for the done line.
|
||||
pub fn cmd_start(action: &str, name: &str, payload: &str) {
|
||||
let line = format_row(action, name, payload, "🔄", "");
|
||||
println!("{}", line);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
/// Overwrite the previous line (cursor up + clear) with the completed row.
|
||||
pub fn cmd_done(action: &str, name: &str, payload: &str, success: bool, result: &str) {
|
||||
let status = if success { "✅" } else { "❌" };
|
||||
let result_colored = if success {
|
||||
result.to_string()
|
||||
} else {
|
||||
result.red().to_string()
|
||||
};
|
||||
let line = format_row(action, name, payload, status, &result_colored);
|
||||
// \x1b[1A = cursor up 1, \r = go to col 0, \x1b[2K = clear line
|
||||
print!("\x1b[1A\r\x1b[2K{}\n", line);
|
||||
let _ = std::io::stdout().flush();
|
||||
crate::logger::write_line(if success { "OK" } else { "ERROR" }, &format!("{name} {payload} → {result}"));
|
||||
}
|
||||
|
||||
/// Plain status line (banner / connection info), not in table format.
|
||||
/// Uses 2 leading spaces + emoji + 2 spaces + message (no tab).
|
||||
pub fn status(emoji: &str, msg: &str) {
|
||||
println!(" {} {}", emoji, msg);
|
||||
}
|
||||
|
||||
/// Same as status() but also logs to file.
|
||||
pub fn ok(emoji: &str, msg: &str) {
|
||||
println!(" {} {}", emoji, msg);
|
||||
crate::logger::write_line("OK", msg);
|
||||
}
|
||||
|
||||
pub fn err(emoji: &str, msg: &str) {
|
||||
println!(" {} {}", emoji, msg.red());
|
||||
crate::logger::write_line("ERROR", msg);
|
||||
}
|
||||
|
|
@ -13,28 +13,19 @@ use base64::Engine;
|
|||
use helios_common::{ClientMessage, ServerMessage};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod display;
|
||||
mod logger;
|
||||
mod shell;
|
||||
mod screenshot;
|
||||
mod input;
|
||||
mod windows_mgmt;
|
||||
|
||||
// ─── CLI Output ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Truncate a string to at most `max_chars` Unicode characters.
|
||||
fn trunc(s: &str, max_chars: usize) -> String {
|
||||
let mut chars = s.chars();
|
||||
let truncated: String = chars.by_ref().take(max_chars).collect();
|
||||
if chars.next().is_some() {
|
||||
format!("{}…", truncated)
|
||||
} else {
|
||||
truncated
|
||||
}
|
||||
}
|
||||
// Re-export trunc for use in this file
|
||||
use display::trunc;
|
||||
|
||||
fn banner() {
|
||||
println!();
|
||||
println!(" {}\tHELIOS REMOTE ({})", "☀".yellow().bold(), env!("GIT_COMMIT"));
|
||||
println!(" {} HELIOS REMOTE ({})", "☀".yellow().bold(), env!("GIT_COMMIT"));
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let admin = is_admin();
|
||||
|
|
@ -43,7 +34,7 @@ fn banner() {
|
|||
} else {
|
||||
("👤", "user (no admin)".yellow().to_string())
|
||||
};
|
||||
println!(" {}\t{}", icon, admin_str);
|
||||
println!(" {} {}", icon, admin_str);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
|
@ -70,36 +61,6 @@ fn enable_ansi() {
|
|||
}
|
||||
}
|
||||
|
||||
macro_rules! log_status {
|
||||
($($arg:tt)*) => {
|
||||
println!(" {}", format!($($arg)*));
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! log_ok {
|
||||
($($arg:tt)*) => {{
|
||||
let msg = format!($($arg)*);
|
||||
println!(" {}\t{}", "✅", msg);
|
||||
logger::write_line("OK", &msg);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! log_err {
|
||||
($($arg:tt)*) => {{
|
||||
let msg = format!($($arg)*);
|
||||
println!(" {}\t{}", "❌", msg);
|
||||
logger::write_line("ERROR", &msg);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! log_cmd {
|
||||
($emoji:expr, $($arg:tt)*) => {{
|
||||
let msg = format!($($arg)*);
|
||||
println!(" {}\t{}", $emoji, msg);
|
||||
logger::write_line("CMD", &msg);
|
||||
}};
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -193,14 +154,14 @@ async fn main() {
|
|||
let config = match Config::load() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
log_status!("No config found — first-time setup");
|
||||
display::status("ℹ", "No config found — first-time setup");
|
||||
println!();
|
||||
let c = prompt_config();
|
||||
println!();
|
||||
if let Err(e) = c.save() {
|
||||
log_err!("Failed to save config: {e}");
|
||||
display::err("❌", &format!("Failed to save config: {e}"));
|
||||
} else {
|
||||
log_ok!("Config saved");
|
||||
display::ok("✅", "Config saved");
|
||||
}
|
||||
c
|
||||
}
|
||||
|
|
@ -214,7 +175,7 @@ async fn main() {
|
|||
let mut cfg = config.clone();
|
||||
cfg.session_id = Some(id.to_string());
|
||||
if let Err(e) = cfg.save() {
|
||||
log_err!("Failed to save session_id: {e}");
|
||||
display::err("❌", &format!("Failed to save session_id: {e}"));
|
||||
}
|
||||
id
|
||||
}
|
||||
|
|
@ -235,7 +196,7 @@ async fn main() {
|
|||
.next()
|
||||
.unwrap_or(&config.relay_url);
|
||||
|
||||
log_status!("🌐 Connecting to {}...", host);
|
||||
display::status("🌐", &format!("Connecting to {}...", host));
|
||||
|
||||
let tls_connector = TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
|
|
@ -246,13 +207,13 @@ async fn main() {
|
|||
match connect_async_tls_with_config(&config.relay_url, None, false, Some(connector)).await {
|
||||
Ok((ws_stream, _)) => {
|
||||
let label = config.label.clone().unwrap_or_else(|| hostname());
|
||||
log_ok!(
|
||||
display::ok("✅", &format!(
|
||||
"Connected {} {} {} Session {}",
|
||||
"·".dimmed(),
|
||||
label.bold(),
|
||||
"·".dimmed(),
|
||||
sid.to_string().dimmed()
|
||||
);
|
||||
));
|
||||
println!();
|
||||
backoff = Duration::from_secs(1);
|
||||
|
||||
|
|
@ -264,7 +225,7 @@ async fn main() {
|
|||
};
|
||||
let hello_json = serde_json::to_string(&hello).unwrap();
|
||||
if let Err(e) = write.send(Message::Text(hello_json)).await {
|
||||
log_err!("Failed to send Hello: {e}");
|
||||
display::err("❌", &format!("Failed to send Hello: {e}"));
|
||||
tokio::time::sleep(backoff).await;
|
||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||
continue;
|
||||
|
|
@ -278,7 +239,7 @@ async fn main() {
|
|||
let server_msg: ServerMessage = match serde_json::from_str(&text) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log_err!("Failed to parse server message: {e}");
|
||||
display::err("❌", &format!("Failed to parse server message: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
|
@ -294,13 +255,13 @@ async fn main() {
|
|||
let json = match serde_json::to_string(&response) {
|
||||
Ok(j) => j,
|
||||
Err(e) => {
|
||||
log_err!("Failed to serialize response: {e}");
|
||||
display::err("❌", &format!("Failed to serialize response: {e}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut w = write_clone.lock().await;
|
||||
if let Err(e) = w.send(Message::Text(json)).await {
|
||||
log_err!("Failed to send response: {e}");
|
||||
display::err("❌", &format!("Failed to send response: {e}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -309,11 +270,11 @@ async fn main() {
|
|||
let _ = w.send(Message::Pong(data)).await;
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
log_err!("Connection lost — reconnecting...");
|
||||
display::err("❌", "Connection lost — reconnecting...");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("Connection lost: {e} — reconnecting...");
|
||||
display::err("❌", &format!("Connection lost: {e} — reconnecting..."));
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -321,7 +282,7 @@ async fn main() {
|
|||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("Connection failed: {e}");
|
||||
display::err("❌", &format!("Connection failed: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -359,37 +320,30 @@ async fn handle_message(
|
|||
) -> ClientMessage {
|
||||
match msg {
|
||||
ServerMessage::WindowScreenshotRequest { request_id, window_id } => {
|
||||
log_cmd!("📷", "screenshot window {}", window_id);
|
||||
let payload = format!("window {window_id}");
|
||||
display::cmd_start("📷", "screenshot", &payload);
|
||||
match screenshot::take_window_screenshot(window_id) {
|
||||
Ok((image_base64, width, height)) => {
|
||||
log_ok!("Done {} {}×{}", "·".dimmed(), width, height);
|
||||
display::cmd_done("📷", "screenshot", &payload, true, &format!("{width}×{height}"));
|
||||
ClientMessage::ScreenshotResponse { request_id, image_base64, width, height }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("Window screenshot failed: {e}");
|
||||
display::cmd_done("📷", "screenshot", &payload, false, &format!("{e}"));
|
||||
ClientMessage::Error { request_id, message: format!("Window screenshot failed: {e}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::ScreenshotRequest { request_id } => {
|
||||
log_cmd!("📷", "screenshot");
|
||||
display::cmd_start("📷", "screenshot", "");
|
||||
match screenshot::take_screenshot() {
|
||||
Ok((image_base64, width, height)) => {
|
||||
log_ok!("Done {} {}×{}", "·".dimmed(), width, height);
|
||||
ClientMessage::ScreenshotResponse {
|
||||
request_id,
|
||||
image_base64,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
display::cmd_done("📷", "screenshot", "", true, &format!("{width}×{height}"));
|
||||
ClientMessage::ScreenshotResponse { request_id, image_base64, width, height }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("Screenshot failed: {e}");
|
||||
ClientMessage::Error {
|
||||
request_id,
|
||||
message: format!("Screenshot failed: {e}"),
|
||||
}
|
||||
display::cmd_done("📷", "screenshot", "", false, &format!("{e}"));
|
||||
ClientMessage::Error { request_id, message: format!("Screenshot failed: {e}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -398,15 +352,14 @@ async fn handle_message(
|
|||
let _title = title.unwrap_or_else(|| "Helios Remote".to_string());
|
||||
#[cfg(windows)]
|
||||
let title = _title.clone();
|
||||
log_cmd!("💬", "prompt › {}", trunc(&message, 60));
|
||||
let payload = trunc(&message, 60);
|
||||
display::cmd_start("💬", "prompt", &payload);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, HWND_DESKTOP};
|
||||
let msg_wide: Vec<u16> = message.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let title_wide: Vec<u16> = title.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
// Run blocking MessageBox in a thread so we don't block the async runtime
|
||||
let msg_clone = message.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
unsafe {
|
||||
MessageBoxW(
|
||||
|
|
@ -417,164 +370,153 @@ async fn handle_message(
|
|||
);
|
||||
}
|
||||
}).await.ok();
|
||||
log_ok!("User confirmed: {}", trunc(&msg_clone, 40));
|
||||
display::cmd_done("💬", "prompt", &payload, true, "confirmed");
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
// On non-Windows just log it
|
||||
println!(" [PROMPT] {}", message);
|
||||
display::cmd_done("💬", "prompt", &payload, true, "shown");
|
||||
}
|
||||
ClientMessage::Ack { request_id }
|
||||
}
|
||||
|
||||
ServerMessage::ExecRequest { request_id, command, timeout_ms } => {
|
||||
let cmd_display = trunc(&command, 60);
|
||||
log_cmd!("⚡", "exec › {}", cmd_display);
|
||||
let payload = trunc(&command, 80);
|
||||
display::cmd_start("⚡", "exec", &payload);
|
||||
let mut sh = shell.lock().await;
|
||||
match sh.run(&command, timeout_ms).await {
|
||||
Ok((stdout, stderr, exit_code)) => {
|
||||
let out = stdout.trim().lines().next().unwrap_or("").to_string();
|
||||
let out_display = trunc(&out, 60);
|
||||
if exit_code != 0 {
|
||||
if out_display.is_empty() {
|
||||
log_err!("exit {}", exit_code);
|
||||
let first_line = stdout.trim().lines().next().unwrap_or("").to_string();
|
||||
let result = if exit_code != 0 {
|
||||
if first_line.is_empty() {
|
||||
format!("exit {exit_code}")
|
||||
} else {
|
||||
log_err!("{} {} exit {}", out_display, "·".dimmed(), exit_code);
|
||||
format!("{first_line} · exit {exit_code}")
|
||||
}
|
||||
} else if out_display.is_empty() {
|
||||
log_ok!("exit 0");
|
||||
} else if first_line.is_empty() {
|
||||
"exit 0".to_string()
|
||||
} else {
|
||||
log_ok!("{} {} exit 0", out_display, "·".dimmed());
|
||||
}
|
||||
format!("{first_line} · exit 0")
|
||||
};
|
||||
display::cmd_done("⚡", "exec", &payload, exit_code == 0, &result);
|
||||
let _ = stderr;
|
||||
ClientMessage::ExecResponse {
|
||||
request_id,
|
||||
stdout,
|
||||
stderr,
|
||||
exit_code,
|
||||
}
|
||||
ClientMessage::ExecResponse { request_id, stdout, stderr, exit_code }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("exec failed: {e}");
|
||||
ClientMessage::Error {
|
||||
request_id,
|
||||
message: format!(
|
||||
"Exec failed for command {:?}.\nError: {e}",
|
||||
command
|
||||
),
|
||||
}
|
||||
display::cmd_done("⚡", "exec", &payload, false, &format!("exec failed: {e}"));
|
||||
ClientMessage::Error { request_id, message: format!("Exec failed for command {:?}.\nError: {e}", command) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::ClickRequest { request_id, x, y, button } => {
|
||||
log_cmd!("🖱 ", "click ({x}, {y}) {:?}", button);
|
||||
let payload = format!("({x}, {y}) {button:?}");
|
||||
display::cmd_start("🖱", "click", &payload);
|
||||
match input::click(x, y, &button) {
|
||||
Ok(()) => {
|
||||
log_ok!("Done");
|
||||
display::cmd_done("🖱", "click", &payload, true, "done");
|
||||
ClientMessage::Ack { request_id }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("click failed: {e}");
|
||||
ClientMessage::Error {
|
||||
request_id,
|
||||
message: format!("Click at ({x},{y}) failed: {e}"),
|
||||
}
|
||||
display::cmd_done("🖱", "click", &payload, false, &format!("{e}"));
|
||||
ClientMessage::Error { request_id, message: format!("Click at ({x},{y}) failed: {e}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::TypeRequest { request_id, text } => {
|
||||
log_cmd!("⌨ ", "type {} chars", text.len());
|
||||
let payload = format!("{} chars", text.len());
|
||||
display::cmd_start("⌨", "type", &payload);
|
||||
match input::type_text(&text) {
|
||||
Ok(()) => {
|
||||
log_ok!("Done");
|
||||
display::cmd_done("⌨", "type", &payload, true, "done");
|
||||
ClientMessage::Ack { request_id }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("type failed: {e}");
|
||||
ClientMessage::Error {
|
||||
request_id,
|
||||
message: format!("Type failed: {e}"),
|
||||
}
|
||||
display::cmd_done("⌨", "type", &payload, false, &format!("{e}"));
|
||||
ClientMessage::Error { request_id, message: format!("Type failed: {e}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::ListWindowsRequest { request_id } => {
|
||||
log_cmd!("🪟", "list-windows");
|
||||
display::cmd_start("🪟", "list-windows", "");
|
||||
match windows_mgmt::list_windows() {
|
||||
Ok(windows) => {
|
||||
log_ok!("{} windows", windows.len());
|
||||
display::cmd_done("🪟", "list-windows", "", true, &format!("{} windows", windows.len()));
|
||||
ClientMessage::ListWindowsResponse { request_id, windows }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("list-windows failed: {e}");
|
||||
display::cmd_done("🪟", "list-windows", "", false, &e);
|
||||
ClientMessage::Error { request_id, message: e }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::MinimizeAllRequest { request_id } => {
|
||||
log_cmd!("🪟", "minimize-all");
|
||||
display::cmd_start("🪟", "minimize-all", "");
|
||||
match windows_mgmt::minimize_all() {
|
||||
Ok(()) => {
|
||||
log_ok!("Done");
|
||||
display::cmd_done("🪟", "minimize-all", "", true, "done");
|
||||
ClientMessage::Ack { request_id }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("minimize-all failed: {e}");
|
||||
display::cmd_done("🪟", "minimize-all", "", false, &e);
|
||||
ClientMessage::Error { request_id, message: e }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::FocusWindowRequest { request_id, window_id } => {
|
||||
log_cmd!("🪟", "focus-window {window_id}");
|
||||
let payload = format!("{window_id}");
|
||||
display::cmd_start("🪟", "focus-window", &payload);
|
||||
match windows_mgmt::focus_window(window_id) {
|
||||
Ok(()) => {
|
||||
log_ok!("Done");
|
||||
display::cmd_done("🪟", "focus-window", &payload, true, "done");
|
||||
ClientMessage::Ack { request_id }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("focus-window failed: {e}");
|
||||
display::cmd_done("🪟", "focus-window", &payload, false, &e);
|
||||
ClientMessage::Error { request_id, message: e }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::MaximizeAndFocusRequest { request_id, window_id } => {
|
||||
log_cmd!("🪟", "maximize-and-focus {window_id}");
|
||||
let payload = format!("{window_id}");
|
||||
display::cmd_start("🪟", "maximize", &payload);
|
||||
match windows_mgmt::maximize_and_focus(window_id) {
|
||||
Ok(()) => {
|
||||
log_ok!("Done");
|
||||
display::cmd_done("🪟", "maximize", &payload, true, "done");
|
||||
ClientMessage::Ack { request_id }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("maximize-and-focus failed: {e}");
|
||||
display::cmd_done("🪟", "maximize", &payload, false, &e);
|
||||
ClientMessage::Error { request_id, message: e }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::VersionRequest { request_id } => {
|
||||
log_cmd!("ℹ ", "version");
|
||||
ClientMessage::VersionResponse {
|
||||
request_id,
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
commit: env!("GIT_COMMIT").to_string(),
|
||||
}
|
||||
display::cmd_start("ℹ", "version", "");
|
||||
let version = env!("CARGO_PKG_VERSION").to_string();
|
||||
let commit = env!("GIT_COMMIT").to_string();
|
||||
display::cmd_done("ℹ", "version", "", true, &format!("v{version} ({commit})"));
|
||||
ClientMessage::VersionResponse { request_id, version, commit }
|
||||
}
|
||||
|
||||
ServerMessage::LogsRequest { request_id, lines } => {
|
||||
log_cmd!("📜", "logs (last {lines} lines)");
|
||||
let payload = format!("last {lines} lines");
|
||||
display::cmd_start("📜", "logs", &payload);
|
||||
let content = logger::tail(lines);
|
||||
let log_path = logger::get_log_path();
|
||||
display::cmd_done("📜", "logs", &payload, true, &log_path);
|
||||
ClientMessage::LogsResponse { request_id, content, log_path }
|
||||
}
|
||||
|
||||
ServerMessage::UploadRequest { request_id, path, content_base64 } => {
|
||||
log_cmd!("⬆ ", "upload → {}", path);
|
||||
let payload = trunc(&path, 60);
|
||||
display::cmd_start("⬆", "upload", &payload);
|
||||
match (|| -> Result<(), String> {
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(&content_base64)
|
||||
|
|
@ -586,74 +528,84 @@ async fn handle_message(
|
|||
Ok(())
|
||||
})() {
|
||||
Ok(()) => {
|
||||
log_ok!("Saved {}", path);
|
||||
display::cmd_done("⬆", "upload", &payload, true, "saved");
|
||||
ClientMessage::Ack { request_id }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("upload failed: {e}");
|
||||
display::cmd_done("⬆", "upload", &payload, false, &e);
|
||||
ClientMessage::Error { request_id, message: e }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::DownloadRequest { request_id, path } => {
|
||||
log_cmd!("⬇ ", "download ← {}", path);
|
||||
let payload = trunc(&path, 60);
|
||||
display::cmd_start("⬇", "download", &payload);
|
||||
match std::fs::read(&path) {
|
||||
Ok(bytes) => {
|
||||
let size = bytes.len() as u64;
|
||||
let content_base64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||
log_ok!("Sent {} bytes", size);
|
||||
display::cmd_done("⬇", "download", &payload, true, &format!("{size} bytes"));
|
||||
ClientMessage::DownloadResponse { request_id, content_base64, size }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("download failed: {e}");
|
||||
display::cmd_done("⬇", "download", &payload, false, &format!("read failed: {e}"));
|
||||
ClientMessage::Error { request_id, message: format!("Read failed: {e}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::RunRequest { request_id, program, args } => {
|
||||
log_cmd!("🚀", "run › {}", program);
|
||||
let payload = if args.is_empty() { program.clone() } else { format!("{program} {}", args.join(" ")) };
|
||||
let payload = trunc(&payload, 60);
|
||||
display::cmd_start("🚀", "run", &payload);
|
||||
use std::process::Command as StdCommand;
|
||||
match StdCommand::new(&program).args(&args).spawn() {
|
||||
Ok(_) => {
|
||||
log_ok!("Started {}", program);
|
||||
display::cmd_done("🚀", "run", &payload, true, "started");
|
||||
ClientMessage::Ack { request_id }
|
||||
}
|
||||
Err(e) => {
|
||||
log_err!("run failed: {e}");
|
||||
display::cmd_done("🚀", "run", &payload, false, &format!("{e}"));
|
||||
ClientMessage::Error { request_id, message: format!("Failed to start '{}': {e}", program) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::ClipboardGetRequest { request_id } => {
|
||||
log_cmd!("📋", "clipboard-get");
|
||||
display::cmd_start("📋", "clipboard-get", "");
|
||||
let out = tokio::process::Command::new("powershell.exe")
|
||||
.args(["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard"])
|
||||
.output().await;
|
||||
match out {
|
||||
Ok(o) => {
|
||||
let text = String::from_utf8_lossy(&o.stdout).trim().to_string();
|
||||
log_ok!("Got {} chars", text.len());
|
||||
display::cmd_done("📋", "clipboard-get", "", true, &format!("{} chars", text.len()));
|
||||
ClientMessage::ClipboardGetResponse { request_id, text }
|
||||
}
|
||||
Err(e) => ClientMessage::Error { request_id, message: format!("Clipboard get failed: {e}") }
|
||||
Err(e) => {
|
||||
display::cmd_done("📋", "clipboard-get", "", false, &format!("{e}"));
|
||||
ClientMessage::Error { request_id, message: format!("Clipboard get failed: {e}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServerMessage::ClipboardSetRequest { request_id, text } => {
|
||||
log_cmd!("📋", "clipboard-set › {} chars", text.len());
|
||||
let payload = format!("{} chars", text.len());
|
||||
display::cmd_start("📋", "clipboard-set", &payload);
|
||||
let cmd = format!("Set-Clipboard -Value '{}'", text.replace('\'', "''"));
|
||||
let out = tokio::process::Command::new("powershell.exe")
|
||||
.args(["-NoProfile", "-NonInteractive", "-Command", &cmd])
|
||||
.output().await;
|
||||
match out {
|
||||
Ok(_) => {
|
||||
log_ok!("Set clipboard");
|
||||
display::cmd_done("📋", "clipboard-set", &payload, true, "done");
|
||||
ClientMessage::Ack { request_id }
|
||||
}
|
||||
Err(e) => ClientMessage::Error { request_id, message: format!("Clipboard set failed: {e}") }
|
||||
Err(e) => {
|
||||
display::cmd_done("📋", "clipboard-set", &payload, false, &format!("{e}"));
|
||||
ClientMessage::Error { request_id, message: format!("Clipboard set failed: {e}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -662,7 +614,7 @@ async fn handle_message(
|
|||
}
|
||||
|
||||
ServerMessage::Error { request_id, message } => {
|
||||
log_err!("server error: {message}");
|
||||
display::err("❌", &format!("server error: {message}"));
|
||||
if let Some(rid) = request_id {
|
||||
ClientMessage::Ack { request_id: rid }
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue