├── .gitignore ├── Cargo.toml ├── Makefile ├── README.md ├── examples ├── echo │ ├── Cargo.toml │ ├── Makefile │ ├── echo.wasm │ ├── index.html │ └── src │ │ └── lib.rs └── helloworld │ ├── Cargo.toml │ ├── Makefile │ ├── helloworld.wasm │ ├── index.html │ └── src │ └── lib.rs ├── src └── lib.rs ├── wash.js └── wash.wasm /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wash" 3 | version = "0.0.1" 4 | authors = ["Richard Anaya"] 5 | edition = "2018" 6 | description = "an interactive shell for web assembly" 7 | license = "MIT" 8 | 9 | [lib] 10 | crate-type =["cdylib"] 11 | 12 | [dependencies] 13 | web-dom = "0.1.3" 14 | joss = "0" 15 | ref_thread_local = "0.0" 16 | serde_derive="1" 17 | serde="1" 18 | serde_json="1" 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @cargo build --target wasm32-unknown-unknown --release 3 | @cp target/wasm32-unknown-unknown/release/wash.wasm . 4 | serve: 5 | http-server -p 8080 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wash 🖥️ 2 | 3 | `wash` is a web assembly shell for a unix-like operating system. Applications use [JOSS (JSON Operating System Schema)](https://github.com/web-dom/joss/) to communicate with the operating system. For example a hello world: 4 | 5 | ```rust 6 | use joss; 7 | 8 | #[no_mangle] 9 | pub fn main() -> () { 10 | // write to stdout 11 | joss::syscall(r#"{ 12 | "operation": "write_file", 13 | "file_descriptor": 1, 14 | "text":"Hello World!" 15 | }"#.to_owned()); 16 | } 17 | ``` 18 | 19 | See a demo [here](https://web-dom.github.io/wash/examples/helloworld/) 20 | 21 | `wash` is meant to be very easily embeddable with a simple html element that defines what commands are available: 22 | 23 | ```html 24 | 25 | 26 | 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/echo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "echo" 3 | version = "0.1.0" 4 | authors = ["Richard "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type =["cdylib"] 9 | 10 | [dependencies] 11 | joss="0" 12 | serde_derive="1" 13 | serde="1" 14 | serde_json="1" 15 | malloc="0" 16 | web-dom = "0.1.3" 17 | -------------------------------------------------------------------------------- /examples/echo/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @cd .. && cd .. && make 3 | @cargo build --target wasm32-unknown-unknown --release 4 | @cp target/wasm32-unknown-unknown/release/echo.wasm . 5 | -------------------------------------------------------------------------------- /examples/echo/echo.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-dom/wash/4d7a88a77eff06c82b124e05ab6b7bd96dd33c16/examples/echo/echo.wasm -------------------------------------------------------------------------------- /examples/echo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wash 5 | 6 | 7 | 8 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/echo/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | extern crate malloc; 4 | use joss; 5 | use serde_json::{from_str, json}; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | struct CommandLineArguments { 9 | arguments: Vec, 10 | } 11 | 12 | #[no_mangle] 13 | fn main() { 14 | // get command line args 15 | let request_json = json!({ 16 | "operation": "get_command_line_arguments" 17 | }); 18 | let response = joss::syscall(request_json.to_string()); 19 | let response_json: CommandLineArguments = from_str(&response).unwrap(); 20 | let output = response_json.arguments.clone().join(" "); 21 | 22 | // write to stdout 23 | let output_json = json!({ 24 | "operation": "write_file", 25 | "file_descriptor": 1, 26 | "text":output 27 | }); 28 | joss::syscall(output_json.to_string()); 29 | } 30 | -------------------------------------------------------------------------------- /examples/helloworld/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "helloworld" 3 | version = "0.0.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | crate-type =["cdylib"] 8 | 9 | [dependencies] 10 | joss="0" 11 | -------------------------------------------------------------------------------- /examples/helloworld/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @cd .. && cd .. && make 3 | @cargo build --target wasm32-unknown-unknown --release 4 | @cp target/wasm32-unknown-unknown/release/helloworld.wasm . 5 | -------------------------------------------------------------------------------- /examples/helloworld/helloworld.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-dom/wash/4d7a88a77eff06c82b124e05ab6b7bd96dd33c16/examples/helloworld/helloworld.wasm -------------------------------------------------------------------------------- /examples/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wash 5 | 6 | 7 | 8 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/helloworld/src/lib.rs: -------------------------------------------------------------------------------- 1 | use joss; 2 | 3 | #[no_mangle] 4 | pub fn main() -> () { 5 | // write to stdout 6 | joss::syscall(r#"{ 7 | "operation": "write_file", 8 | "file_descriptor": 1, 9 | "text":"Hello World!" 10 | }"#.to_owned()); 11 | } 12 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use core::str::from_utf8; 2 | use joss; 3 | use ref_thread_local::RefThreadLocal; 4 | use std::collections::HashMap; 5 | use web_dom::*; 6 | #[macro_use] 7 | extern crate ref_thread_local; 8 | 9 | #[derive(Default)] 10 | struct Shell { 11 | window: DOMReference, 12 | document: DOMReference, 13 | component: DOMReference, 14 | canvas: DOMReference, 15 | ctx: DOMReference, 16 | shadow: DOMReference, 17 | characters: Vec, 18 | width: usize, 19 | height: usize, 20 | pos: usize, 21 | key_down_listener: EventListener, 22 | command: Vec, 23 | known_commands: HashMap, 24 | current_directory: String, 25 | } 26 | 27 | ref_thread_local! { 28 | static managed SHELL: Shell = Shell::default(); 29 | } 30 | 31 | impl Shell { 32 | fn handle_syscall(&mut self, response: CString) -> CString { 33 | let str = to_string(response); 34 | let command: serde_json::Value = serde_json::from_str(&str).unwrap(); 35 | let operation = match command["operation"].as_str() { 36 | Some(o) => o, 37 | _ => { 38 | console::error("unknown response"); 39 | return -1; 40 | } 41 | }; 42 | if operation == "wash_os_registered" { 43 | self.current_directory = "/".to_owned(); 44 | self.width = 60; 45 | self.height = 40; 46 | self.characters = vec![32; self.width * self.height]; 47 | self.component = command["root_component"].as_u64().unwrap() as i32; 48 | self.window = window(); 49 | self.document = window::get_document(self.window); 50 | self.canvas = document::create_element(self.document, "canvas"); 51 | element::set_attribute( 52 | self.canvas, 53 | "style", 54 | r#"image-rendering: -moz-crisp-edges; 55 | image-rendering: -webkit-crisp-edges; 56 | image-rendering: pixelated; 57 | image-rendering: crisp-edges;"#, 58 | ); 59 | element::set_attribute(self.canvas, "width", "800"); 60 | element::set_attribute(self.canvas, "height", "600"); 61 | self.shadow = customelement::attach_shadow(self.component); 62 | node::append_child(self.shadow, self.canvas); 63 | self.ctx = htmlcanvas::get_context(self.canvas, "2d"); 64 | canvas::set_fill_style(self.ctx, "black"); 65 | canvas::fill_rect(self.ctx, 0.0, 0.0, 800.0, 600.0); 66 | self.key_down_listener = create_event_listener(); 67 | eventtarget::add_event_listener(self.document, "keydown", self.key_down_listener); 68 | self.print("welcome to WASH, type \"help\" to see a list of commands\n"); 69 | self.characters[self.pos] = 124; 70 | self.render(); 71 | let child_count = element::get_child_element_count(self.component) as i32; 72 | if child_count > 0 { 73 | let mut el = element::get_first_element_child(self.component); 74 | for i in 0..child_count { 75 | if i != 0 { 76 | el = element::get_next_element_sibling(el); 77 | } 78 | self.known_commands.insert( 79 | to_string(element::get_attribute(el, "name")), 80 | to_string(element::get_attribute(el, "module")), 81 | ); 82 | } 83 | } 84 | } else if operation == "wash_process_command" { 85 | let op = command["command"]["operation"].as_str().unwrap(); 86 | if op == "get_command_line_arguments" { 87 | return to_cstring(r#"{"arguments":["vim","hello.txt"]}"#); 88 | } if op == "write_file" { 89 | let text = command["command"]["text"].as_str().unwrap(); 90 | self.print(text); 91 | } 92 | } else { 93 | console::error("unknown command"); 94 | return -1; 95 | } 96 | 0 97 | } 98 | 99 | fn print(&mut self, s: &str) { 100 | let cs: Vec = s.chars().collect(); 101 | for i in 0..cs.len() { 102 | let c = cs[i] as u8; 103 | self.characters[self.pos] = c; 104 | if c == 10 { 105 | self.pos = (self.pos / self.width + 1) * self.width; 106 | } else { 107 | self.pos += 1; 108 | } 109 | } 110 | self.render(); 111 | } 112 | 113 | fn handle_event(&mut self, listener: EventListener, event: Event) { 114 | if listener == self.key_down_listener { 115 | let key = keyboardevent::get_key(event); 116 | let key_chars: Vec = key.chars().collect(); 117 | if key.len() == 1 { 118 | let key_code = key_chars[0] as u8; 119 | self.process_char(key_code); 120 | self.command.push(key_code); 121 | } else if key == "Backspace" { 122 | self.process_char(8); 123 | } else if key == "Enter" { 124 | self.process_char(13); 125 | } else { 126 | return; 127 | } 128 | self.render(); 129 | } 130 | } 131 | 132 | fn execute(&mut self) { 133 | let s = from_utf8(&self.command).unwrap(); 134 | if s == "help" { 135 | let keys: Vec = self.known_commands.keys().map(|x| x.clone()).collect(); 136 | if keys.len() == 0 { 137 | self.print("embarassing...there doesn't seem to be any commands\n"); 138 | self.characters[self.pos] = 124; 139 | self.render(); 140 | } else { 141 | self.print(&keys.join(" ")); 142 | self.print("\n"); 143 | self.characters[self.pos] = 124; 144 | self.render(); 145 | } 146 | } else { 147 | let cmd = self.known_commands.get(s); 148 | if cmd.is_some() { 149 | joss::syscall( 150 | format!( 151 | r#"{{"operation":"wash_spawn_process","path":"{}"}}"#, 152 | cmd.unwrap() 153 | ) 154 | .to_owned(), 155 | ); 156 | } else { 157 | self.print("command not found\n"); 158 | self.characters[self.pos] = 124; 159 | self.render(); 160 | } 161 | } 162 | self.command = vec![]; 163 | } 164 | 165 | fn process_char(&mut self, k: u8) { 166 | if k == 13 { 167 | self.characters[self.pos] = 32; 168 | self.pos = (self.pos / self.width + 1) * self.width; 169 | self.characters[self.pos] = 124; 170 | self.execute(); 171 | return; 172 | } 173 | if k == 10 { 174 | self.characters[self.pos] = 32; 175 | self.pos = (self.pos / self.width + 1) * self.width; 176 | self.characters[self.pos] = 124; 177 | return; 178 | } 179 | if k == 8 { 180 | if self.command.len() == 0 { 181 | return; 182 | } 183 | self.command.pop(); 184 | self.characters[self.pos] = 32; 185 | self.pos -= 1; 186 | self.characters[self.pos] = 124; 187 | return; 188 | } 189 | self.characters[self.pos] = k; 190 | self.pos += 1; 191 | self.characters[self.pos] = 124; 192 | } 193 | 194 | fn render(&self) { 195 | canvas::set_fill_style(self.ctx, "black"); 196 | canvas::fill_rect(self.ctx, 0.0, 0.0, 800.0, 600.0); 197 | canvas::set_fill_style(self.ctx, "white"); 198 | canvas::set_font(self.ctx, "18px monospace"); 199 | for x in 0..self.width { 200 | for y in 0..self.height { 201 | canvas::fill_text( 202 | self.ctx, 203 | &(self.characters[y * self.width + x] as char).to_string(), 204 | 0.0 + (800.0 / self.width as f32) * x as f32, 205 | 15.0 + (600.0 / self.height as f32) * y as f32, 206 | ); 207 | } 208 | } 209 | } 210 | } 211 | 212 | #[no_mangle] 213 | pub fn callback(listener: EventListener, event: Event) -> () { 214 | SHELL.borrow_mut().handle_event(listener, event); 215 | } 216 | 217 | #[no_mangle] 218 | pub fn joss_syscall_handler(response: CString) -> CString { 219 | SHELL.borrow_mut().handle_syscall(response) 220 | } 221 | 222 | #[no_mangle] 223 | pub fn main() -> () { 224 | joss::syscall(r#"{"operation":"wash_register_os"}"#.to_owned()); 225 | } 226 | -------------------------------------------------------------------------------- /wash.js: -------------------------------------------------------------------------------- 1 | const OP_SYSTEM = 0; 2 | const SUBOP_INITIALIZATION = 0; 3 | const SUBOP_SPAWN = 1; 4 | 5 | class WasmShell extends HTMLElement { 6 | connectedCallback(){ 7 | this.nextProcessID = 0; 8 | let mod = this.getAttribute("module"); 9 | this.autorun = this.getAttribute("autorun"); 10 | this.spawn(mod==null?"wash.wasm":mod,this.nextProcessID); 11 | } 12 | 13 | spawn(wasmSrc,processId,os){ 14 | let isOperatingSystem = processId == 0; 15 | let component = this; 16 | component.nextProcessID+=1; 17 | WebDOM.run(wasmSrc,{ 18 | joss_syscall:function(request){ 19 | let _request = this.readStringFromMemory(request); 20 | let command = JSON.parse(_request); 21 | if(command.operation == "wash_register_os"){ 22 | if(isOperatingSystem){ 23 | let el = this.env.allocator().a(component); 24 | let response = { 25 | operation:"wash_os_registered", 26 | root_component: el 27 | }; 28 | this.exports.joss_syscall_handler(this.makeString(JSON.stringify(response))); 29 | } 30 | } 31 | else if(command.operation == "wash_spawn_process"){ 32 | if(isOperatingSystem){ 33 | window.setTimeout(()=>{ 34 | component.spawn(command.path,component.nextProcessID,this); 35 | },1) 36 | } 37 | } else { 38 | let response = JSON.stringify({operation:"wash_process_command",process:processId,command:command}); 39 | let r = os.exports.joss_syscall_handler(os.makeString(response)); 40 | let s = os.readStringFromMemory(r); 41 | return this.makeString(s); 42 | } 43 | } 44 | },function(){}) 45 | } 46 | } 47 | 48 | customElements.define('wasm-shell', WasmShell); 49 | -------------------------------------------------------------------------------- /wash.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-dom/wash/4d7a88a77eff06c82b124e05ab6b7bd96dd33c16/wash.wasm --------------------------------------------------------------------------------