added support for local editing with different editor

This commit is contained in:
Parker TenBroeck 2026-03-11 10:06:04 -04:00
parent 3cce2983a5
commit 0289d1171f
11 changed files with 2252 additions and 1096 deletions

View file

@ -1,10 +1,10 @@
use std::{
collections::HashMap,
ops::Deref,
path::{Path, PathBuf},
collections::HashMap, ffi::OsStr, ops::Deref, path::{Path, PathBuf}
};
use tokio::process::{Child, Command};
use crate::HResult;
async fn ensure_ok(child: Child) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let result = child.wait_with_output().await?;
if !result.status.success() {
@ -43,9 +43,9 @@ impl AsRef<Path> for TempDir {
}
}
pub async fn build(
pub async fn copy_and_build(
files: HashMap<String, String>,
) -> Result<TempDir, Box<dyn std::error::Error + Send + Sync>> {
) -> HResult<TempDir> {
use std::hash::*;
let mut hasher = std::hash::DefaultHasher::default();
for (key, value) in &files {
@ -56,7 +56,7 @@ pub async fn build(
let mut work_dir = std::env::temp_dir();
work_dir.push(format!("ghdl-relay-{hash:x?}"));
_ = std::fs::create_dir(&work_dir);
std::fs::create_dir_all(&work_dir)?;
let work_dir = TempDir(work_dir);
for (name, contents) in &files {
@ -65,36 +65,51 @@ pub async fn build(
std::fs::write(path, contents)?;
}
let mut cmd = Command::new("ghdl");
build(&work_dir, &work_dir).await?;
Ok(work_dir)
}
pub async fn build(path: &Path, src: &Path) -> HResult<()>{
std::fs::create_dir_all(path)?;
let mut cmd = Command::new("ghdl");
cmd.kill_on_drop(true);
cmd.args(["-a", "-g", "--std=08"]);
for name in files.keys() {
let mut path = work_dir.clone();
path.push(name);
cmd.arg(path);
cmd.args(["-i", "-g", "--std=08"]);
for file in src.read_dir().unwrap().flatten(){
if Path::new(&file.file_name()).extension() == Some(OsStr::new("vhdl")) {
cmd.arg(file.path());
}
}
cmd.arg(std::fs::canonicalize("../rtl/tb.vhdl")?);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
cmd.current_dir(&work_dir);
cmd.current_dir(path);
ensure_ok(cmd.spawn()?).await?;
let mut cmd = Command::new("ghdl");
cmd.kill_on_drop(true);
cmd.args(["-e", "--std=08"]);
cmd.args(["-m", "--std=08"]);
cmd.arg(format!(
"-Wl,{}",
std::fs::canonicalize("../conn/target/release/libvhdl_ui.a")?.display()
std::fs::canonicalize("../target/release/libvhdl_ui.a")?.display()
));
cmd.arg("tb");
cmd.current_dir(&work_dir);
cmd.current_dir(path);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
ensure_ok(cmd.spawn()?).await?;
Ok(work_dir)
}
Ok(())
}

View file

@ -5,43 +5,15 @@ use futures_util::{
SinkExt, StreamExt,
stream::{SplitSink, SplitStream},
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf, time::Duration};
use std::{path::PathBuf, time::Duration};
use tokio::{
io::{AsyncBufReadExt, BufReader, Lines},
process::{Child, ChildStderr, ChildStdin, ChildStdout},
process::{Child, ChildStderr, ChildStdin, ChildStdout}, time::Instant,
};
use crate::{build, run};
use crate::{ClientMsg, ServerMsg, build, run};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum ClientMsg {
Compile(Option<HashMap<String, String>>),
Start,
Stop,
Input {
/// bitfield of 32 switches
switch: u32,
/// bitfield of 32 buttons
buttons: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ServerMsg<'a> {
Log { stream: &'a str, line: &'a str },
Start,
Stop,
Led(u32),
Seg0(u32),
Seg1(u32),
Seg2(u32),
Seg3(u32),
}
struct Process {
process: Child,
@ -50,10 +22,6 @@ struct Process {
stdin: ChildStdin,
}
enum Mode {
SingleLocal,
Remote,
}
struct Handler {
sender: SplitSink<WebSocket, Message>,
@ -68,11 +36,9 @@ struct Handler {
refresh_time: Duration,
}
type HResult<T> = Result<T, Box<dyn std::error::Error + Sync + Send>>;
impl Handler {
async fn local(socket: WebSocket, build: PathBuf, src: PathBuf) -> Self {
fn local(socket: WebSocket, build: PathBuf, src: PathBuf) -> Self {
let (sender, receiver) = socket.split();
Self {
sender,
@ -108,20 +74,18 @@ impl Handler {
_ = self.sender.send(Message::Text(serde_json::to_string(&ServerMsg::Stop).unwrap_or_default().into())).await;
}
async fn handle_websocket_msg(&mut self, msg: ClientMsg) -> HResult<bool> {
async fn handle_websocket_msg(&mut self, msg: ClientMsg) {
match msg{
ClientMsg::Compile(_) => todo!(),
ClientMsg::Start => self.run_program().await,
ClientMsg::Stop => self.stop_process().await,
ClientMsg::Input { switch, buttons } => {
if let Some(process) = &mut self.process{
use tokio::io::AsyncWriteExt;
process.stdin.write_all(format!("btn={}\n", buttons).as_bytes()).await?;
process.stdin.write_all(format!("sw={}\n", switch).as_bytes()).await?;
_ = process.stdin.write_all(format!("btn={}\n", buttons).as_bytes()).await;
_ = process.stdin.write_all(format!("sw={}\n", switch).as_bytes()).await;
}
},
}
Ok(false)
}
async fn handle_websocket_receive(
@ -150,13 +114,16 @@ impl Handler {
}
}
async fn run(&mut self) -> Result<(), Box<dyn std::error::Error + Sync + Send>> {
async fn run(&mut self) {
loop {
if let Some(process) = &mut self.process {
if let Ok(Some(_)) = process.process.try_wait(){
self.stop_process().await;
continue;
}
let mut print_deadline = Instant::now();
tokio::select! {
receive = self.receiver.next() => {
if self.handle_websocket_receive(receive).await{
@ -190,7 +157,7 @@ impl Handler {
self.eprint(line).await;
continue;
};
self.sender.send(Message::Text(serde_json::to_string(&msg)?.into())).await?;
_ = self.sender.send(Message::Text(serde_json::to_string(&msg).unwrap_or_default().into())).await;
},
Ok(None) => self.stop_process().await,
Err(err) => {
@ -199,8 +166,9 @@ impl Handler {
}
}
}
_ = tokio::time::sleep(self.refresh_time) => {
_ = tokio::time::sleep_until(print_deadline) => {
use tokio::io::AsyncWriteExt;
print_deadline += self.refresh_time;
_ = process.stdin.write_all("\n".as_bytes()).await;
}
}
@ -211,20 +179,12 @@ impl Handler {
}
}
}
Ok(())
}
async fn build_program(&mut self) {
let files = if let Some(Ok(Message::Text(msg))) = self.receiver.next().await
&& let Ok(files) = serde_json::from_str::<'_, HashMap<String, String>>(&msg)
{
files
} else {
return;
};
async fn run_program(&mut self) {
let artifact_dir = match build::build(files).await {
Ok(dir) => dir,
match build::build(&self.build_dir, &self.src_dir).await {
Ok(_) => {},
Err(err) => {
_ = self
.sender
@ -233,9 +193,7 @@ impl Handler {
return;
}
};
}
async fn run_program(&mut self) {
let process = match run::run(&self.build_dir).await {
Ok(process) => process,
Err(err) => {
@ -247,6 +205,7 @@ impl Handler {
let stderr = BufReader::new(process.stderr).lines();
let stdin = process.stdin;
_ = self.sender.send(Message::Text(serde_json::to_string(&ServerMsg::Start).unwrap_or_default().into())).await;
self.process = Some(
Process { process: process.child, stderr, stdout, stdin }
)
@ -254,5 +213,5 @@ impl Handler {
}
pub async fn ws_handler(socket: WebSocket) {
Handler::local(socket, "target".into(), "src".into());
Handler::local(socket, "../target".into(), "../src".into()).run().await;
}

View file

@ -1,21 +1,12 @@
use axum::{
Router,
extract::ws::{Message, WebSocket, WebSocketUpgrade},
extract::ws::WebSocketUpgrade,
routing::get,
};
use futures_util::{
stream::{SplitSink, SplitStream},
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, net::SocketAddr, path::PathBuf, time::Duration};
use tokio::{
io::{BufReader, Lines},
process::{Child, ChildStderr, ChildStdin, ChildStdout},
};
use std::net::SocketAddr;
use tower_http::services::ServeDir;
use crate::build::TempDir;
pub mod build;
pub mod run;
pub mod local;
@ -25,9 +16,13 @@ pub mod remote;
async fn main() {
let app = Router::new()
.route(
"/ws",
"/ws/local",
get(|ws: WebSocketUpgrade| async move { ws.on_upgrade(remote::ws_handler) }),
)
.route(
"/ws/remote",
get(|ws: WebSocketUpgrade| async move { ws.on_upgrade(local::ws_handler) }),
)
.fallback_service(ServeDir::new("ui"));
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
@ -39,9 +34,8 @@ async fn main() {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum ClientMsg {
Compile(Option<HashMap<String, String>>),
Start,
Stop,
Input {
@ -65,25 +59,4 @@ pub enum ServerMsg<'a> {
Seg3(u32),
}
struct Process {
process: Child,
stderr: Lines<BufReader<ChildStderr>>,
stdout: Lines<BufReader<ChildStdout>>,
stdin: ChildStdin,
}
struct Handler {
sender: SplitSink<WebSocket, Message>,
receiver: SplitStream<WebSocket>,
build_dir: TempDir,
src_dir: PathBuf,
program: Option<PathBuf>,
process: Option<Process>,
refresh_time: Duration,
}
pub type HResult<T> = Result<T, Box<dyn std::error::Error + Sync + Send>>;
pub type HResult<T> = Result<T, Box<dyn std::error::Error + Sync + Send>>;

View file

@ -4,9 +4,9 @@ use axum::{
use futures_util::{
SinkExt, StreamExt,
};
use std::collections::HashMap;
use std::{collections::HashMap, time::Duration};
use tokio::{
io::{AsyncBufReadExt, BufReader},
io::{AsyncBufReadExt, BufReader}, time::Instant,
};
use crate::{ClientMsg, HResult, ServerMsg, build, run};
@ -23,7 +23,7 @@ pub async fn ws_handler(socket: WebSocket) {
};
let artifact_dir = match build::build(files).await{
let artifact_dir = match build::copy_and_build(files).await{
Ok(dir) => dir,
Err(err) => {
_ = sender.send(Message::Text(format!("Failed to build: {err}").into())).await;
@ -42,6 +42,8 @@ pub async fn ws_handler(socket: WebSocket) {
let mut serr = BufReader::new(process.stderr).lines();
let artifact_prefix = artifact_dir.to_str().unwrap_or("\0\0NOPE");
let mut print_deadline = Instant::now();
let result: HResult<()> = async {
loop{
@ -52,7 +54,6 @@ pub async fn ws_handler(socket: WebSocket) {
Some(Ok(Message::Text(msg))) => {
let input = serde_json::from_str::<'_, ClientMsg>(&msg)?;
match input{
ClientMsg::Compile(_) => {},
ClientMsg::Start => {},
ClientMsg::Stop => break,
ClientMsg::Input { switch, buttons } => {
@ -110,8 +111,9 @@ pub async fn ws_handler(socket: WebSocket) {
}
}
}
_ = tokio::time::sleep(std::time::Duration::from_millis(30)) => {
_ = tokio::time::sleep_until(print_deadline) => {
use tokio::io::AsyncWriteExt;
print_deadline += Duration::from_millis(30);
process.stdin.write_all("\n".as_bytes()).await?;
}
}