use std::io::{IsTerminal, Write};
#[cfg(unix)]
use std::sync::OnceLock;
#[cfg(unix)]
struct ArgvSpan {
start: *mut libc::c_char,
capacity: usize,
}
#[cfg(unix)]
unsafe impl Send for ArgvSpan {}
#[cfg(unix)]
unsafe impl Sync for ArgvSpan {}
#[cfg(unix)]
static ARGV_SPAN: OnceLock<Option<ArgvSpan>> = OnceLock::new();
#[cfg(target_os = "macos")]
extern "C" {
fn _NSGetArgv() -> *mut *mut *mut libc::c_char;
fn _NSGetArgc() -> *mut libc::c_int;
}
#[cfg(target_os = "macos")]
unsafe fn capture_argv_span() -> Option<ArgvSpan> {
let argv_ptr = _NSGetArgv();
let argc_ptr = _NSGetArgc();
if argv_ptr.is_null() || argc_ptr.is_null() {
return None;
}
let argv = *argv_ptr;
let argc = *argc_ptr as isize;
if argv.is_null() || argc <= 0 {
return None;
}
let first = *argv;
if first.is_null() {
return None;
}
let last = *argv.offset(argc - 1);
if last.is_null() {
return None;
}
let last_len = libc::strlen(last);
let end = last.add(last_len + 1);
let capacity = end.offset_from(first) as usize;
Some(ArgvSpan {
start: first,
capacity,
})
}
#[cfg(target_os = "linux")]
unsafe fn capture_argv_span() -> Option<ArgvSpan> {
extern "C" {
static mut program_invocation_name: *mut libc::c_char;
}
let arg0 = program_invocation_name;
if arg0.is_null() {
return None;
}
let capacity = std::fs::read("/proc/self/cmdline")
.map(|v| v.len())
.unwrap_or_else(|_| libc::strlen(arg0));
Some(ArgvSpan {
start: arg0,
capacity,
})
}
#[cfg(all(unix, not(any(target_os = "macos", target_os = "linux"))))]
unsafe fn capture_argv_span() -> Option<ArgvSpan> {
None
}
#[cfg(unix)]
pub fn set_process_title(title: &str) {
#[cfg(target_os = "linux")]
unsafe {
const PR_SET_NAME: libc::c_int = 15;
let bytes = title.as_bytes();
let len = bytes.len().min(15);
let mut buf = [0u8; 16];
buf[..len].copy_from_slice(&bytes[..len]);
libc::prctl(PR_SET_NAME, buf.as_ptr() as libc::c_ulong, 0u64, 0u64, 0u64);
}
let span = ARGV_SPAN.get_or_init(|| unsafe { capture_argv_span() });
if let Some(span) = span {
unsafe {
let title_bytes = title.as_bytes();
let copy_len = title_bytes.len().min(span.capacity.saturating_sub(1));
std::ptr::copy_nonoverlapping(title_bytes.as_ptr(), span.start as *mut u8, copy_len);
std::ptr::write_bytes(span.start.add(copy_len), 0u8, span.capacity - copy_len);
}
}
}
#[cfg(not(unix))]
pub fn set_process_title(_title: &str) {
}
pub fn set_terminal_title(title: &str) {
let mut stderr = std::io::stderr();
if !stderr.is_terminal() {
return;
}
let _ = write!(stderr, "\x1b]0;{title}\x07");
let _ = stderr.flush();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_process_title_does_not_panic() {
set_process_title("test-title");
set_process_title("");
set_process_title("a-very-long-title-that-exceeds-argv-capacity-to-test-padding-behavior");
}
#[test]
fn set_terminal_title_does_not_panic() {
set_terminal_title("test-title");
set_terminal_title("");
set_terminal_title("title with \x1b special chars");
}
#[test]
fn set_terminal_title_noop_when_not_tty() {
set_terminal_title("should-be-noop");
}
}