feat(client): modern CLI output + Windows exe icon
- Replace timestamp-based log macros with colored CLI output system
using the `colored` crate
- Banner on startup: ☀ HELIOS REMOTE + separator line
- Colored prefixes: cyan → (status), green ✓ (ok), red ✗ (error),
yellow ⚡ (incoming commands)
- Session info on connect: ✓ Connected · <label> · Session <id>
- No timestamps, no [CMD]/[OK]/[ERR] prefixes
- Suppress tracing output by default (RUST_LOG=off unless set)
- Add build.rs to embed logo.ico as Windows resource via winres
- Add winres as build-dependency in client Cargo.toml
This commit is contained in:
parent
1d019fa2b4
commit
e32e09996b
3 changed files with 131 additions and 50 deletions
|
|
@ -22,6 +22,10 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
png = "0.17"
|
png = "0.17"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
colored = "2"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
winres = "0.1"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.54", features = [
|
windows = { version = "0.54", features = [
|
||||||
|
|
|
||||||
8
crates/client/build.rs
Normal file
8
crates/client/build.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fn main() {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let mut res = winres::WindowsResource::new();
|
||||||
|
res.set_icon("../../assets/logo.ico");
|
||||||
|
res.compile().expect("Failed to compile Windows resources");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,12 @@ use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::Local;
|
use colored::Colorize;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use native_tls::TlsConnector;
|
use native_tls::TlsConnector;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::Message, Connector};
|
use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::Message, Connector};
|
||||||
use tracing::{error, warn};
|
|
||||||
|
|
||||||
use helios_common::{ClientMessage, ServerMessage};
|
use helios_common::{ClientMessage, ServerMessage};
|
||||||
|
|
||||||
|
|
@ -17,23 +16,49 @@ mod screenshot;
|
||||||
mod input;
|
mod input;
|
||||||
mod windows_mgmt;
|
mod windows_mgmt;
|
||||||
|
|
||||||
fn ts() -> String {
|
// ─── CLI Output ─────────────────────────────────────────────────────────────
|
||||||
Local::now().format("%H:%M:%S").to_string()
|
|
||||||
|
fn banner() {
|
||||||
|
println!();
|
||||||
|
println!(" {} HELIOS REMOTE", "☀".yellow().bold());
|
||||||
|
println!(" {}", "─".repeat(45).dimmed());
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! log_info {
|
macro_rules! log_status {
|
||||||
($($arg:tt)*) => { println!("[{}] {}", ts(), format!($($arg)*)) };
|
($($arg:tt)*) => {
|
||||||
}
|
println!(" {} {}", "→".cyan().bold(), format!($($arg)*));
|
||||||
macro_rules! log_cmd {
|
};
|
||||||
($($arg:tt)*) => { println!("[{}] [CMD] {}", ts(), format!($($arg)*)) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! log_ok {
|
macro_rules! log_ok {
|
||||||
($($arg:tt)*) => { println!("[{}] [OK] {}", ts(), format!($($arg)*)) };
|
($($arg:tt)*) => {
|
||||||
|
println!(" {} {}", "✓".green().bold(), format!($($arg)*));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! log_err {
|
macro_rules! log_err {
|
||||||
($($arg:tt)*) => { println!("[{}] [ERR] {}", ts(), format!($($arg)*)) };
|
($($arg:tt)*) => {
|
||||||
|
println!(" {} {}", "✗".red().bold(), format!($($arg)*));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! log_cmd {
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
println!(" {} {}", "⚡".yellow().bold(), format!($($arg)*));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_id() -> String {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
let t = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.subsec_nanos();
|
||||||
|
format!("{:06x}", t & 0xFFFFFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct Config {
|
struct Config {
|
||||||
relay_url: String,
|
relay_url: String,
|
||||||
|
|
@ -97,21 +122,24 @@ fn prompt_config() -> Config {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
println!("=== Helios Remote Client ===");
|
// Suppress tracing output by default
|
||||||
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
|
unsafe { std::env::set_var("RUST_LOG", "off"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
banner();
|
||||||
|
|
||||||
// Load or prompt for config
|
// Load or prompt for config
|
||||||
let config = match Config::load() {
|
let config = match Config::load() {
|
||||||
Some(c) => {
|
Some(c) => c,
|
||||||
log_info!("Config loaded from {:?}", Config::config_path());
|
|
||||||
c
|
|
||||||
}
|
|
||||||
None => {
|
None => {
|
||||||
log_info!("No config found — first-time setup");
|
log_status!("No config found — first-time setup");
|
||||||
|
println!();
|
||||||
let c = prompt_config();
|
let c = prompt_config();
|
||||||
if let Err(e) = c.save() {
|
if let Err(e) = c.save() {
|
||||||
log_err!("Failed to save config: {e}");
|
log_err!("Failed to save config: {e}");
|
||||||
} else {
|
} else {
|
||||||
log_info!("Config saved to {:?}", Config::config_path());
|
log_ok!("Config saved");
|
||||||
}
|
}
|
||||||
c
|
c
|
||||||
}
|
}
|
||||||
|
|
@ -125,17 +153,34 @@ async fn main() {
|
||||||
const MAX_BACKOFF: Duration = Duration::from_secs(30);
|
const MAX_BACKOFF: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
log_info!("Connecting to {}...", config.relay_url);
|
let host = config.relay_url
|
||||||
// Build TLS connector - accepts self-signed certs for internal CA (Caddy tls internal)
|
.trim_start_matches("wss://")
|
||||||
|
.trim_start_matches("ws://")
|
||||||
|
.split('/')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(&config.relay_url);
|
||||||
|
|
||||||
|
log_status!("Connecting to {}...", host);
|
||||||
|
|
||||||
let tls_connector = TlsConnector::builder()
|
let tls_connector = TlsConnector::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.build()
|
.build()
|
||||||
.expect("TLS connector build failed");
|
.expect("TLS connector build failed");
|
||||||
let connector = Connector::NativeTls(tls_connector);
|
let connector = Connector::NativeTls(tls_connector);
|
||||||
|
|
||||||
match connect_async_tls_with_config(&config.relay_url, None, false, Some(connector)).await {
|
match connect_async_tls_with_config(&config.relay_url, None, false, Some(connector)).await {
|
||||||
Ok((ws_stream, _)) => {
|
Ok((ws_stream, _)) => {
|
||||||
log_info!("Connected!");
|
let sid = session_id();
|
||||||
backoff = Duration::from_secs(1); // reset on success
|
let label = config.label.clone().unwrap_or_else(|| hostname());
|
||||||
|
log_ok!(
|
||||||
|
"Connected {} {} {} Session {}",
|
||||||
|
"·".dimmed(),
|
||||||
|
label.bold(),
|
||||||
|
"·".dimmed(),
|
||||||
|
sid.dimmed()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
backoff = Duration::from_secs(1);
|
||||||
|
|
||||||
let (mut write, mut read) = ws_stream.split();
|
let (mut write, mut read) = ws_stream.split();
|
||||||
|
|
||||||
|
|
@ -151,17 +196,15 @@ async fn main() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared write half
|
|
||||||
let write = Arc::new(Mutex::new(write));
|
let write = Arc::new(Mutex::new(write));
|
||||||
|
|
||||||
// Process messages
|
|
||||||
while let Some(msg_result) = read.next().await {
|
while let Some(msg_result) = read.next().await {
|
||||||
match msg_result {
|
match msg_result {
|
||||||
Ok(Message::Text(text)) => {
|
Ok(Message::Text(text)) => {
|
||||||
let server_msg: ServerMessage = match serde_json::from_str(&text) {
|
let server_msg: ServerMessage = match serde_json::from_str(&text) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_err!("Failed to parse server message: {e} | raw: {text}");
|
log_err!("Failed to parse server message: {e}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -183,18 +226,16 @@ async fn main() {
|
||||||
let _ = w.send(Message::Pong(data)).await;
|
let _ = w.send(Message::Pong(data)).await;
|
||||||
}
|
}
|
||||||
Ok(Message::Close(_)) => {
|
Ok(Message::Close(_)) => {
|
||||||
log_info!("Server closed connection");
|
log_err!("Connection lost — reconnecting...");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_err!("WebSocket error: {e}");
|
log_err!("Connection lost: {e} — reconnecting...");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warn!("Disconnected. Reconnecting in {:?}...", backoff);
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_err!("Connection failed: {e}");
|
log_err!("Connection failed: {e}");
|
||||||
|
|
@ -206,6 +247,29 @@ async fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hostname() -> String {
|
||||||
|
std::fs::read_to_string("/etc/hostname")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
.or_else(|| std::env::var("COMPUTERNAME").ok())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
trait OrElseString {
|
||||||
|
fn or_else(self, f: impl FnOnce() -> Option<String>) -> String;
|
||||||
|
fn unwrap_or_else(self, f: impl FnOnce() -> String) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OrElseString for String {
|
||||||
|
fn or_else(self, f: impl FnOnce() -> Option<String>) -> String {
|
||||||
|
if self.is_empty() { f().unwrap_or_default() } else { self }
|
||||||
|
}
|
||||||
|
fn unwrap_or_else(self, f: impl FnOnce() -> String) -> String {
|
||||||
|
if self.is_empty() { f() } else { self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_message(
|
async fn handle_message(
|
||||||
msg: ServerMessage,
|
msg: ServerMessage,
|
||||||
shell: Arc<Mutex<shell::PersistentShell>>,
|
shell: Arc<Mutex<shell::PersistentShell>>,
|
||||||
|
|
@ -215,7 +279,7 @@ async fn handle_message(
|
||||||
log_cmd!("screenshot");
|
log_cmd!("screenshot");
|
||||||
match screenshot::take_screenshot() {
|
match screenshot::take_screenshot() {
|
||||||
Ok((image_base64, width, height)) => {
|
Ok((image_base64, width, height)) => {
|
||||||
log_ok!("screenshot taken ({}x{})", width, height);
|
log_ok!("Done {} {}×{}", "·".dimmed(), width, height);
|
||||||
ClientMessage::ScreenshotResponse {
|
ClientMessage::ScreenshotResponse {
|
||||||
request_id,
|
request_id,
|
||||||
image_base64,
|
image_base64,
|
||||||
|
|
@ -234,11 +298,17 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::ExecRequest { request_id, command } => {
|
ServerMessage::ExecRequest { request_id, command } => {
|
||||||
log_cmd!("exec: {command}");
|
log_cmd!("exec › {}", command);
|
||||||
let mut sh = shell.lock().await;
|
let mut sh = shell.lock().await;
|
||||||
match sh.run(&command).await {
|
match sh.run(&command).await {
|
||||||
Ok((stdout, stderr, exit_code)) => {
|
Ok((stdout, stderr, exit_code)) => {
|
||||||
log_ok!("exec completed (exit {})", exit_code);
|
let out = stdout.trim().lines().next().unwrap_or("").to_string();
|
||||||
|
if out.is_empty() {
|
||||||
|
log_ok!("Done {} exit {}", "·".dimmed(), exit_code);
|
||||||
|
} else {
|
||||||
|
log_ok!("{} {} exit {}", out, "·".dimmed(), exit_code);
|
||||||
|
}
|
||||||
|
let _ = stderr;
|
||||||
ClientMessage::ExecResponse {
|
ClientMessage::ExecResponse {
|
||||||
request_id,
|
request_id,
|
||||||
stdout,
|
stdout,
|
||||||
|
|
@ -247,11 +317,11 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_err!("exec failed for {:?}: {e}", command);
|
log_err!("exec failed: {e}");
|
||||||
ClientMessage::Error {
|
ClientMessage::Error {
|
||||||
request_id,
|
request_id,
|
||||||
message: format!(
|
message: format!(
|
||||||
"Exec failed for command {:?}.\nError: {e}\nContext: persistent shell may have died.",
|
"Exec failed for command {:?}.\nError: {e}",
|
||||||
command
|
command
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -260,14 +330,14 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::ClickRequest { request_id, x, y, button } => {
|
ServerMessage::ClickRequest { request_id, x, y, button } => {
|
||||||
log_cmd!("click {} {} {:?}", x, y, button);
|
log_cmd!("click ({x}, {y}) {:?}", button);
|
||||||
match input::click(x, y, &button) {
|
match input::click(x, y, &button) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log_ok!("click done");
|
log_ok!("Done");
|
||||||
ClientMessage::Ack { request_id }
|
ClientMessage::Ack { request_id }
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_err!("click failed at ({x},{y}): {e}");
|
log_err!("click failed: {e}");
|
||||||
ClientMessage::Error {
|
ClientMessage::Error {
|
||||||
request_id,
|
request_id,
|
||||||
message: format!("Click at ({x},{y}) failed: {e}"),
|
message: format!("Click at ({x},{y}) failed: {e}"),
|
||||||
|
|
@ -277,10 +347,10 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::TypeRequest { request_id, text } => {
|
ServerMessage::TypeRequest { request_id, text } => {
|
||||||
log_cmd!("type: {} chars", text.len());
|
log_cmd!("type {} chars", text.len());
|
||||||
match input::type_text(&text) {
|
match input::type_text(&text) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log_ok!("type done");
|
log_ok!("Done");
|
||||||
ClientMessage::Ack { request_id }
|
ClientMessage::Ack { request_id }
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -297,7 +367,7 @@ async fn handle_message(
|
||||||
log_cmd!("list-windows");
|
log_cmd!("list-windows");
|
||||||
match windows_mgmt::list_windows() {
|
match windows_mgmt::list_windows() {
|
||||||
Ok(windows) => {
|
Ok(windows) => {
|
||||||
log_ok!("list-windows: {} windows", windows.len());
|
log_ok!("{} windows", windows.len());
|
||||||
ClientMessage::ListWindowsResponse { request_id, windows }
|
ClientMessage::ListWindowsResponse { request_id, windows }
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -311,7 +381,7 @@ async fn handle_message(
|
||||||
log_cmd!("minimize-all");
|
log_cmd!("minimize-all");
|
||||||
match windows_mgmt::minimize_all() {
|
match windows_mgmt::minimize_all() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log_ok!("minimize-all done");
|
log_ok!("Done");
|
||||||
ClientMessage::Ack { request_id }
|
ClientMessage::Ack { request_id }
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -322,10 +392,10 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::FocusWindowRequest { request_id, window_id } => {
|
ServerMessage::FocusWindowRequest { request_id, window_id } => {
|
||||||
log_cmd!("focus-window: {window_id}");
|
log_cmd!("focus-window {window_id}");
|
||||||
match windows_mgmt::focus_window(window_id) {
|
match windows_mgmt::focus_window(window_id) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log_ok!("focus-window done");
|
log_ok!("Done");
|
||||||
ClientMessage::Ack { request_id }
|
ClientMessage::Ack { request_id }
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -336,10 +406,10 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::MaximizeAndFocusRequest { request_id, window_id } => {
|
ServerMessage::MaximizeAndFocusRequest { request_id, window_id } => {
|
||||||
log_cmd!("maximize-and-focus: {window_id}");
|
log_cmd!("maximize-and-focus {window_id}");
|
||||||
match windows_mgmt::maximize_and_focus(window_id) {
|
match windows_mgmt::maximize_and_focus(window_id) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log_ok!("maximize-and-focus done");
|
log_ok!("Done");
|
||||||
ClientMessage::Ack { request_id }
|
ClientMessage::Ack { request_id }
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -350,12 +420,11 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::Ack { request_id } => {
|
ServerMessage::Ack { request_id } => {
|
||||||
// Nothing to do - server acked something we sent
|
|
||||||
ClientMessage::Ack { request_id }
|
ClientMessage::Ack { request_id }
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::Error { request_id, message } => {
|
ServerMessage::Error { request_id, message } => {
|
||||||
log_err!("server error (req={request_id:?}): {message}");
|
log_err!("server error: {message}");
|
||||||
if let Some(rid) = request_id {
|
if let Some(rid) = request_id {
|
||||||
ClientMessage::Ack { request_id: rid }
|
ClientMessage::Ack { request_id: rid }
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue