use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use windows_sys::Win32::Foundation::{ERROR_FILE_NOT_FOUND, ERROR_SUCCESS};
use windows_sys::Win32::System::Registry::{
RegDeleteKeyValueW, RegGetValueW, RegSetKeyValueW, HKEY_CURRENT_USER, REG_SZ, RRF_NOEXPAND,
RRF_RT_REG_EXPAND_SZ, RRF_RT_REG_SZ,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{
SendMessageTimeoutW, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE,
};
pub(super) const MANUAL_CLEAR: &str =
"`reg delete HKCU\\Environment /v CLINE_WRAPPER_PATH /f` and \
`reg delete HKCU\\Environment /v CLINE_JS_RUNTIME_PATH /f`";
const SUBKEY: &str = "Environment";
const MAX_VALUE_BYTES: u32 = 64 * 1024;
const BROADCAST_MS: u32 = 5000;
fn wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn broadcast() -> Result<(), String> {
let param = wide(SUBKEY);
let mut result: usize = 0;
let rc = unsafe {
SendMessageTimeoutW(
HWND_BROADCAST,
WM_SETTINGCHANGE,
0,
param.as_ptr() as isize,
SMTO_ABORTIFHUNG,
BROADCAST_MS,
&mut result,
)
};
if rc == 0 {
Err("the WM_SETTINGCHANGE broadcast did not complete".to_string())
} else {
Ok(())
}
}
pub(super) fn read(name: &str) -> Result<Option<String>, String> {
let subkey = wide(SUBKEY);
let value = wide(name);
let flags = RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ | RRF_NOEXPAND;
let mut bytes: u32 = 0;
let rc = unsafe {
RegGetValueW(
HKEY_CURRENT_USER,
subkey.as_ptr(),
value.as_ptr(),
flags,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut bytes,
)
};
if rc == ERROR_FILE_NOT_FOUND {
return Ok(None);
}
if rc != ERROR_SUCCESS {
return Err(format!("RegGetValueW({name}) failed: {rc}"));
}
if bytes > MAX_VALUE_BYTES {
return Err(format!("{name} is larger than {MAX_VALUE_BYTES} bytes"));
}
let mut buf = vec![0u16; bytes as usize / 2 + 1];
let mut written = bytes;
let rc = unsafe {
RegGetValueW(
HKEY_CURRENT_USER,
subkey.as_ptr(),
value.as_ptr(),
flags,
std::ptr::null_mut(),
buf.as_mut_ptr().cast(),
&mut written,
)
};
if rc == ERROR_FILE_NOT_FOUND {
return Ok(None);
}
if rc != ERROR_SUCCESS {
return Err(format!("RegGetValueW({name}) failed: {rc}"));
}
let len = (written as usize / 2).saturating_sub(1).min(buf.len());
let text = OsString::from_wide(&buf[..len])
.to_string_lossy()
.into_owned();
Ok((!text.is_empty()).then_some(text))
}
pub(super) fn apply(name: &str, value: &str) -> Result<(), String> {
let subkey = wide(SUBKEY);
let key = wide(name);
let data = wide(value);
let len = u32::try_from(data.len() * 2).map_err(|_| format!("{name}: value too long"))?;
let rc = unsafe {
RegSetKeyValueW(
HKEY_CURRENT_USER,
subkey.as_ptr(),
key.as_ptr(),
REG_SZ,
data.as_ptr().cast(),
len,
)
};
if rc != ERROR_SUCCESS {
return Err(format!("RegSetKeyValueW({name}) failed: {rc}"));
}
if let Err(e) = broadcast() {
tracing::debug!(error = %e, "Cline login environment: broadcast after a write");
}
Ok(())
}
pub(super) fn clear(name: &str) -> Result<(), String> {
let subkey = wide(SUBKEY);
let key = wide(name);
let rc = unsafe { RegDeleteKeyValueW(HKEY_CURRENT_USER, subkey.as_ptr(), key.as_ptr()) };
if rc != ERROR_SUCCESS && rc != ERROR_FILE_NOT_FOUND {
return Err(format!("RegDeleteKeyValueW({name}) failed: {rc}"));
}
if let Err(e) = broadcast() {
tracing::debug!(error = %e, "Cline login environment: broadcast after a removal");
}
Ok(())
}