mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 00:21:38 +03:00
Merge branch 'v0.4.0' into hf/start-from-commandline
Conflicts: src/main.rs src/terminal.rs
This commit is contained in:
commit
fd8c478a8d
28
README.md
28
README.md
@ -1,4 +1,3 @@
|
||||
Last updated: 11/01/23
|
||||
## Setup
|
||||
|
||||
### Building components
|
||||
@ -32,7 +31,7 @@ Get an eth-sepolia-rpc API key and pass that as an argument. You can get one for
|
||||
|
||||
Make sure not to use the same home directory for two nodes at once! You can use any name for the home directory: here we just use `home`.
|
||||
```bash
|
||||
cargo +nightly run --release -- --home home --rpc wss://eth-sepolia.g.alchemy.com/v2/<your-api-key>
|
||||
cargo +nightly run --release -- home --rpc wss://eth-sepolia.g.alchemy.com/v2/<your-api-key>
|
||||
```
|
||||
|
||||
On boot you will be prompted to navigate to `localhost:8080`. Make sure your ETH wallet is connected to the Sepolia test network. Login should be straightforward, just submit the transactions and follow the flow. If you want to register a new ID you will either need [Sepolia testnet tokens](https://www.infura.io/faucet/sepolia) or an invite code.
|
||||
@ -40,20 +39,33 @@ On boot you will be prompted to navigate to `localhost:8080`. Make sure your ETH
|
||||
|
||||
## Terminal syntax
|
||||
|
||||
- CTRL+C or CTRL+D to shutdown node
|
||||
- CTRL+V to toggle verbose mode, which is on by default
|
||||
- CTRL+C or CTRL+D to gracefully shutdown node
|
||||
- CTRL+V to toggle through verbose modes (0-3, 0 is default and lowest verbosity)
|
||||
|
||||
- CTRL+J to toggle debug mode
|
||||
- CTRL+S to step through events in debug mode
|
||||
|
||||
- CTRL+L to toggle logging mode, which writes all terminal output to the `.terminal_log` file. Off by default, this will write all events and verbose prints with timestamps.
|
||||
|
||||
- CTRL+A to jump to beginning of input
|
||||
- CTRL+E to jump to end of input
|
||||
- UpArrow/DownArrow or CTRL+P/CTRL+N to move up and down through command history
|
||||
- CTRL+R to search history, CTRL+R again to toggle through search results, CTRL+G to cancel search
|
||||
|
||||
- `!message <name> <app> <json>`: send a card with a JSON value to another node or yourself. <name> can be `our`, which will be interpreted as our node's username.
|
||||
- `!hi <name> <string>`: send a text message to another node's command line.
|
||||
- `<name>` is either the name of a node or `our`, which will fill in the present node name
|
||||
- more to come
|
||||
- `/message <address> <json>`: send an inter-process message. <address> is formatted as <node>@<process_id>. <process_id> is formatted as <process_name>:<package_name>:<publisher_node>.
|
||||
- Example: `/message our@net:sys:uqbar diagnostics`
|
||||
- `our` will always be interpolated by the system as your node's name
|
||||
- Can also use `/m` for same command: `/m our@net:sys:uqbar diagnostics`
|
||||
- `/app <address>`: set the terminal to a mode where all messages go to a specific app. To clear this selection, use `/app clear` or simply `/app`. This is useful for apps that have a command line interface.
|
||||
- Example: `/app our@net:sys:uqbar`, then `/m diagnostics`
|
||||
- Can also use `/a` for same command: `/a our@net:sys:uqbar`
|
||||
- Example of sending many messages:
|
||||
- `/a ben.uq@net:sys:uqbar`
|
||||
- `/m hey there`
|
||||
- `/m how are you?`
|
||||
- `/a` (to exit app mode)
|
||||
- `/hi <name> <string>`: send a text message to another node's command line.
|
||||
- Example: `/hi ben.uq hello world`
|
||||
|
||||
## Example usage
|
||||
|
||||
|
117
modules/terminal/Cargo.lock
generated
117
modules/terminal/Cargo.lock
generated
@ -23,6 +23,12 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@ -35,6 +41,21 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.10"
|
||||
@ -61,12 +82,33 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.1.0"
|
||||
@ -102,6 +144,12 @@ version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
@ -237,12 +285,62 @@ dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.10.1"
|
||||
@ -257,16 +355,31 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "uqbar_process_lib"
|
||||
version = "0.2.0"
|
||||
source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=e53c124#e53c124ec95ef99c06d201d4d08dada8ec691d29"
|
||||
version = "0.3.0"
|
||||
source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=5e1b94a#5e1b94ae2f85c66da33ec52117a72b90d53c4d22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"http",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"url",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
@ -15,7 +15,7 @@ anyhow = "1.0"
|
||||
bincode = "1.3.3"
|
||||
serde = {version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" }
|
||||
uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "5e1b94a" }
|
||||
wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "5390bab780733f1660d14c254ec985df2816bf1d" }
|
||||
|
||||
[lib]
|
||||
|
@ -1,6 +1,6 @@
|
||||
use anyhow::anyhow;
|
||||
use uqbar_process_lib::uqbar::process::standard as wit;
|
||||
use uqbar_process_lib::{Address, ProcessId, Request, println};
|
||||
use uqbar_process_lib::{println, Address, Request};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
path: "../../wit",
|
||||
@ -10,46 +10,70 @@ wit_bindgen::generate!({
|
||||
},
|
||||
});
|
||||
|
||||
fn serialize_message(message: &&str) -> anyhow::Result<Vec<u8>> {
|
||||
struct TerminalState {
|
||||
our: Address,
|
||||
current_target: Option<Address>,
|
||||
}
|
||||
|
||||
fn serialize_message(message: &str) -> anyhow::Result<Vec<u8>> {
|
||||
Ok(message.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn parse_command(our_name: &str, line: &str) -> anyhow::Result<()> {
|
||||
fn parse_command(state: &mut TerminalState, line: &str) -> anyhow::Result<()> {
|
||||
let (head, tail) = line.split_once(" ").unwrap_or((&line, ""));
|
||||
match head {
|
||||
"" | " " => return Ok(()),
|
||||
"!hi" => {
|
||||
// send a raw text message over the network to a node
|
||||
"/hi" => {
|
||||
let (node_id, message) = match tail.split_once(" ") {
|
||||
Some((s, t)) => (s, t),
|
||||
None => return Err(anyhow!("invalid command: \"{line}\"")),
|
||||
};
|
||||
let node_id = if node_id == "our" { our_name } else { node_id };
|
||||
let node_id = if node_id == "our" { &state.our.node } else { node_id };
|
||||
Request::new()
|
||||
.target(Address::new(node_id, "net:sys:uqbar").unwrap())?
|
||||
.ipc(&message, serialize_message)?
|
||||
.target((node_id, "net", "sys", "uqbar"))
|
||||
.ipc(message)
|
||||
.expects_response(5)
|
||||
.send()?;
|
||||
Ok(())
|
||||
}
|
||||
"!message" => {
|
||||
let (node_id, tail) = match tail.split_once(" ") {
|
||||
Some((s, t)) => (s, t),
|
||||
None => return Err(anyhow!("invalid command: \"{line}\"")),
|
||||
// set the current target, so you can message it without specifying
|
||||
"/a" | "/app" => {
|
||||
if tail == "" || tail == "clear" {
|
||||
state.current_target = None;
|
||||
println!("current target cleared");
|
||||
return Ok(());
|
||||
}
|
||||
let Ok(target) = Address::from_str(tail) else {
|
||||
return Err(anyhow!("invalid address: \"{tail}\""));
|
||||
};
|
||||
let (target_process, ipc) = match tail.split_once(" ") {
|
||||
Some((a, p)) => (a, p),
|
||||
None => return Err(anyhow!("invalid command: \"{line}\"")),
|
||||
};
|
||||
let node_id = if node_id == "our" { our_name } else { node_id };
|
||||
let process = ProcessId::from_str(target_process).unwrap_or_else(|_| {
|
||||
ProcessId::from_str(&format!("{}:sys:uqbar", target_process)).unwrap()
|
||||
});
|
||||
Request::new()
|
||||
.target(Address::new(node_id, process).unwrap())?
|
||||
.ipc(&ipc, serialize_message)?
|
||||
.send()?;
|
||||
println!("current target set to {target}");
|
||||
state.current_target = Some(target);
|
||||
Ok(())
|
||||
}
|
||||
// send a message to a specified app
|
||||
// if no current_target is set, require it,
|
||||
// otherwise use the current_target
|
||||
"/m" | "/message" => {
|
||||
if let Some(target) = &state.current_target {
|
||||
Request::new()
|
||||
.target(target.clone())
|
||||
.ipc(tail)
|
||||
.send()
|
||||
} else {
|
||||
let (target, ipc) = match tail.split_once(" ") {
|
||||
Some((a, p)) => (a, p),
|
||||
None => return Err(anyhow!("invalid command: \"{line}\"")),
|
||||
};
|
||||
let Ok(target) = Address::from_str(target) else {
|
||||
return Err(anyhow!("invalid address: \"{target}\""));
|
||||
};
|
||||
Request::new()
|
||||
.target(target)
|
||||
.ipc(ipc)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
_ => return Err(anyhow!("invalid command: \"{line}\"")),
|
||||
}
|
||||
}
|
||||
@ -57,8 +81,10 @@ fn parse_command(our_name: &str, line: &str) -> anyhow::Result<()> {
|
||||
struct Component;
|
||||
impl Guest for Component {
|
||||
fn init(our: String) {
|
||||
let our = Address::from_str(&our).unwrap();
|
||||
println!("terminal: start");
|
||||
let mut state = TerminalState {
|
||||
our: Address::from_str(&our).unwrap(),
|
||||
current_target: None,
|
||||
};
|
||||
loop {
|
||||
let (source, message) = match wit::receive() {
|
||||
Ok((source, message)) => (source, message),
|
||||
@ -69,21 +95,22 @@ impl Guest for Component {
|
||||
};
|
||||
match message {
|
||||
wit::Message::Request(wit::Request {
|
||||
expects_response,
|
||||
ipc,
|
||||
..
|
||||
}) => {
|
||||
if our.node != source.node || our.process != source.process {
|
||||
if state.our.node != source.node || state.our.process != source.process {
|
||||
continue;
|
||||
}
|
||||
match parse_command(&our.node, std::str::from_utf8(&ipc).unwrap_or_default()) {
|
||||
match parse_command(&mut state, std::str::from_utf8(&ipc).unwrap_or_default()) {
|
||||
Ok(()) => continue,
|
||||
Err(e) => println!("terminal: {e}"),
|
||||
}
|
||||
}
|
||||
wit::Message::Response((wit::Response { ipc, metadata, .. }, _)) => {
|
||||
wit::Message::Response((wit::Response { ipc, .. }, _)) => {
|
||||
if let Ok(txt) = std::str::from_utf8(&ipc) {
|
||||
println!("terminal: net response: {txt}");
|
||||
println!("response from {source}: {txt}");
|
||||
} else {
|
||||
println!("response from {source}: {ipc:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ type HttpSender = tokio::sync::oneshot::Sender<(HttpResponse, Vec<u8>)>;
|
||||
/// mapping from an open websocket connection to a channel that will ingest
|
||||
/// WebSocketPush messages from the app that handles the connection, and
|
||||
/// send them to the connection.
|
||||
type WebSocketSenders = Arc<DashMap<u64, (ProcessId, WebSocketSender)>>;
|
||||
type WebSocketSenders = Arc<DashMap<u32, (ProcessId, WebSocketSender)>>;
|
||||
type WebSocketSender = tokio::sync::mpsc::Sender<warp::ws::Message>;
|
||||
|
||||
type PathBindings = Arc<RwLock<Router<BoundPath>>>;
|
||||
@ -127,20 +127,31 @@ async fn serve(
|
||||
let cloned_msg_tx = send_to_loop.clone();
|
||||
let cloned_our = our.clone();
|
||||
let cloned_jwt_secret_bytes = jwt_secret_bytes.clone();
|
||||
let cloned_print_tx = print_tx.clone();
|
||||
let ws_route = warp::path::end()
|
||||
.and(warp::ws())
|
||||
.and(warp::any().map(move || cloned_our.clone()))
|
||||
.and(warp::any().map(move || cloned_jwt_secret_bytes.clone()))
|
||||
.and(warp::any().map(move || ws_senders.clone()))
|
||||
.and(warp::any().map(move || cloned_msg_tx.clone()))
|
||||
.and(warp::any().map(move || cloned_print_tx.clone()))
|
||||
.map(
|
||||
|ws_connection: Ws,
|
||||
our: Arc<String>,
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
ws_senders: WebSocketSenders,
|
||||
send_to_loop: MessageSender| {
|
||||
send_to_loop: MessageSender,
|
||||
print_tx: PrintSender| {
|
||||
ws_connection.on_upgrade(move |ws: WebSocket| async move {
|
||||
maintain_websocket(ws, our, jwt_secret_bytes, ws_senders, send_to_loop).await
|
||||
maintain_websocket(
|
||||
ws,
|
||||
our,
|
||||
jwt_secret_bytes,
|
||||
ws_senders,
|
||||
send_to_loop,
|
||||
print_tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
},
|
||||
);
|
||||
@ -379,6 +390,7 @@ async fn maintain_websocket(
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
ws_senders: WebSocketSenders,
|
||||
send_to_loop: MessageSender,
|
||||
_print_tx: PrintSender,
|
||||
) {
|
||||
let (mut write_stream, mut read_stream) = ws.split();
|
||||
|
||||
@ -420,10 +432,35 @@ async fn maintain_websocket(
|
||||
return;
|
||||
}
|
||||
|
||||
let ws_channel_id: u64 = rand::random();
|
||||
let ws_channel_id: u32 = rand::random();
|
||||
let (ws_sender, mut ws_receiver) = tokio::sync::mpsc::channel(100);
|
||||
ws_senders.insert(ws_channel_id, (owner_process.clone(), ws_sender));
|
||||
|
||||
// send a message to the process associated with this channel
|
||||
// notifying them that the channel is now open
|
||||
let _ = send_to_loop
|
||||
.send(KernelMessage {
|
||||
id: rand::random(),
|
||||
source: Address {
|
||||
node: our.to_string(),
|
||||
process: HTTP_SERVER_PROCESS_ID.clone(),
|
||||
},
|
||||
target: Address {
|
||||
node: our.to_string(),
|
||||
process: owner_process.clone(),
|
||||
},
|
||||
rsvp: None,
|
||||
message: Message::Request(Request {
|
||||
inherit: false,
|
||||
expects_response: None,
|
||||
ipc: serde_json::to_vec(&HttpServerAction::WebSocketOpen(ws_channel_id)).unwrap(),
|
||||
metadata: None,
|
||||
}),
|
||||
payload: None,
|
||||
signed_capabilities: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
// respond to the client notifying them that the channel is now open
|
||||
let Ok(()) = write_stream
|
||||
.send(warp::ws::Message::text(
|
||||
@ -505,7 +542,7 @@ async fn maintain_websocket(
|
||||
}
|
||||
|
||||
async fn websocket_close(
|
||||
channel_id: u64,
|
||||
channel_id: u32,
|
||||
process: ProcessId,
|
||||
ws_senders: &WebSocketSenders,
|
||||
send_to_loop: &MessageSender,
|
||||
@ -585,9 +622,6 @@ async fn handle_app_message(
|
||||
// the receiver will automatically trigger a 503 when sender is dropped.
|
||||
return;
|
||||
};
|
||||
let Some((_id, (path, channel))) = http_response_senders.remove(&km.id) else {
|
||||
return;
|
||||
};
|
||||
// XX REFACTOR THIS:
|
||||
// for the login case, todo refactor out?
|
||||
let segments: Vec<&str> = path
|
||||
@ -628,7 +662,7 @@ async fn handle_app_message(
|
||||
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: format!(
|
||||
"SET WS AUTH COOKIE WITH USERNAME: {}",
|
||||
ws_auth_username
|
||||
@ -638,7 +672,7 @@ async fn handle_app_message(
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = channel.send((
|
||||
let _ = sender.send((
|
||||
HttpResponse {
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
|
@ -72,19 +72,19 @@ pub enum HttpServerAction {
|
||||
cache: bool,
|
||||
},
|
||||
/// Processes will RECEIVE this kind of request when a client connects to them.
|
||||
/// If a process does not want this websocket open, they can respond with an
|
||||
/// [`enum@HttpServerAction::WebSocketClose`] message.
|
||||
WebSocketOpen(u64),
|
||||
/// If a process does not want this websocket open, they should issue a *request*
|
||||
/// containing a [`enum@HttpServerAction::WebSocketClose`] message and this channel ID.
|
||||
WebSocketOpen(u32),
|
||||
/// Processes can both SEND and RECEIVE this kind of request.
|
||||
/// When sent, expects a payload containing the WebSocket message bytes to send.
|
||||
WebSocketPush {
|
||||
channel_id: u64,
|
||||
channel_id: u32,
|
||||
message_type: WsMessageType,
|
||||
},
|
||||
/// Processes can both SEND and RECEIVE this kind of request. Sending will
|
||||
/// close a socket the process controls. Receiving will indicate that the
|
||||
/// client closed the socket.
|
||||
WebSocketClose(u64),
|
||||
WebSocketClose(u32),
|
||||
}
|
||||
|
||||
/// The possible message types for WebSocketPush. Ping and Pong are limited to 125 bytes
|
||||
@ -129,7 +129,7 @@ pub struct WsRegister {
|
||||
/// Structure sent from this server to client websocket upon opening a new connection.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct WsRegisterResponse {
|
||||
pub channel_id: u64,
|
||||
pub channel_id: u32,
|
||||
// TODO symmetric key exchange here
|
||||
}
|
||||
|
||||
|
@ -201,30 +201,30 @@ async fn handle_kernel_request(
|
||||
valid_capabilities.insert(cap);
|
||||
}
|
||||
|
||||
// if process is not public, give the initializer and itself the messaging cap.
|
||||
if !public {
|
||||
valid_capabilities.insert(t::Capability {
|
||||
issuer: t::Address {
|
||||
node: our_name.clone(),
|
||||
process: id.clone(),
|
||||
},
|
||||
params: "\"messaging\"".into(),
|
||||
});
|
||||
caps_oracle
|
||||
.send(t::CapMessage::Add {
|
||||
on: km.source.process.clone(),
|
||||
cap: t::Capability {
|
||||
issuer: t::Address {
|
||||
node: our_name.clone(),
|
||||
process: id.clone(),
|
||||
},
|
||||
params: "\"messaging\"".into(),
|
||||
// give the initializer and itself the messaging cap.
|
||||
// NOTE: we do this even if the process is public, because
|
||||
// a process might redundantly call grant_messaging.
|
||||
valid_capabilities.insert(t::Capability {
|
||||
issuer: t::Address {
|
||||
node: our_name.clone(),
|
||||
process: id.clone(),
|
||||
},
|
||||
params: "\"messaging\"".into(),
|
||||
});
|
||||
caps_oracle
|
||||
.send(t::CapMessage::Add {
|
||||
on: km.source.process.clone(),
|
||||
cap: t::Capability {
|
||||
issuer: t::Address {
|
||||
node: our_name.clone(),
|
||||
process: id.clone(),
|
||||
},
|
||||
responder: tokio::sync::oneshot::channel().0,
|
||||
})
|
||||
.await
|
||||
.expect("event loop: fatal: sender died");
|
||||
}
|
||||
params: "\"messaging\"".into(),
|
||||
},
|
||||
responder: tokio::sync::oneshot::channel().0,
|
||||
})
|
||||
.await
|
||||
.expect("event loop: fatal: sender died");
|
||||
|
||||
// fires "success" response back if successful
|
||||
match start_process(
|
||||
@ -380,7 +380,7 @@ async fn handle_kernel_request(
|
||||
None => {
|
||||
let _ = send_to_terminal
|
||||
.send(t::Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: format!("kernel: no such process {:?} to kill", process_id),
|
||||
})
|
||||
.await;
|
||||
@ -771,7 +771,7 @@ pub async fn kernel(
|
||||
Some(wrapped_network_error) = network_error_recv.recv() => {
|
||||
let _ = send_to_terminal.send(
|
||||
t::Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: format!("event loop: got network error: {:?}", wrapped_network_error)
|
||||
}
|
||||
).await;
|
||||
@ -923,7 +923,7 @@ pub async fn kernel(
|
||||
// display every single event when verbose
|
||||
let _ = send_to_terminal.send(
|
||||
t::Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 3,
|
||||
content: format!("event loop: got message: {}", kernel_message)
|
||||
}
|
||||
).await;
|
||||
|
@ -213,7 +213,7 @@ impl ProcessState {
|
||||
let _ = self
|
||||
.send_to_terminal
|
||||
.send(t::Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: format!("kernel: dropping Response {:?}", response),
|
||||
})
|
||||
.await;
|
||||
@ -354,19 +354,13 @@ impl ProcessState {
|
||||
println!("need non-None prompting_message to handle Response");
|
||||
return None;
|
||||
};
|
||||
match &prompting_message.rsvp {
|
||||
None => {
|
||||
let _ = self
|
||||
.send_to_terminal
|
||||
.send(t::Printout {
|
||||
verbosity: 1,
|
||||
content: "kernel: prompting_message has no rsvp".into(),
|
||||
})
|
||||
.await;
|
||||
None
|
||||
}
|
||||
Some(address) => Some((prompting_message.id, address.clone())),
|
||||
}
|
||||
Some((
|
||||
prompting_message.id,
|
||||
match &prompting_message.rsvp {
|
||||
None => prompting_message.source.clone(),
|
||||
Some(address) => address.clone(),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -419,7 +413,7 @@ pub async fn make_process_loop(
|
||||
}
|
||||
|
||||
let component =
|
||||
Component::new(&engine, wasm_bytes).expect("make_process_loop: couldn't read file");
|
||||
Component::new(&engine, wasm_bytes.clone()).expect("make_process_loop: couldn't read file");
|
||||
|
||||
let mut linker = Linker::new(&engine);
|
||||
Process::add_to_linker(&mut linker, |state: &mut ProcessWasi| state).unwrap();
|
||||
@ -476,7 +470,7 @@ pub async fn make_process_loop(
|
||||
Ok(()) => {
|
||||
let _ = send_to_terminal
|
||||
.send(t::Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: format!("process {} returned without error", metadata.our.process,),
|
||||
})
|
||||
.await;
|
||||
@ -564,7 +558,10 @@ pub async fn make_process_loop(
|
||||
.unwrap(),
|
||||
metadata: None,
|
||||
}),
|
||||
payload: None,
|
||||
payload: Some(t::Payload {
|
||||
mime: None,
|
||||
bytes: wasm_bytes,
|
||||
}),
|
||||
signed_capabilities: None,
|
||||
})
|
||||
.await
|
||||
|
@ -154,7 +154,7 @@ async fn indirect_networking(
|
||||
Ok(()) => continue,
|
||||
Err(e) => {
|
||||
print_tx.send(Printout {
|
||||
verbosity: 0,
|
||||
verbosity: 2,
|
||||
content: format!("net: error handling local message: {e}")
|
||||
}).await?;
|
||||
continue
|
||||
@ -322,7 +322,7 @@ async fn direct_networking(
|
||||
Ok(()) => continue,
|
||||
Err(e) => {
|
||||
print_tx.send(Printout {
|
||||
verbosity: 0,
|
||||
verbosity: 2,
|
||||
content: format!("net: error handling local message: {}", e)
|
||||
}).await?;
|
||||
continue;
|
||||
@ -405,14 +405,14 @@ async fn direct_networking(
|
||||
Ok(Ok(res)) => res,
|
||||
Ok(Err(e)) => {
|
||||
print_tx.send(Printout {
|
||||
verbosity: 0,
|
||||
verbosity: 2,
|
||||
content: format!("net: recv_connection failed: {e}"),
|
||||
}).await?;
|
||||
continue;
|
||||
}
|
||||
Err(_e) => {
|
||||
print_tx.send(Printout {
|
||||
verbosity: 0,
|
||||
verbosity: 2,
|
||||
content: "net: recv_connection timed out".into(),
|
||||
}).await?;
|
||||
continue;
|
||||
|
@ -499,7 +499,7 @@ pub async fn parse_hello_message(
|
||||
pub async fn print_debug(print_tx: &PrintSender, content: &str) {
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: content.into(),
|
||||
})
|
||||
.await;
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.e62f8e3a.css",
|
||||
"main.js": "/static/js/main.8ca457ab.js",
|
||||
"main.js": "/static/js/main.ad67a3c7.js",
|
||||
"index.html": "/index.html",
|
||||
"main.e62f8e3a.css.map": "/static/css/main.e62f8e3a.css.map",
|
||||
"main.8ca457ab.js.map": "/static/js/main.8ca457ab.js.map"
|
||||
"main.ad67a3c7.js.map": "/static/js/main.ad67a3c7.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.e62f8e3a.css",
|
||||
"static/js/main.8ca457ab.js"
|
||||
"static/js/main.ad67a3c7.js"
|
||||
]
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/register-ui/build/static/js/main.ad67a3c7.js.map
Normal file
1
src/register-ui/build/static/js/main.ad67a3c7.js.map
Normal file
File diff suppressed because one or more lines are too long
704
src/terminal.rs
704
src/terminal.rs
@ -1,704 +0,0 @@
|
||||
use crate::types::*;
|
||||
use anyhow::Result;
|
||||
use chrono::{Datelike, Local, Timelike};
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{
|
||||
DisableBracketedPaste, EnableBracketedPaste, Event, EventStream, KeyCode, KeyEvent,
|
||||
KeyModifiers,
|
||||
},
|
||||
execute,
|
||||
style::Print,
|
||||
terminal::{self, disable_raw_mode, enable_raw_mode, ClearType},
|
||||
};
|
||||
use futures::{future::FutureExt, StreamExt};
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::{read_to_string, File, OpenOptions};
|
||||
use std::io::{stdout, BufWriter, Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CommandHistory {
|
||||
pub lines: VecDeque<String>,
|
||||
pub working_line: Option<String>,
|
||||
pub max_size: usize,
|
||||
pub index: usize,
|
||||
pub history_writer: BufWriter<File>,
|
||||
}
|
||||
|
||||
impl CommandHistory {
|
||||
fn new(max_size: usize, history: String, history_writer: BufWriter<File>) -> Self {
|
||||
let mut lines = VecDeque::with_capacity(max_size);
|
||||
for line in history.lines() {
|
||||
lines.push_front(line.to_string());
|
||||
}
|
||||
Self {
|
||||
lines,
|
||||
working_line: None,
|
||||
max_size,
|
||||
index: 0,
|
||||
history_writer,
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, line: String) {
|
||||
self.working_line = None;
|
||||
// only add line to history if it's not exactly the same
|
||||
// as the previous line
|
||||
if &line != self.lines.front().unwrap_or(&"".into()) {
|
||||
let _ = writeln!(self.history_writer, "{}", &line);
|
||||
self.lines.push_front(line);
|
||||
}
|
||||
self.index = 0;
|
||||
if self.lines.len() > self.max_size {
|
||||
self.lines.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_prev(&mut self, working_line: &str) -> Option<String> {
|
||||
if self.lines.is_empty() || self.index == self.lines.len() {
|
||||
return None;
|
||||
}
|
||||
self.index += 1;
|
||||
if self.index == 1 {
|
||||
self.working_line = Some(working_line.into());
|
||||
}
|
||||
let line = self.lines[self.index - 1].clone();
|
||||
Some(line)
|
||||
}
|
||||
|
||||
fn get_next(&mut self) -> Option<String> {
|
||||
if self.lines.is_empty() || self.index == 0 || self.index == 1 {
|
||||
self.index = 0;
|
||||
if let Some(line) = self.working_line.clone() {
|
||||
self.working_line = None;
|
||||
return Some(line);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
self.index -= 1;
|
||||
Some(self.lines[self.index - 1].clone())
|
||||
}
|
||||
|
||||
/// if depth = 0, find most recent command in history that contains the
|
||||
/// provided string. otherwise, skip the first <depth> matches.
|
||||
/// yes this is O(n) to provide desired ordering, can revisit if slow
|
||||
fn search(&mut self, find: &str, depth: usize) -> Option<String> {
|
||||
for (skips, line) in self.lines.iter().enumerate() {
|
||||
if line.contains(find) && skips == depth {
|
||||
return Some(line.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* terminal driver
|
||||
*/
|
||||
pub async fn terminal(
|
||||
our: Identity,
|
||||
version: &str,
|
||||
home_directory_path: String,
|
||||
event_loop: MessageSender,
|
||||
debug_event_loop: DebugSender,
|
||||
print_tx: PrintSender,
|
||||
mut print_rx: PrintReceiver,
|
||||
) -> Result<()> {
|
||||
let mut stdout = stdout();
|
||||
execute!(
|
||||
stdout,
|
||||
EnableBracketedPaste,
|
||||
terminal::SetTitle(format!("{}@{}", our.name, "uqbar"))
|
||||
)?;
|
||||
|
||||
let (mut win_cols, mut win_rows) = terminal::size().unwrap();
|
||||
// print initial splash screen, large if there's room, small otherwise
|
||||
if win_cols >= 93 {
|
||||
println!(
|
||||
"\x1b[38;5;128m{}\x1b[0m",
|
||||
format_args!(
|
||||
r#"
|
||||
,, UU
|
||||
s# lUL UU !p
|
||||
!UU lUL UU !UUlb
|
||||
#U !UU lUL UU !UUUUU#
|
||||
UU !UU lUL UU !UUUUUUUb
|
||||
UU !UU %" ;- !UUUUUUUU#
|
||||
$ UU !UU @UU#p !UUUUUUUUU#
|
||||
]U UU !# @UUUUS !UUUUUUUUUUb
|
||||
@U UU ! @UUUUUUlUUUUUUUUUUU 888
|
||||
UU UU ! @UUUUUUUUUUUUUUUUUU 888
|
||||
@U UU ! @UUUUUU!UUUUUUUUUUU 888
|
||||
'U UU !# @UUUU# !UUUUUUUUUU~ 888 888 .d88888 88888b. 8888b. 888d888
|
||||
\ UU !UU @UU#^ !UUUUUUUUU# 888 888 d88" 888 888 "88b "88b 888P"
|
||||
UU !UU @Np ,," !UUUUUUUU# 888 888 888 888 888 888 .d888888 888
|
||||
UU !UU lUL UU !UUUUUUU^ Y88b 888 Y88b 888 888 d88P 888 888 888
|
||||
"U !UU lUL UU !UUUUUf "Y88888 "Y88888 88888P" "Y888888 888
|
||||
!UU lUL UU !UUl^ 888
|
||||
`" lUL UU '^ 888 {}
|
||||
"" 888 version {}
|
||||
a general purpose sovereign cloud computer
|
||||
|
||||
networking public key: {}
|
||||
"#,
|
||||
our.name, version, our.networking_key,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"\x1b[38;5;128m{}\x1b[0m",
|
||||
format_args!(
|
||||
r#"
|
||||
888
|
||||
888
|
||||
888
|
||||
888 888 .d88888 88888b. 8888b. 888d888
|
||||
888 888 d88" 888 888 "88b "88b 888P"
|
||||
888 888 888 888 888 888 .d888888 888
|
||||
Y88b 888 Y88b 888 888 d88P 888 888 888
|
||||
"Y88888 "Y88888 88888P" "Y888888 888
|
||||
888
|
||||
888 {}
|
||||
888 version {}
|
||||
a general purpose sovereign cloud computer
|
||||
|
||||
networking pubkey: {}
|
||||
"#,
|
||||
our.name, version, our.networking_key,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
enable_raw_mode()?;
|
||||
let mut reader = EventStream::new();
|
||||
let mut current_line = format!("{} > ", our.name);
|
||||
let prompt_len: usize = our.name.len() + 3;
|
||||
let mut cursor_col: u16 = prompt_len.try_into().unwrap();
|
||||
let mut line_col: usize = cursor_col as usize;
|
||||
let mut in_step_through: bool = false;
|
||||
let mut verbose_mode: bool = false;
|
||||
let mut search_mode: bool = false;
|
||||
let mut search_depth: usize = 0;
|
||||
|
||||
let history_path = std::fs::canonicalize(&home_directory_path)
|
||||
.unwrap()
|
||||
.join(".terminal_history");
|
||||
let history = read_to_string(&history_path).unwrap_or_default();
|
||||
let history_handle = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(&history_path)
|
||||
.unwrap();
|
||||
let history_writer = BufWriter::new(history_handle);
|
||||
// TODO make adjustable max history length
|
||||
let mut command_history = CommandHistory::new(1000, history, history_writer);
|
||||
|
||||
let log_path = std::fs::canonicalize(&home_directory_path)
|
||||
.unwrap()
|
||||
.join(".terminal_log");
|
||||
let log_handle = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(&log_path)
|
||||
.unwrap();
|
||||
let mut log_writer = BufWriter::new(log_handle);
|
||||
|
||||
loop {
|
||||
let event = reader.next().fuse();
|
||||
|
||||
tokio::select! {
|
||||
prints = print_rx.recv() => match prints {
|
||||
Some(printout) => {
|
||||
let now = Local::now();
|
||||
let _ = writeln!(log_writer, "{} {}", now.to_rfc2822(), printout.content);
|
||||
if match printout.verbosity {
|
||||
0 => false,
|
||||
1 => !verbose_mode,
|
||||
_ => true
|
||||
} {
|
||||
continue;
|
||||
}
|
||||
let mut stdout = stdout.lock();
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows - 1),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(format!("{} {}/{} {:02}:{:02} ",
|
||||
now.weekday(),
|
||||
now.month(),
|
||||
now.day(),
|
||||
now.hour(),
|
||||
now.minute(),
|
||||
)),
|
||||
)?;
|
||||
for line in printout.content.lines() {
|
||||
execute!(
|
||||
stdout,
|
||||
Print(format!("\x1b[38;5;238m{}\x1b[0m\r\n", line)),
|
||||
)?;
|
||||
}
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
None => {
|
||||
write!(stdout.lock(), "terminal: lost print channel, crashing")?;
|
||||
break;
|
||||
}
|
||||
},
|
||||
maybe_event = event => match maybe_event {
|
||||
Some(Ok(event)) => {
|
||||
let mut stdout = stdout.lock();
|
||||
match event {
|
||||
// resize is super annoying because this event trigger often
|
||||
// comes "too late" to stop terminal from messing with the
|
||||
// already-printed lines. TODO figure out the right way
|
||||
// to compensate for this cross-platform and do this in a
|
||||
// generally stable way.
|
||||
Event::Resize(width, height) => {
|
||||
win_cols = width;
|
||||
win_rows = height;
|
||||
},
|
||||
// handle pasting of text from outside
|
||||
Event::Paste(pasted) => {
|
||||
current_line.insert_str(line_col, &pasted);
|
||||
line_col = current_line.len();
|
||||
cursor_col = std::cmp::min(line_col.try_into().unwrap_or(win_cols), win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
}
|
||||
// CTRL+C, CTRL+D: turn off the node
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) |
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('d'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
execute!(stdout, DisableBracketedPaste, terminal::SetTitle(""))?;
|
||||
disable_raw_mode()?;
|
||||
break;
|
||||
},
|
||||
// CTRL+V: toggle verbose mode
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('v'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
let _ = print_tx.send(
|
||||
Printout {
|
||||
verbosity: 0,
|
||||
content: match verbose_mode {
|
||||
true => "verbose mode off".into(),
|
||||
false => "verbose mode on".into(),
|
||||
}
|
||||
}
|
||||
).await;
|
||||
verbose_mode = !verbose_mode;
|
||||
},
|
||||
// CTRL+J: toggle debug mode -- makes system-level event loop step-through
|
||||
// CTRL+S: step through system-level event loop
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('j'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
let _ = print_tx.send(
|
||||
Printout {
|
||||
verbosity: 0,
|
||||
content: match in_step_through {
|
||||
true => "debug mode off".into(),
|
||||
false => "debug mode on: use CTRL+S to step through events".into(),
|
||||
}
|
||||
}
|
||||
).await;
|
||||
let _ = debug_event_loop.send(DebugCommand::Toggle).await;
|
||||
in_step_through = !in_step_through;
|
||||
},
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('s'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
let _ = debug_event_loop.send(DebugCommand::Step).await;
|
||||
},
|
||||
//
|
||||
// UP / CTRL+P: go up one command in history
|
||||
// DOWN / CTRL+N: go down one command in history
|
||||
//
|
||||
Event::Key(KeyEvent { code: KeyCode::Up, .. }) |
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
// go up one command in history
|
||||
match command_history.get_prev(¤t_line[prompt_len..]) {
|
||||
Some(line) => {
|
||||
current_line = format!("{} > {}", our.name, line);
|
||||
line_col = current_line.len();
|
||||
},
|
||||
None => {
|
||||
print!("\x07");
|
||||
},
|
||||
}
|
||||
cursor_col = std::cmp::min(current_line.len() as u16, win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(truncate_rightward(¤t_line, prompt_len, win_cols)),
|
||||
)?;
|
||||
},
|
||||
Event::Key(KeyEvent { code: KeyCode::Down, .. }) |
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
// go down one command in history
|
||||
match command_history.get_next() {
|
||||
Some(line) => {
|
||||
current_line = format!("{} > {}", our.name, line);
|
||||
line_col = current_line.len();
|
||||
},
|
||||
None => {
|
||||
print!("\x07");
|
||||
},
|
||||
}
|
||||
cursor_col = std::cmp::min(current_line.len() as u16, win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(truncate_rightward(¤t_line, prompt_len, win_cols)),
|
||||
)?;
|
||||
},
|
||||
//
|
||||
// CTRL+A: jump to beginning of line
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
line_col = prompt_len;
|
||||
cursor_col = prompt_len.try_into().unwrap();
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_from_left(¤t_line, prompt_len, win_cols, line_col)),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
//
|
||||
// CTRL+E: jump to end of line
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('e'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
line_col = current_line.len();
|
||||
cursor_col = std::cmp::min(line_col.try_into().unwrap_or(win_cols), win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_from_right(¤t_line, prompt_len, win_cols, line_col)),
|
||||
)?;
|
||||
},
|
||||
//
|
||||
// CTRL+R: enter search mode
|
||||
// if already in search mode, increase search depth
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('r'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
if search_mode {
|
||||
search_depth += 1;
|
||||
}
|
||||
search_mode = true;
|
||||
if let Some(result) = command_history.search(¤t_line[prompt_len..], search_depth) {
|
||||
// todo show search result with search query underlined
|
||||
// and cursor in correct spot
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_in_place(
|
||||
&format!("{} * {}", our.name, result),
|
||||
prompt_len,
|
||||
win_cols,
|
||||
(line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
} else {
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
}
|
||||
},
|
||||
//
|
||||
// CTRL+G: exit search mode
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('g'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
// just show true current line as usual
|
||||
search_mode = false;
|
||||
search_depth = 0;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
//
|
||||
// handle keypress events
|
||||
//
|
||||
Event::Key(k) => {
|
||||
match k.code {
|
||||
KeyCode::Char(c) => {
|
||||
current_line.insert(line_col, c);
|
||||
if cursor_col < win_cols {
|
||||
cursor_col += 1;
|
||||
}
|
||||
line_col += 1;
|
||||
if search_mode {
|
||||
if let Some(result) = command_history.search(¤t_line[prompt_len..], search_depth) {
|
||||
// todo show search result with search query underlined
|
||||
// and cursor in correct spot
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_in_place(
|
||||
&format!("{} * {}", our.name, result),
|
||||
prompt_len,
|
||||
win_cols,
|
||||
(line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
continue
|
||||
}
|
||||
}
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
KeyCode::Backspace => {
|
||||
if line_col == prompt_len {
|
||||
continue;
|
||||
}
|
||||
if cursor_col as usize == line_col {
|
||||
cursor_col -= 1;
|
||||
}
|
||||
line_col -= 1;
|
||||
current_line.remove(line_col);
|
||||
if search_mode {
|
||||
if let Some(result) = command_history.search(¤t_line[prompt_len..], search_depth) {
|
||||
// todo show search result with search query underlined
|
||||
// and cursor in correct spot
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_in_place(
|
||||
&format!("{} * {}", our.name, result),
|
||||
prompt_len,
|
||||
win_cols,
|
||||
(line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
continue
|
||||
}
|
||||
}
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
KeyCode::Left => {
|
||||
if cursor_col as usize == prompt_len {
|
||||
if line_col == prompt_len {
|
||||
// at the very beginning of the current typed line
|
||||
continue;
|
||||
} else {
|
||||
// virtual scroll leftward through line
|
||||
line_col -= 1;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_from_left(¤t_line, prompt_len, win_cols, line_col)),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
// simply move cursor and line position left
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveLeft(1),
|
||||
)?;
|
||||
cursor_col -= 1;
|
||||
line_col -= 1;
|
||||
}
|
||||
},
|
||||
KeyCode::Right => {
|
||||
if line_col == current_line.len() {
|
||||
// at the very end of the current typed line
|
||||
continue;
|
||||
}
|
||||
if cursor_col < (win_cols - 1) {
|
||||
// simply move cursor and line position right
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveRight(1),
|
||||
)?;
|
||||
cursor_col += 1;
|
||||
line_col += 1;
|
||||
} else {
|
||||
// virtual scroll rightward through line
|
||||
line_col += 1;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(truncate_from_right(¤t_line, prompt_len, win_cols, line_col)),
|
||||
)?;
|
||||
}
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
// if we were in search mode, pull command from that
|
||||
let command = if !search_mode {
|
||||
current_line[prompt_len..].to_string()
|
||||
} else {
|
||||
command_history.search(
|
||||
¤t_line[prompt_len..],
|
||||
search_depth
|
||||
).unwrap_or(current_line[prompt_len..].to_string())
|
||||
};
|
||||
let next = format!("{} > ", our.name);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(¤t_line),
|
||||
Print("\r\n"),
|
||||
Print(&next),
|
||||
)?;
|
||||
search_mode = false;
|
||||
search_depth = 0;
|
||||
current_line = next;
|
||||
command_history.add(command.clone());
|
||||
cursor_col = prompt_len.try_into().unwrap();
|
||||
line_col = prompt_len;
|
||||
// println!("terminal: sending\r");
|
||||
let _err = event_loop.send(
|
||||
KernelMessage {
|
||||
id: rand::random(),
|
||||
source: Address {
|
||||
node: our.name.clone(),
|
||||
process: TERMINAL_PROCESS_ID.clone(),
|
||||
},
|
||||
target: Address {
|
||||
node: our.name.clone(),
|
||||
process: TERMINAL_PROCESS_ID.clone(),
|
||||
},
|
||||
rsvp: None,
|
||||
message: Message::Request(Request {
|
||||
inherit: false,
|
||||
expects_response: None,
|
||||
ipc: command.into_bytes(),
|
||||
metadata: None,
|
||||
}),
|
||||
payload: None,
|
||||
signed_capabilities: None,
|
||||
}
|
||||
).await;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => println!("Error: {:?}\r", e),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
execute!(stdout.lock(), DisableBracketedPaste, terminal::SetTitle(""))?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn truncate_rightward(s: &str, prompt_len: usize, width: u16) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
let sans_prompt = &s[prompt_len..];
|
||||
s[..prompt_len].to_string() + &sans_prompt[(s.len() - width as usize)..]
|
||||
}
|
||||
|
||||
/// print prompt, then as many chars as will fit in term starting from line_col
|
||||
fn truncate_from_left(s: &str, prompt_len: usize, width: u16, line_col: usize) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
s[..prompt_len].to_string() + &s[line_col..(width as usize - prompt_len + line_col)]
|
||||
}
|
||||
|
||||
/// print prompt, then as many chars as will fit in term leading up to line_col
|
||||
fn truncate_from_right(s: &str, prompt_len: usize, width: u16, line_col: usize) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
s[..prompt_len].to_string() + &s[(prompt_len + (line_col - width as usize))..line_col]
|
||||
}
|
||||
|
||||
/// if line is wider than the terminal, truncate it intelligently,
|
||||
/// keeping the cursor in the same relative position.
|
||||
fn truncate_in_place(
|
||||
s: &str,
|
||||
prompt_len: usize,
|
||||
width: u16,
|
||||
(line_col, cursor_col): (usize, u16),
|
||||
) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
// always keep prompt at left
|
||||
let prompt = &s[..prompt_len];
|
||||
// print as much of the command fits left of col_in_command before cursor_col,
|
||||
// then fill out the rest up to width
|
||||
let end = width as usize + line_col - cursor_col as usize;
|
||||
if end > s.len() {
|
||||
return s.to_string();
|
||||
}
|
||||
prompt.to_string() + &s[(prompt_len + line_col - cursor_col as usize)..end]
|
||||
}
|
602
src/terminal/mod.rs
Normal file
602
src/terminal/mod.rs
Normal file
@ -0,0 +1,602 @@
|
||||
use crate::types::*;
|
||||
use anyhow::Result;
|
||||
use chrono::{Datelike, Local, Timelike};
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{
|
||||
DisableBracketedPaste, EnableBracketedPaste, Event, EventStream, KeyCode, KeyEvent,
|
||||
KeyModifiers,
|
||||
},
|
||||
execute,
|
||||
style::Print,
|
||||
terminal::{self, disable_raw_mode, enable_raw_mode, ClearType},
|
||||
};
|
||||
use futures::{future::FutureExt, StreamExt};
|
||||
use std::fs::{read_to_string, OpenOptions};
|
||||
use std::io::{stdout, BufWriter, Write};
|
||||
|
||||
mod utils;
|
||||
|
||||
/*
|
||||
* terminal driver
|
||||
*/
|
||||
pub async fn terminal(
|
||||
our: Identity,
|
||||
version: &str,
|
||||
home_directory_path: String,
|
||||
event_loop: MessageSender,
|
||||
debug_event_loop: DebugSender,
|
||||
print_tx: PrintSender,
|
||||
mut print_rx: PrintReceiver,
|
||||
) -> Result<()> {
|
||||
let mut stdout = stdout();
|
||||
execute!(
|
||||
stdout,
|
||||
EnableBracketedPaste,
|
||||
terminal::SetTitle(format!("{}@{}", our.name, "uqbar"))
|
||||
)?;
|
||||
|
||||
let (mut win_cols, mut win_rows) = terminal::size().unwrap();
|
||||
// print initial splash screen, large if there's room, small otherwise
|
||||
if win_cols >= 93 {
|
||||
println!(
|
||||
"\x1b[38;5;128m{}\x1b[0m",
|
||||
format_args!(
|
||||
r#"
|
||||
,, UU
|
||||
s# lUL UU !p
|
||||
!UU lUL UU !UUlb
|
||||
#U !UU lUL UU !UUUUU#
|
||||
UU !UU lUL UU !UUUUUUUb
|
||||
UU !UU %" ;- !UUUUUUUU#
|
||||
$ UU !UU @UU#p !UUUUUUUUU#
|
||||
]U UU !# @UUUUS !UUUUUUUUUUb
|
||||
@U UU ! @UUUUUUlUUUUUUUUUUU 888
|
||||
UU UU ! @UUUUUUUUUUUUUUUUUU 888
|
||||
@U UU ! @UUUUUU!UUUUUUUUUUU 888
|
||||
'U UU !# @UUUU# !UUUUUUUUUU~ 888 888 .d88888 88888b. 8888b. 888d888
|
||||
\ UU !UU @UU#^ !UUUUUUUUU# 888 888 d88" 888 888 "88b "88b 888P"
|
||||
UU !UU @Np ,," !UUUUUUUU# 888 888 888 888 888 888 .d888888 888
|
||||
UU !UU lUL UU !UUUUUUU^ Y88b 888 Y88b 888 888 d88P 888 888 888
|
||||
"U !UU lUL UU !UUUUUf "Y88888 "Y88888 88888P" "Y888888 888
|
||||
!UU lUL UU !UUl^ 888
|
||||
`" lUL UU '^ 888 {}
|
||||
"" 888 version {}
|
||||
a general purpose sovereign cloud computer
|
||||
|
||||
networking public key: {}
|
||||
"#,
|
||||
our.name, version, our.networking_key,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"\x1b[38;5;128m{}\x1b[0m",
|
||||
format_args!(
|
||||
r#"
|
||||
888
|
||||
888
|
||||
888
|
||||
888 888 .d88888 88888b. 8888b. 888d888
|
||||
888 888 d88" 888 888 "88b "88b 888P"
|
||||
888 888 888 888 888 888 .d888888 888
|
||||
Y88b 888 Y88b 888 888 d88P 888 888 888
|
||||
"Y88888 "Y88888 88888P" "Y888888 888
|
||||
888
|
||||
888 {}
|
||||
888 version {}
|
||||
a general purpose sovereign cloud computer
|
||||
|
||||
networking pubkey: {}
|
||||
"#,
|
||||
our.name, version, our.networking_key,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
enable_raw_mode()?;
|
||||
let mut reader = EventStream::new();
|
||||
let mut current_line = format!("{} > ", our.name);
|
||||
let prompt_len: usize = our.name.len() + 3;
|
||||
let mut cursor_col: u16 = prompt_len.try_into().unwrap();
|
||||
let mut line_col: usize = cursor_col as usize;
|
||||
let mut in_step_through: bool = false;
|
||||
let mut verbose_mode: u8 = 0; // least verbose mode
|
||||
let mut search_mode: bool = false;
|
||||
let mut search_depth: usize = 0;
|
||||
let mut logging_mode: bool = false;
|
||||
|
||||
let history_path = std::fs::canonicalize(&home_directory_path)
|
||||
.unwrap()
|
||||
.join(".terminal_history");
|
||||
let history = read_to_string(&history_path).unwrap_or_default();
|
||||
let history_handle = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(&history_path)
|
||||
.unwrap();
|
||||
let history_writer = BufWriter::new(history_handle);
|
||||
// TODO make adjustable max history length
|
||||
let mut command_history = utils::CommandHistory::new(1000, history, history_writer);
|
||||
|
||||
let log_path = std::fs::canonicalize(&home_directory_path)
|
||||
.unwrap()
|
||||
.join(".terminal_log");
|
||||
let log_handle = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(&log_path)
|
||||
.unwrap();
|
||||
let mut log_writer = BufWriter::new(log_handle);
|
||||
|
||||
loop {
|
||||
let event = reader.next().fuse();
|
||||
|
||||
tokio::select! {
|
||||
Some(printout) = print_rx.recv() => {
|
||||
let now = Local::now();
|
||||
if logging_mode {
|
||||
let _ = writeln!(log_writer, "[{}] {}", now.to_rfc2822(), printout.content);
|
||||
}
|
||||
if printout.verbosity > verbose_mode {
|
||||
continue;
|
||||
}
|
||||
let mut stdout = stdout.lock();
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows - 1),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(format!("{} {}/{} {:02}:{:02} ",
|
||||
now.weekday(),
|
||||
now.month(),
|
||||
now.day(),
|
||||
now.hour(),
|
||||
now.minute(),
|
||||
)),
|
||||
)?;
|
||||
for line in printout.content.lines() {
|
||||
execute!(
|
||||
stdout,
|
||||
Print(format!("\x1b[38;5;238m{}\x1b[0m\r\n", line)),
|
||||
)?;
|
||||
}
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(utils::truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
Some(Ok(event)) = event => {
|
||||
let mut stdout = stdout.lock();
|
||||
match event {
|
||||
// resize is super annoying because this event trigger often
|
||||
// comes "too late" to stop terminal from messing with the
|
||||
// already-printed lines. TODO figure out the right way
|
||||
// to compensate for this cross-platform and do this in a
|
||||
// generally stable way.
|
||||
Event::Resize(width, height) => {
|
||||
win_cols = width;
|
||||
win_rows = height;
|
||||
},
|
||||
// handle pasting of text from outside
|
||||
Event::Paste(pasted) => {
|
||||
current_line.insert_str(line_col, &pasted);
|
||||
line_col = current_line.len();
|
||||
cursor_col = std::cmp::min(line_col.try_into().unwrap_or(win_cols), win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(utils::truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
}
|
||||
// CTRL+C, CTRL+D: turn off the node
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) |
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('d'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
execute!(stdout, DisableBracketedPaste, terminal::SetTitle(""))?;
|
||||
disable_raw_mode()?;
|
||||
break;
|
||||
},
|
||||
// CTRL+V: toggle through verbosity modes
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('v'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
// go from low to high, then reset to 0
|
||||
match verbose_mode {
|
||||
0 => verbose_mode = 1,
|
||||
1 => verbose_mode = 2,
|
||||
2 => verbose_mode = 3,
|
||||
_ => verbose_mode = 0,
|
||||
}
|
||||
let _ = print_tx.send(
|
||||
Printout {
|
||||
verbosity: 0,
|
||||
content: match verbose_mode {
|
||||
0 => "verbose mode: off".into(),
|
||||
1 => "verbose mode: debug".into(),
|
||||
2 => "verbose mode: super-debug".into(),
|
||||
_ => "verbose mode: full event loop".into(),
|
||||
}
|
||||
}
|
||||
).await;
|
||||
},
|
||||
// CTRL+J: toggle debug mode -- makes system-level event loop step-through
|
||||
// CTRL+S: step through system-level event loop
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('j'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
let _ = print_tx.send(
|
||||
Printout {
|
||||
verbosity: 0,
|
||||
content: match in_step_through {
|
||||
true => "debug mode off".into(),
|
||||
false => "debug mode on: use CTRL+S to step through events".into(),
|
||||
}
|
||||
}
|
||||
).await;
|
||||
let _ = debug_event_loop.send(DebugCommand::Toggle).await;
|
||||
in_step_through = !in_step_through;
|
||||
},
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('s'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
let _ = debug_event_loop.send(DebugCommand::Step).await;
|
||||
},
|
||||
//
|
||||
// CTRL+L: toggle logging mode
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('l'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
logging_mode = !logging_mode;
|
||||
let _ = print_tx.send(
|
||||
Printout {
|
||||
verbosity: 0,
|
||||
content: match logging_mode {
|
||||
true => "logging mode: on".into(),
|
||||
false => "logging mode: off".into(),
|
||||
}
|
||||
}
|
||||
).await;
|
||||
},
|
||||
//
|
||||
// UP / CTRL+P: go up one command in history
|
||||
// DOWN / CTRL+N: go down one command in history
|
||||
//
|
||||
Event::Key(KeyEvent { code: KeyCode::Up, .. }) |
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
// go up one command in history
|
||||
match command_history.get_prev(¤t_line[prompt_len..]) {
|
||||
Some(line) => {
|
||||
current_line = format!("{} > {}", our.name, line);
|
||||
line_col = current_line.len();
|
||||
},
|
||||
None => {
|
||||
print!("\x07");
|
||||
},
|
||||
}
|
||||
cursor_col = std::cmp::min(current_line.len() as u16, win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_rightward(¤t_line, prompt_len, win_cols)),
|
||||
)?;
|
||||
},
|
||||
Event::Key(KeyEvent { code: KeyCode::Down, .. }) |
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
// go down one command in history
|
||||
match command_history.get_next() {
|
||||
Some(line) => {
|
||||
current_line = format!("{} > {}", our.name, line);
|
||||
line_col = current_line.len();
|
||||
},
|
||||
None => {
|
||||
print!("\x07");
|
||||
},
|
||||
}
|
||||
cursor_col = std::cmp::min(current_line.len() as u16, win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_rightward(¤t_line, prompt_len, win_cols)),
|
||||
)?;
|
||||
},
|
||||
//
|
||||
// CTRL+A: jump to beginning of line
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
line_col = prompt_len;
|
||||
cursor_col = prompt_len.try_into().unwrap();
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(utils::truncate_from_left(¤t_line, prompt_len, win_cols, line_col)),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
//
|
||||
// CTRL+E: jump to end of line
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('e'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
line_col = current_line.len();
|
||||
cursor_col = std::cmp::min(line_col.try_into().unwrap_or(win_cols), win_cols);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(utils::truncate_from_right(¤t_line, prompt_len, win_cols, line_col)),
|
||||
)?;
|
||||
},
|
||||
//
|
||||
// CTRL+R: enter search mode
|
||||
// if already in search mode, increase search depth
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('r'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
if search_mode {
|
||||
search_depth += 1;
|
||||
}
|
||||
search_mode = true;
|
||||
let search_query = ¤t_line[prompt_len..];
|
||||
if search_query.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(result) = command_history.search(search_query, search_depth) {
|
||||
let result_underlined = utils::underline(result, search_query);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_in_place(
|
||||
&format!("{} * {}", our.name, result_underlined),
|
||||
prompt_len,
|
||||
win_cols,
|
||||
(line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
} else {
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
}
|
||||
},
|
||||
//
|
||||
// CTRL+G: exit search mode
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('g'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
// just show true current line as usual
|
||||
search_mode = false;
|
||||
search_depth = 0;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
//
|
||||
// handle keypress events
|
||||
//
|
||||
Event::Key(k) => {
|
||||
match k.code {
|
||||
KeyCode::Char(c) => {
|
||||
current_line.insert(line_col, c);
|
||||
if cursor_col < win_cols {
|
||||
cursor_col += 1;
|
||||
}
|
||||
line_col += 1;
|
||||
if search_mode {
|
||||
let search_query = ¤t_line[prompt_len..];
|
||||
if let Some(result) = command_history.search(search_query, search_depth) {
|
||||
let result_underlined = utils::underline(result, search_query);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_in_place(
|
||||
&format!("{} * {}", our.name, result_underlined),
|
||||
prompt_len,
|
||||
win_cols,
|
||||
(line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
KeyCode::Backspace => {
|
||||
if line_col == prompt_len {
|
||||
continue;
|
||||
}
|
||||
if cursor_col as usize == line_col {
|
||||
cursor_col -= 1;
|
||||
}
|
||||
line_col -= 1;
|
||||
current_line.remove(line_col);
|
||||
if search_mode {
|
||||
let search_query = ¤t_line[prompt_len..];
|
||||
if let Some(result) = command_history.search(search_query, search_depth) {
|
||||
let result_underlined = utils::underline(result, search_query);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_in_place(
|
||||
&format!("{} * {}", our.name, result_underlined),
|
||||
prompt_len,
|
||||
win_cols,
|
||||
(line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(utils::truncate_in_place(¤t_line, prompt_len, win_cols, (line_col, cursor_col))),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
},
|
||||
KeyCode::Left => {
|
||||
if cursor_col as usize == prompt_len {
|
||||
if line_col == prompt_len {
|
||||
// at the very beginning of the current typed line
|
||||
continue;
|
||||
} else {
|
||||
// virtual scroll leftward through line
|
||||
line_col -= 1;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(utils::truncate_from_left(¤t_line, prompt_len, win_cols, line_col)),
|
||||
cursor::MoveTo(cursor_col, win_rows),
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
// simply move cursor and line position left
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveLeft(1),
|
||||
)?;
|
||||
cursor_col -= 1;
|
||||
line_col -= 1;
|
||||
}
|
||||
},
|
||||
KeyCode::Right => {
|
||||
if line_col == current_line.len() {
|
||||
// at the very end of the current typed line
|
||||
continue;
|
||||
}
|
||||
if cursor_col < (win_cols - 1) {
|
||||
// simply move cursor and line position right
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveRight(1),
|
||||
)?;
|
||||
cursor_col += 1;
|
||||
line_col += 1;
|
||||
} else {
|
||||
// virtual scroll rightward through line
|
||||
line_col += 1;
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
Print(utils::truncate_from_right(¤t_line, prompt_len, win_cols, line_col)),
|
||||
)?;
|
||||
}
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
// if we were in search mode, pull command from that
|
||||
let command = if !search_mode {
|
||||
current_line[prompt_len..].to_string()
|
||||
} else {
|
||||
command_history.search(
|
||||
¤t_line[prompt_len..],
|
||||
search_depth
|
||||
).unwrap_or(¤t_line[prompt_len..]).to_string()
|
||||
};
|
||||
let next = format!("{} > ", our.name);
|
||||
execute!(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(&format!("{} > {}", our.name, command)),
|
||||
Print("\r\n"),
|
||||
Print(&next),
|
||||
)?;
|
||||
search_mode = false;
|
||||
search_depth = 0;
|
||||
current_line = next;
|
||||
command_history.add(command.clone());
|
||||
cursor_col = prompt_len.try_into().unwrap();
|
||||
line_col = prompt_len;
|
||||
event_loop.send(
|
||||
KernelMessage {
|
||||
id: rand::random(),
|
||||
source: Address {
|
||||
node: our.name.clone(),
|
||||
process: TERMINAL_PROCESS_ID.clone(),
|
||||
},
|
||||
target: Address {
|
||||
node: our.name.clone(),
|
||||
process: TERMINAL_PROCESS_ID.clone(),
|
||||
},
|
||||
rsvp: None,
|
||||
message: Message::Request(Request {
|
||||
inherit: false,
|
||||
expects_response: None,
|
||||
ipc: command.into_bytes(),
|
||||
metadata: None,
|
||||
}),
|
||||
payload: None,
|
||||
signed_capabilities: None,
|
||||
}
|
||||
).await.expect("terminal: couldn't execute command!");
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
execute!(stdout.lock(), DisableBracketedPaste, terminal::SetTitle(""))?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
147
src/terminal/utils.rs
Normal file
147
src/terminal/utils.rs
Normal file
@ -0,0 +1,147 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::{BufWriter, Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandHistory {
|
||||
lines: VecDeque<String>,
|
||||
working_line: Option<String>,
|
||||
max_size: usize,
|
||||
index: usize,
|
||||
history_writer: BufWriter<File>,
|
||||
}
|
||||
|
||||
impl CommandHistory {
|
||||
pub fn new(max_size: usize, history: String, history_writer: BufWriter<File>) -> Self {
|
||||
let mut lines = VecDeque::with_capacity(max_size);
|
||||
for line in history.lines() {
|
||||
lines.push_front(line.to_string());
|
||||
}
|
||||
Self {
|
||||
lines,
|
||||
working_line: None,
|
||||
max_size,
|
||||
index: 0,
|
||||
history_writer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, line: String) {
|
||||
self.working_line = None;
|
||||
// only add line to history if it's not exactly the same
|
||||
// as the previous line
|
||||
if &line != self.lines.front().unwrap_or(&"".into()) {
|
||||
let _ = writeln!(self.history_writer, "{}", &line);
|
||||
self.lines.push_front(line);
|
||||
}
|
||||
self.index = 0;
|
||||
if self.lines.len() > self.max_size {
|
||||
self.lines.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_prev(&mut self, working_line: &str) -> Option<String> {
|
||||
if self.lines.is_empty() || self.index == self.lines.len() {
|
||||
return None;
|
||||
}
|
||||
self.index += 1;
|
||||
if self.index == 1 {
|
||||
self.working_line = Some(working_line.into());
|
||||
}
|
||||
let line = self.lines[self.index - 1].clone();
|
||||
Some(line)
|
||||
}
|
||||
|
||||
pub fn get_next(&mut self) -> Option<String> {
|
||||
if self.lines.is_empty() || self.index == 0 || self.index == 1 {
|
||||
self.index = 0;
|
||||
if let Some(line) = self.working_line.clone() {
|
||||
self.working_line = None;
|
||||
return Some(line);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
self.index -= 1;
|
||||
Some(self.lines[self.index - 1].clone())
|
||||
}
|
||||
|
||||
/// if depth = 0, find most recent command in history that contains the
|
||||
/// provided string. otherwise, skip the first <depth> matches.
|
||||
/// yes this is O(n) to provide desired ordering, can revisit if slow
|
||||
pub fn search(&mut self, find: &str, depth: usize) -> Option<&str> {
|
||||
let mut skips = 0;
|
||||
// if there is at least one match, and we've skipped past it, return oldest match
|
||||
let mut last_match: Option<&str> = None;
|
||||
for line in self.lines.iter() {
|
||||
if line.contains(find) {
|
||||
last_match = Some(line);
|
||||
if skips == depth {
|
||||
return Some(line);
|
||||
} else {
|
||||
skips += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
last_match
|
||||
}
|
||||
}
|
||||
|
||||
pub fn underline(s: &str, to_underline: &str) -> String {
|
||||
// format result string to have query portion underlined
|
||||
let mut result = s.to_string();
|
||||
let u_start = s.find(to_underline).unwrap();
|
||||
let u_end = u_start + to_underline.len();
|
||||
result.insert_str(u_end, "\x1b[24m");
|
||||
result.insert_str(u_start, "\x1b[4m");
|
||||
result
|
||||
}
|
||||
|
||||
pub fn truncate_rightward(s: &str, prompt_len: usize, width: u16) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
let sans_prompt = &s[prompt_len..];
|
||||
s[..prompt_len].to_string() + &sans_prompt[(s.len() - width as usize)..]
|
||||
}
|
||||
|
||||
/// print prompt, then as many chars as will fit in term starting from line_col
|
||||
pub fn truncate_from_left(s: &str, prompt_len: usize, width: u16, line_col: usize) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
s[..prompt_len].to_string() + &s[line_col..(width as usize - prompt_len + line_col)]
|
||||
}
|
||||
|
||||
/// print prompt, then as many chars as will fit in term leading up to line_col
|
||||
pub fn truncate_from_right(s: &str, prompt_len: usize, width: u16, line_col: usize) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
s[..prompt_len].to_string() + &s[(prompt_len + (line_col - width as usize))..line_col]
|
||||
}
|
||||
|
||||
/// if line is wider than the terminal, truncate it intelligently,
|
||||
/// keeping the cursor in the same relative position.
|
||||
pub fn truncate_in_place(
|
||||
s: &str,
|
||||
prompt_len: usize,
|
||||
width: u16,
|
||||
(line_col, cursor_col): (usize, u16),
|
||||
) -> String {
|
||||
if s.len() <= width as usize {
|
||||
// no adjustment to be made
|
||||
return s.to_string();
|
||||
}
|
||||
// always keep prompt at left
|
||||
let prompt = &s[..prompt_len];
|
||||
// print as much of the command fits left of col_in_command before cursor_col,
|
||||
// then fill out the rest up to width
|
||||
let end = width as usize + line_col - cursor_col as usize;
|
||||
if end > s.len() {
|
||||
return s.to_string();
|
||||
}
|
||||
prompt.to_string() + &s[(prompt_len + line_col - cursor_col as usize)..end]
|
||||
}
|
@ -833,10 +833,11 @@ pub struct WrappedSendError {
|
||||
pub error: SendError,
|
||||
}
|
||||
|
||||
/// A terminal printout. Verbosity level is from low to high, and for
|
||||
/// now, only 0 and 1 are used. Level 0 is always printed, level 1 is
|
||||
/// only printed if the terminal is in verbose mode. Numbers greater
|
||||
/// than 1 are reserved for future use and will be ignored for now.
|
||||
/// A terminal printout. Verbosity level is from low to high.
|
||||
/// - `0`: always printed
|
||||
/// - `1`: verbose, used for debugging
|
||||
/// - `2`: very verbose: shows runtime information
|
||||
/// - `3`: very verbose: shows every event in event loop
|
||||
pub struct Printout {
|
||||
pub verbosity: u8,
|
||||
pub content: String,
|
||||
|
@ -717,7 +717,7 @@ async fn handle_request(
|
||||
} else {
|
||||
send_to_terminal
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: format!(
|
||||
"vfs: not sending response: {:?}",
|
||||
serde_json::from_slice::<VfsResponse>(&ipc)
|
||||
@ -798,7 +798,7 @@ async fn match_request(
|
||||
} else {
|
||||
send_to_terminal
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: format!("vfs: overwriting file {}", full_path),
|
||||
})
|
||||
.await
|
||||
@ -866,7 +866,7 @@ async fn match_request(
|
||||
if vfs.path_to_key.contains_key(&full_path) {
|
||||
send_to_terminal
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: format!("vfs: overwriting file {}", full_path),
|
||||
})
|
||||
.await
|
||||
@ -924,7 +924,7 @@ async fn match_request(
|
||||
} else {
|
||||
send_to_terminal
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
verbosity: 2,
|
||||
content: format!("vfs: overwriting file {}", full_path),
|
||||
})
|
||||
.await
|
||||
|
Loading…
Reference in New Issue
Block a user