use clap::{ArgAction, Parser};
use hx_remote::{
SOCKET_ENV, SocketRequest, absolute_path, default_sentinel_path, parse_target,
resolve_socket_path, run_server, send_socket_request,
};
use std::ffi::{OsStr, OsString};
use std::fs::{self, OpenOptions};
use std::io::{self, Read};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
const HELIX_CONFIG: &str = r#"[language-server.hx-remote]
command = "hxr"
args = ["--lsp"]
[[language]]
name = "hx-remote"
scope = "source.hx-remote"
file-types = ["hxremote"]
language-servers = ["hx-remote"]
"#;
#[derive(Debug, Parser)]
#[command(
name = "hxr",
version,
about = "Open files in an existing Helix session",
after_help = "Examples:\n hxr --listen\n hxr --open src/main.rs Cargo.toml\n hxr --open src/main.rs:50:12\n git diff | hxr --stdin-name changes.diff --open -"
)]
struct Cli {
#[arg(long, conflicts_with_all = ["open", "lsp", "print_config"])]
listen: bool,
#[arg(long, conflicts_with_all = ["listen", "lsp", "print_config"])]
open: bool,
#[arg(long, conflicts_with_all = ["listen", "open", "lsp"])]
print_config: bool,
#[arg(long, hide = true, conflicts_with_all = ["listen", "open", "print_config"])]
lsp: bool,
#[arg(long, env = SOCKET_ENV, value_name = "PATH")]
socket: Option<PathBuf>,
#[arg(long, default_value = "hx", value_name = "COMMAND")]
helix: OsString,
#[arg(long, value_name = "PATH")]
sentinel: Option<PathBuf>,
#[arg(long, action = ArgAction::Append, allow_hyphen_values = true, value_name = "ARG")]
helix_arg: Vec<OsString>,
#[arg(long, default_value = "stdin.txt", value_name = "NAME")]
stdin_name: String,
#[arg(value_name = "PATH", allow_hyphen_values = true)]
paths: Vec<OsString>,
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("hxr: {message}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), String> {
let cli = Cli::parse();
let action_count = [cli.listen, cli.open, cli.print_config, cli.lsp]
.into_iter()
.filter(|enabled| *enabled)
.count();
if action_count != 1 {
return Err("choose exactly one of --listen, --open, or --print-config".into());
}
if cli.print_config {
if !cli.paths.is_empty() {
return Err("--print-config does not accept paths".into());
}
print!("{HELIX_CONFIG}");
return Ok(());
}
let socket_path = resolve_socket_path(cli.socket.clone());
if cli.lsp {
if !cli.paths.is_empty() {
return Err("--lsp does not accept paths".into());
}
return run_server(socket_path).map_err(|error| error.to_string());
}
if cli.listen {
if !cli.paths.is_empty() {
return Err("--listen does not accept paths; use --helix-arg for Helix options".into());
}
return launch_helix(cli, socket_path);
}
if cli.paths.is_empty() {
return Err("--open requires at least one path (or - for stdin)".into());
}
open_paths(cli.paths, &cli.stdin_name, &socket_path)
}
fn launch_helix(cli: Cli, socket_path: PathBuf) -> Result<(), String> {
let sentinel = cli.sentinel.unwrap_or_else(default_sentinel_path);
if let Some(parent) = sentinel.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"cannot create sentinel directory {}: {error}",
parent.display()
)
})?;
}
OpenOptions::new()
.create(true)
.append(true)
.open(&sentinel)
.map_err(|error| format!("cannot create sentinel {}: {error}", sentinel.display()))?;
let error = Command::new(&cli.helix)
.args(&cli.helix_arg)
.arg(&sentinel)
.env(SOCKET_ENV, &socket_path)
.exec();
Err(format!(
"cannot launch {}: {error}",
cli.helix.to_string_lossy()
))
}
fn open_paths(paths: Vec<OsString>, stdin_name: &str, socket_path: &Path) -> Result<(), String> {
let stdin_count = paths
.iter()
.filter(|path| path.as_os_str() == OsStr::new("-"))
.count();
if stdin_count > 1 {
return Err("stdin (-) can only be opened once per invocation".into());
}
for input in paths {
let request = if input.as_os_str() == OsStr::new("-") {
let mut contents = String::new();
io::stdin()
.read_to_string(&mut contents)
.map_err(|error| format!("cannot read stdin as UTF-8 text: {error}"))?;
SocketRequest::OpenStdin {
contents,
name: stdin_name.to_owned(),
}
} else {
let target = parse_target(&input)?;
let path = absolute_path(&target.path)
.map_err(|error| format!("cannot resolve {}: {error}", target.path.display()))?;
SocketRequest::Open {
path,
line: target.line,
column: target.column,
}
};
let message =
send_socket_request(socket_path, &request).map_err(|error| error.to_string())?;
println!("{message}");
}
Ok(())
}