mod stdio_reader;
use std::{
env,
fs::{File, OpenOptions},
io::{self, Read, Write},
path::PathBuf,
process::exit,
rc::Rc,
};
use clap::{Parser, Subcommand};
use futures::lock::Mutex;
use tokio::{runtime, task::LocalSet};
use tracing_subscriber::EnvFilter;
use qlue_ls::{Server, format_raw, handle_message};
use crate::stdio_reader::StdioMessages;
#[derive(Debug, Parser)]
#[command(version, about, long_about= None)]
struct Cli {
#[clap(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Server,
Format {
#[arg(short, long)]
writeback: bool,
#[arg(short, long)]
check: bool,
path: Option<PathBuf>,
},
Logs,
}
fn get_logfile_path() -> PathBuf {
let mut app_dir = dirs_next::data_dir().expect("Failed to find data directory");
app_dir.push("qlue-ls");
if !app_dir.exists() {
std::fs::create_dir_all(&app_dir).expect("Failed to create app directory");
}
app_dir.join("qlue-ls.log")
}
fn configure_logging() {
let level = env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string());
let logfile_path = get_logfile_path();
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&logfile_path)
.expect("Failed to open logfile");
tracing_subscriber::fmt()
.with_writer(std::sync::Mutex::new(file))
.with_env_filter(EnvFilter::new(level))
.with_ansi(false)
.init();
}
fn send_message(message: String) {
print!("Content-Length: {}\r\n\r\n{}", message.len(), message);
io::stdout().flush().expect("No IO errors or EOFs");
}
fn main() {
#[cfg(not(target_arch = "wasm32"))]
configure_logging();
let cli = Cli::parse();
match cli.command {
Command::Server => {
let rt = runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Single threaded runtime should be usable");
let local = LocalSet::new();
rt.block_on(local.run_until(async {
let server = Server::new(send_message);
let server_rc = Rc::new(Mutex::new(server));
for message in StdioMessages::new() {
handle_message(server_rc.clone(), message).await
}
}));
}
Command::Format {
path,
writeback,
check,
} => {
if let Some(path) = path {
match File::open(path.clone()) {
Ok(mut file) => {
let mut contents = String::new();
file.read_to_string(&mut contents)
.expect("Could not read file");
match format_raw(contents.clone()) {
Ok(formatted_contents) => {
let unchanged = formatted_contents == contents;
if check {
if unchanged {
println!("{} is already formatted", path.to_string_lossy());
exit(0);
} else {
println!("{} would be reformatted", path.to_string_lossy());
exit(1);
}
}
if writeback {
if unchanged {
println!("{} left unchanged", path.to_string_lossy());
} else {
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.append(false)
.open(path.clone())
.expect("Could not write to file");
file.write_all(formatted_contents.as_bytes())
.expect("Unable to write");
println!("{} reformatted", path.to_string_lossy());
}
} else {
println!("{}", formatted_contents);
}
}
Err(e) => panic!("Error during formatting:\n{}", e),
}
}
Err(e) => {
panic!("Could not open file: {}", e)
}
};
} else {
let mut buffer = Vec::new();
io::stdin()
.read_to_end(&mut buffer)
.expect("Should read all bytes from stdin");
let input = String::from_utf8(buffer).expect("input should be valid UTF8");
match format_raw(input) {
Ok(res) => {
print!("{}", res);
}
Err(e) => panic!("Error during formatting:\n{}", e),
}
}
}
Command::Logs => {
let logfile_path = get_logfile_path();
if !std::process::Command::new("tail")
.args(["--lines", "0"])
.arg("-f")
.arg(logfile_path)
.status()
.expect("Failed to start 'tail' process")
.success()
{
println!("tail exited with non-zero status");
}
}
};
}