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, socket_is_listening,
};
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 a new or existing Helix session",
after_help = "Examples:\n hxr README.md\n hxr --listen\n hxr --status\n hxr --open src/main.rs Cargo.toml\n hxr --open src/main.rs:50:12\n hxr --stop\n git diff | hxr --stdin-name changes.diff --open -"
)]
struct Cli {
#[arg(long, conflicts_with_all = ["open", "status", "stop", "lsp", "print_config"])]
listen: bool,
#[arg(long, conflicts_with_all = ["listen", "status", "stop", "lsp", "print_config"])]
open: bool,
#[arg(long, conflicts_with_all = ["listen", "open", "stop", "lsp", "print_config"])]
status: bool,
#[arg(long, conflicts_with_all = ["listen", "open", "status", "lsp", "print_config"])]
stop: bool,
#[arg(long, requires = "stop")]
force: bool,
#[arg(long, conflicts_with_all = ["listen", "open", "status", "stop", "lsp"])]
print_config: bool,
#[arg(long, hide = true, conflicts_with_all = ["listen", "open", "status", "stop", "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")]
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.status,
cli.stop,
cli.print_config,
cli.lsp,
]
.into_iter()
.filter(|enabled| *enabled)
.count();
if action_count > 1 || (action_count == 0 && cli.paths.is_empty()) {
return Err(
"choose exactly one of --listen, --open, --status, --stop, 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 action_count == 0 {
if cli
.paths
.iter()
.any(|path| path.as_os_str() == OsStr::new("-"))
{
return Err("stdin (-) requires --open and a listening Helix instance".into());
}
return launch_helix(cli, socket_path);
}
if cli.status {
if !cli.paths.is_empty() {
return Err("--status does not accept paths".into());
}
return report_status(&socket_path);
}
if cli.stop {
if !cli.paths.is_empty() {
return Err("--stop does not accept paths".into());
}
let request = if cli.force {
SocketRequest::ForceStop
} else {
SocketRequest::Stop
};
let message =
send_socket_request(&socket_path, &request).map_err(|error| error.to_string())?;
println!("{message}");
return Ok(());
}
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 report_status(socket_path: &Path) -> Result<(), String> {
if socket_is_listening(socket_path).map_err(|error| error.to_string())? {
println!("listening on {}", socket_path.display());
Ok(())
} else {
Err(format!(
"no socket is listening on {}",
socket_path.display()
))
}
}
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 uses_split_layout = cli
.helix_arg
.iter()
.any(|argument| argument == OsStr::new("--vsplit") || argument == OsStr::new("--hsplit"));
let mut command = Command::new(&cli.helix);
command.args(&cli.helix_arg);
if cli.paths.is_empty() {
command.arg(&sentinel);
} else {
command.arg("--");
if uses_split_layout {
command.arg(&sentinel).args(&cli.paths);
} else {
command.args(&cli.paths).arg(&sentinel);
}
}
let error = command.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(())
}