happy bday!
This commit is contained in:
6
shell/.gitignore
vendored
Normal file
6
shell/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
bin/
|
||||
pkg/
|
||||
wasm-pack.log
|
||||
3
shell/.rustfmt.toml
Normal file
3
shell/.rustfmt.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
max_width = 120
|
||||
tab_spaces = 2
|
||||
format_macro_matchers = true
|
||||
28
shell/Cargo.toml
Normal file
28
shell/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "shell"
|
||||
version = "0.1.0"
|
||||
authors = ["Joey Eamigh <55670930+JoeyEamigh@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.84"
|
||||
yansi = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
once_cell = "1"
|
||||
rand = "0.8"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.34"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
26
shell/package.json
Normal file
26
shell/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "shell",
|
||||
"collaborators": [
|
||||
"Joey Eamigh <55670930+JoeyEamigh@users.noreply.github.com>"
|
||||
],
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "wasm-pack build --debug --no-pack",
|
||||
"build": "wasm-pack build --no-pack"
|
||||
},
|
||||
"files": [
|
||||
"./pkg/shell_bg.wasm",
|
||||
"./pkg/shell.js",
|
||||
"./pkg/shell_bg.js",
|
||||
"./pkg/shell.d.ts"
|
||||
],
|
||||
"module": "./pkg/shell.js",
|
||||
"types": "./pkg/shell.d.ts",
|
||||
"sideEffects": [
|
||||
"./pkg/shell.js",
|
||||
"./pkg/snippets/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@xterm/xterm": "^5.5.0"
|
||||
}
|
||||
}
|
||||
3
shell/rust-toolchain.toml
Normal file
3
shell/rust-toolchain.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
188
shell/src/core/challenge.rs
Normal file
188
shell/src/core/challenge.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{coreutils, program, vfs::DICTIONARY};
|
||||
use crate::ffi::xterm;
|
||||
|
||||
macro_rules! console_log { ($($t:tt)*) => (crate::ffi::console::log(&format_args!($($t)*).to_string())) }
|
||||
|
||||
static DICT: OnceCell<Vec<DictEntry>> = OnceCell::new();
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DictEntry {
|
||||
pub word: String,
|
||||
pub translation: String,
|
||||
}
|
||||
|
||||
pub fn challenge(term: &xterm::Terminal, _args: Vec<&str>) -> program::Program {
|
||||
coreutils::clear(term, vec![]);
|
||||
|
||||
term.writeln("welcome to the challenge!");
|
||||
|
||||
term.writeln(
|
||||
"\nyour task today is to translate 15 words from esperanto to english, then 15 words from english to esperanto",
|
||||
);
|
||||
|
||||
term.writeln("\ngood luck!");
|
||||
|
||||
term.writeln("\npress return to continue");
|
||||
|
||||
handle_input
|
||||
}
|
||||
|
||||
fn handle_input(term: &xterm::Terminal, state: &mut program::ProgramState, char: char) -> Result<Option<usize>, ()> {
|
||||
let data = if let Some(data) = state.data.downcast_mut::<HashMap<String, usize>>() {
|
||||
data
|
||||
} else {
|
||||
state.data = Box::new(HashMap::<String, usize>::new());
|
||||
state.data.downcast_mut::<HashMap<String, usize>>().unwrap()
|
||||
};
|
||||
|
||||
let correct = data.get("correct").copied().unwrap_or(0);
|
||||
let incorrect = data.get("incorrect").copied().unwrap_or(0);
|
||||
let remaining = data.get("remaining").copied().unwrap_or(30);
|
||||
|
||||
match char {
|
||||
'\r' => {
|
||||
if data.is_empty() {
|
||||
coreutils::clear(term, vec![]);
|
||||
|
||||
data.insert("correct".to_string(), 0);
|
||||
data.insert("incorrect".to_string(), 0);
|
||||
data.insert("remaining".to_string(), 30);
|
||||
|
||||
draw_score(term, correct, incorrect, remaining);
|
||||
draw_question(term, data);
|
||||
term.write("> ");
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if remaining == 0 {
|
||||
use yansi::Paint;
|
||||
|
||||
term.writeln(&format!(
|
||||
"\n\n🎉 {} 🎉\n\nclick here to claim your prize: {}\n\n",
|
||||
"happy birthday kirsten!".green().bright(),
|
||||
"https://www.youtube.com/watch?v=dQw4w9WgXcQ".cyan().bright()
|
||||
));
|
||||
|
||||
return Ok(Some(0));
|
||||
}
|
||||
|
||||
handle_answer(term, data, &state.buffer);
|
||||
|
||||
state.buffer.clear();
|
||||
return Ok(None);
|
||||
}
|
||||
'\u{7f}' => {
|
||||
if state.buffer.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
state.buffer.pop();
|
||||
term.write("\u{8} \u{8}");
|
||||
}
|
||||
_ => {
|
||||
state.buffer.push(char);
|
||||
term.write_char(char);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_answer(term: &xterm::Terminal, data: &mut HashMap<String, usize>, answer: &str) {
|
||||
let dict = DICT.get_or_init(|| serde_json::from_str(DICTIONARY).expect("failed to parse dictionary"));
|
||||
let index = data.get("index").copied().unwrap_or(0);
|
||||
let mut correct = data.get("correct").copied().unwrap_or(0);
|
||||
let mut incorrect = data.get("incorrect").copied().unwrap_or(0);
|
||||
let mut remaining = data.get("remaining").copied().unwrap_or(30);
|
||||
|
||||
console_log!("index: {}", index);
|
||||
console_log!("translation: {}", dict[index].translation);
|
||||
console_log!("word: {}", dict[index].word);
|
||||
console_log!("answer: {}", answer);
|
||||
console_log!("correct: {}", dict[index].translation == *answer);
|
||||
|
||||
if correct < 15 {
|
||||
if dict[index].translation == *answer {
|
||||
correct += 1;
|
||||
remaining -= 1;
|
||||
|
||||
data.insert("correct".to_string(), correct);
|
||||
data.insert("remaining".to_string(), remaining);
|
||||
} else {
|
||||
incorrect += 1;
|
||||
data.insert("incorrect".to_string(), incorrect);
|
||||
}
|
||||
} else if dict[index].word == *answer {
|
||||
correct += 1;
|
||||
remaining -= 1;
|
||||
|
||||
data.insert("correct".to_string(), correct);
|
||||
data.insert("remaining".to_string(), remaining);
|
||||
} else {
|
||||
incorrect += 1;
|
||||
data.insert("incorrect".to_string(), incorrect);
|
||||
}
|
||||
|
||||
if remaining == 0 {
|
||||
use yansi::Paint;
|
||||
|
||||
coreutils::clear(term, vec![]);
|
||||
term.writeln(&format!("{}", "challenge complete!".green().bright()));
|
||||
draw_score(term, correct, incorrect, remaining);
|
||||
term.writeln("\npress return to claim your reward!");
|
||||
return;
|
||||
}
|
||||
|
||||
data.insert("index".to_string(), rand::random::<usize>() % dict.len());
|
||||
|
||||
coreutils::clear(term, vec![]);
|
||||
draw_score(term, correct, incorrect, remaining);
|
||||
draw_question(term, data);
|
||||
term.write("> ");
|
||||
}
|
||||
|
||||
fn draw_question(term: &xterm::Terminal, data: &mut HashMap<String, usize>) {
|
||||
if data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dict = DICT.get_or_init(|| serde_json::from_str(DICTIONARY).expect("failed to parse dictionary"));
|
||||
let index = data
|
||||
.get("index")
|
||||
.copied()
|
||||
.unwrap_or(rand::random::<usize>() % dict.len());
|
||||
data.insert("index".to_string(), index);
|
||||
|
||||
let correct = data.get("correct").copied().unwrap_or(0);
|
||||
|
||||
use yansi::Paint;
|
||||
|
||||
if correct < 10 {
|
||||
term.writeln(&format!(
|
||||
"what is the english translation of the esperanto word '{}'?",
|
||||
dict[index].word.yellow()
|
||||
));
|
||||
} else {
|
||||
term.writeln(&format!(
|
||||
"what is the esperanto translation of the english word '{}'?",
|
||||
dict[index].translation.yellow()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_score(term: &xterm::Terminal, correct: usize, incorrect: usize, remaining: usize) {
|
||||
use yansi::Paint;
|
||||
|
||||
let score = format!(
|
||||
"correct: {}; incorrect: {}; remaining: {}",
|
||||
correct.green(),
|
||||
incorrect.red(),
|
||||
remaining.cyan()
|
||||
);
|
||||
term.writeln(&score);
|
||||
}
|
||||
235
shell/src/core/coreutils.rs
Normal file
235
shell/src/core/coreutils.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
use super::vfs::{Directory, RawVfsEntry, VfsDir, VfsEntry, VirtualFileSystem};
|
||||
use crate::{core::vfs::VfsFile, ffi::xterm};
|
||||
|
||||
pub fn ls(vfs: &mut VirtualFileSystem, term: &xterm::Terminal, args: Vec<&str>) {
|
||||
use yansi::Paint;
|
||||
|
||||
let path = args.first().unwrap_or(&"").to_string();
|
||||
let dir = parse_path(vfs, &path, false).unwrap_or(vfs.current.clone());
|
||||
|
||||
let children = &dir.borrow().children;
|
||||
let mut files = children.iter().collect::<Vec<_>>();
|
||||
files.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
|
||||
|
||||
let files = files
|
||||
.into_iter()
|
||||
.map(|(name, entry)| match entry {
|
||||
VfsEntry::Dir(_) => format!("{}", name.cyan().bright()),
|
||||
VfsEntry::File(file) => match file {
|
||||
VfsFile::VfsCallable(_) => format!("{}", name.green().bright()),
|
||||
VfsFile::Callable(_) => format!("{}", name.green().bright()),
|
||||
VfsFile::InputCallable(_) => format!("{}", name.green().bright()),
|
||||
// VfsFile::Data(_) => format!("{}", Red.paint(name)),
|
||||
_ => name.to_string(),
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
term.writeln(&files.join("\n"));
|
||||
}
|
||||
|
||||
pub fn cd(vfs: &mut VirtualFileSystem, term: &xterm::Terminal, args: Vec<&str>) {
|
||||
let path = args.first().unwrap_or(&"").to_string();
|
||||
|
||||
if path.is_empty() {
|
||||
vfs.current = vfs.home.clone();
|
||||
return;
|
||||
} else if path == "/" {
|
||||
vfs.current = vfs.root.clone();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(dir) = parse_path(vfs, &path, false) {
|
||||
vfs.current = dir;
|
||||
} else {
|
||||
term.writeln(&format!("cd: {}: no such directory", path));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(term: &xterm::Terminal, _args: Vec<&str>) {
|
||||
term.write("\u{1b}[2J\u{1b}[H");
|
||||
}
|
||||
|
||||
pub fn pwd(vfs: &mut VirtualFileSystem, term: &xterm::Terminal, _args: Vec<&str>) {
|
||||
term.writeln(&vfs.cwd());
|
||||
}
|
||||
|
||||
pub fn whoami(term: &xterm::Terminal, _args: Vec<&str>) {
|
||||
term.writeln("kpl");
|
||||
}
|
||||
|
||||
pub fn arch(term: &xterm::Terminal, _args: Vec<&str>) {
|
||||
term.writeln("wasm32");
|
||||
}
|
||||
|
||||
pub fn uname(term: &xterm::Terminal, _args: Vec<&str>) {
|
||||
term.writeln("jinux 0.1.0");
|
||||
}
|
||||
|
||||
pub fn mkdir(vfs: &mut VirtualFileSystem, term: &xterm::Terminal, args: Vec<&str>) {
|
||||
let path = args.first().unwrap_or(&"").to_string();
|
||||
if path.is_empty() {
|
||||
term.writeln("usage: mkdir directory_name");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(dir) = parse_path(vfs, &path, true) {
|
||||
let name = parse_name(&path);
|
||||
|
||||
vfs.new_entry(
|
||||
&dir,
|
||||
&name,
|
||||
RawVfsEntry::Dir(Directory {
|
||||
children: Default::default(),
|
||||
name: name.clone(),
|
||||
parent: Some(dir.clone()),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
term.writeln(&format!("touch: {}: no such entry", path));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rm(vfs: &mut VirtualFileSystem, term: &xterm::Terminal, args: Vec<&str>) {
|
||||
let path = args.first().unwrap_or(&"").to_string();
|
||||
if path.is_empty() {
|
||||
term.writeln("usage: rm name");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(dir) = parse_path(vfs, &path, true) {
|
||||
let name = parse_name(&path);
|
||||
dir.borrow_mut().children.remove(&name);
|
||||
} else {
|
||||
term.writeln(&format!("rm: {}: no such file or directory", path));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn touch(vfs: &mut VirtualFileSystem, term: &xterm::Terminal, args: Vec<&str>) {
|
||||
let path = args.first().unwrap_or(&"").to_string();
|
||||
if path.is_empty() {
|
||||
term.writeln("usage: touch file_name");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(dir) = parse_path(vfs, &path, true) {
|
||||
let name = parse_name(&path);
|
||||
vfs.new_entry(&dir, &name, RawVfsEntry::File(VfsFile::Data("".to_string())));
|
||||
} else {
|
||||
term.writeln(&format!("touch: {}: no such entry", path));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cat(vfs: &mut VirtualFileSystem, term: &xterm::Terminal, args: Vec<&str>) {
|
||||
let path = args.first().unwrap_or(&"").to_string();
|
||||
if path.is_empty() {
|
||||
term.writeln("usage: cat file_name");
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = parse_path(vfs, &path, true).unwrap_or(vfs.current.clone());
|
||||
|
||||
let name = parse_name(&path);
|
||||
let binding = dir.borrow();
|
||||
let entry = binding.children.get(&name);
|
||||
|
||||
if let Some(VfsEntry::File(file)) = entry {
|
||||
if let VfsFile::Data(data) = file {
|
||||
term.writeln(data);
|
||||
}
|
||||
} else {
|
||||
term.writeln(&format!("cat: {}: no such file", path));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help(vfs: &mut VirtualFileSystem, term: &xterm::Terminal, _args: Vec<&str>) {
|
||||
let root = vfs.root.borrow();
|
||||
let bin = root.children.get("bin");
|
||||
|
||||
if let Some(VfsEntry::Dir(bin)) = bin {
|
||||
let commands = bin
|
||||
.borrow()
|
||||
.children
|
||||
.keys()
|
||||
.map(|name| name.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
term.writeln(&format!("commands in path: {}", commands.join(", ")));
|
||||
} else {
|
||||
term.writeln("no commands found");
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_path(vfs: &VirtualFileSystem, path: &str, parent: bool) -> Option<VfsDir> {
|
||||
if path == "/" {
|
||||
return Some(vfs.root.clone());
|
||||
}
|
||||
|
||||
let mut path = path.to_string();
|
||||
let mut dir = if path.starts_with('/') {
|
||||
path = path[1..].to_string();
|
||||
vfs.root.clone()
|
||||
} else {
|
||||
vfs.current.clone()
|
||||
};
|
||||
let path = path.trim().split('/').collect::<Vec<_>>();
|
||||
|
||||
for name in path {
|
||||
if name == ".." {
|
||||
let parent = dir.borrow().parent.clone();
|
||||
|
||||
if let Some(parent) = parent {
|
||||
dir = parent;
|
||||
}
|
||||
} else if name == "." {
|
||||
continue;
|
||||
} else {
|
||||
let binding = dir.borrow().clone();
|
||||
let child = binding.children.get(name);
|
||||
if let Some(VfsEntry::Dir(child)) = child {
|
||||
dir = child.clone();
|
||||
} else if parent {
|
||||
return Some(dir);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if parent {
|
||||
Some(dir.borrow().parent.clone().unwrap_or(vfs.root.clone()))
|
||||
} else {
|
||||
Some(dir)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_name(path: &str) -> String {
|
||||
let mut path = path.to_string();
|
||||
if path.ends_with('/') {
|
||||
path.pop();
|
||||
}
|
||||
|
||||
let parts = path.split('/').collect::<Vec<_>>();
|
||||
parts.last().unwrap_or(&"").to_string()
|
||||
}
|
||||
|
||||
pub fn callable(vfs: &VirtualFileSystem, path: &str) -> Option<VfsFile> {
|
||||
let root = vfs.root.borrow();
|
||||
let dir = parse_path(vfs, path, true).unwrap_or(vfs.current.clone());
|
||||
let name = parse_name(path);
|
||||
|
||||
if let Some(entry) = dir.borrow().children.get(&name)
|
||||
&& let VfsEntry::File(file) = entry
|
||||
{
|
||||
return Some(file.clone());
|
||||
}
|
||||
|
||||
let bin = root.children.get("bin");
|
||||
if let Some(VfsEntry::Dir(bin)) = bin
|
||||
&& let Some(VfsEntry::File(file)) = bin.borrow().children.get(&name)
|
||||
{
|
||||
return Some(file.clone());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
5
shell/src/core/mod.rs
Normal file
5
shell/src/core/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod program;
|
||||
pub mod vfs;
|
||||
|
||||
mod challenge;
|
||||
mod coreutils;
|
||||
17
shell/src/core/program.rs
Normal file
17
shell/src/core/program.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use crate::ffi::xterm;
|
||||
|
||||
pub type Program = fn(&xterm::Terminal, &mut ProgramState, char) -> Result<Option<usize>, ()>;
|
||||
|
||||
pub struct ProgramState {
|
||||
pub buffer: String,
|
||||
pub data: Box<dyn std::any::Any>,
|
||||
}
|
||||
|
||||
impl Default for ProgramState {
|
||||
fn default() -> Self {
|
||||
ProgramState {
|
||||
buffer: String::new(),
|
||||
data: Box::new(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
249
shell/src/core/vfs.rs
Normal file
249
shell/src/core/vfs.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use crate::{core::challenge, ffi::xterm};
|
||||
|
||||
use super::{coreutils, program};
|
||||
|
||||
pub const DICTIONARY: &str = include_str!("../../../en_eo_dict_clean.json");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum VfsFile {
|
||||
VfsCallable(fn(&mut VirtualFileSystem, &xterm::Terminal, Vec<&str>)),
|
||||
Callable(fn(&xterm::Terminal, Vec<&str>)),
|
||||
InputCallable(fn(&xterm::Terminal, Vec<&str>) -> program::Program),
|
||||
Data(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VfsFile {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VfsFile::VfsCallable(_) => write!(f, "VfsCallable"),
|
||||
VfsFile::Callable(_) => write!(f, "Callable"),
|
||||
VfsFile::InputCallable(_) => write!(f, "InputCallable"),
|
||||
VfsFile::Data(_) => write!(f, "Data"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Directory {
|
||||
pub name: String,
|
||||
pub children: HashMap<String, VfsEntry>,
|
||||
pub parent: Option<VfsDir>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Directory {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut dbg = f.debug_struct("Directory");
|
||||
|
||||
dbg.field("name", &self.name).field("children", &self.children);
|
||||
|
||||
if let Some(parent) = &self.parent {
|
||||
dbg.field("parent", &parent.borrow().name);
|
||||
} else {
|
||||
dbg.field("parent", &"None");
|
||||
}
|
||||
|
||||
dbg.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub type VfsDir = Rc<RefCell<Directory>>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum VfsEntry {
|
||||
File(VfsFile),
|
||||
Dir(VfsDir),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RawVfsEntry {
|
||||
File(VfsFile),
|
||||
Dir(Directory),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VirtualFileSystem {
|
||||
pub root: VfsDir,
|
||||
pub home: VfsDir,
|
||||
pub current: VfsDir,
|
||||
}
|
||||
|
||||
impl VirtualFileSystem {
|
||||
pub fn new() -> VirtualFileSystem {
|
||||
let (root, home) = build_root_fs();
|
||||
|
||||
VirtualFileSystem {
|
||||
current: home.clone(),
|
||||
home,
|
||||
root,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cwd(&self) -> String {
|
||||
crate::ffi::console::log(&format!("vfs: {:#?}", self));
|
||||
let mut path = Vec::new();
|
||||
let mut current = self.current.clone();
|
||||
crate::ffi::console::log(&format!("current: {:#?}", current));
|
||||
|
||||
loop {
|
||||
let name = current.borrow().name.clone();
|
||||
path.push(name.clone());
|
||||
|
||||
crate::ffi::console::log(&format!("name: {:?}; path: {:?}", name, path));
|
||||
let parent = current.borrow().parent.clone();
|
||||
|
||||
if let Some(parent) = parent {
|
||||
current = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
path.reverse();
|
||||
|
||||
let cwd = path.join("/")[1..].to_string();
|
||||
if cwd.is_empty() {
|
||||
return "/".to_string();
|
||||
}
|
||||
|
||||
cwd
|
||||
}
|
||||
|
||||
pub fn call(&mut self, term: &xterm::Terminal, path: &str, args: Vec<&str>) -> Result<Option<program::Program>, ()> {
|
||||
let callable = coreutils::callable(self, path);
|
||||
|
||||
if let Some(VfsFile::VfsCallable(callable)) = callable {
|
||||
callable(self, term, args);
|
||||
Ok(None)
|
||||
} else if let Some(VfsFile::Callable(callable)) = callable {
|
||||
callable(term, args);
|
||||
Ok(None)
|
||||
} else if let Some(VfsFile::InputCallable(callable)) = callable {
|
||||
Ok(Some(callable(term, args)))
|
||||
} else if let Some(VfsFile::Data(data)) = callable {
|
||||
term.writeln(&data);
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_entry(&mut self, parent: &VfsDir, name: &str, entry: RawVfsEntry) {
|
||||
match entry {
|
||||
RawVfsEntry::File(file) => add_child(parent, name, VfsEntry::File(file)),
|
||||
RawVfsEntry::Dir(dir) => add_child(parent, name, VfsEntry::Dir(Rc::new(RefCell::new(dir)))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_child(parent: &VfsDir, name: &str, child: VfsEntry) {
|
||||
parent.borrow_mut().children.insert(name.to_string(), child);
|
||||
}
|
||||
|
||||
fn build_root_fs() -> (VfsDir, VfsDir) {
|
||||
let root = Rc::new(RefCell::new(Directory {
|
||||
name: "/".to_string(),
|
||||
children: Default::default(),
|
||||
parent: None,
|
||||
}));
|
||||
|
||||
add_child(
|
||||
&root,
|
||||
"tmp",
|
||||
VfsEntry::Dir(Rc::new(RefCell::new(Directory {
|
||||
name: "tmp".to_string(),
|
||||
children: HashMap::from_iter(vec![(
|
||||
"some-trash".to_string(),
|
||||
VfsEntry::File(VfsFile::Data("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string())),
|
||||
)]),
|
||||
parent: Some(root.clone()),
|
||||
}))),
|
||||
);
|
||||
|
||||
add_child(
|
||||
&root,
|
||||
"bin",
|
||||
VfsEntry::Dir(Rc::new(RefCell::new(Directory {
|
||||
name: "bin".to_string(),
|
||||
children: build_bin(),
|
||||
parent: Some(root.clone()),
|
||||
}))),
|
||||
);
|
||||
|
||||
let home = Rc::new(RefCell::new(Directory {
|
||||
name: "home".to_string(),
|
||||
children: Default::default(),
|
||||
parent: Some(root.clone()),
|
||||
}));
|
||||
|
||||
use yansi::Paint;
|
||||
let kpl = Rc::new(RefCell::new(Directory {
|
||||
name: "kpl".to_string(),
|
||||
children: HashMap::from_iter(vec![(
|
||||
"README.txt".to_string(),
|
||||
VfsEntry::File(VfsFile::Data(format!(
|
||||
"🎉 Happy Birthday, Kirsten! 🎉\n\nThis shell is written in Rust!\n\nTo claim your present, beat {}\n\nHint: it's not in path (and sorry no tab completions)",
|
||||
"bday".green().bright()
|
||||
))),
|
||||
)]),
|
||||
parent: Some(home.clone()),
|
||||
}));
|
||||
|
||||
add_child(&home, "kpl", VfsEntry::Dir(kpl.clone()));
|
||||
|
||||
add_child(&root, "home", VfsEntry::Dir(home));
|
||||
|
||||
let etc = Rc::new(RefCell::new(Directory {
|
||||
name: "etc".to_string(),
|
||||
children: HashMap::from_iter(vec![(
|
||||
"en_eo_dict.json".to_string(),
|
||||
VfsEntry::File(VfsFile::Data(DICTIONARY.to_string())),
|
||||
)]),
|
||||
parent: Some(root.clone()),
|
||||
}));
|
||||
|
||||
add_child(&root, "etc", VfsEntry::Dir(etc));
|
||||
|
||||
let challenge = Rc::new(RefCell::new(Directory {
|
||||
name: "challenge".to_string(),
|
||||
children: HashMap::from_iter(vec![(
|
||||
"bday".to_string(),
|
||||
VfsEntry::File(VfsFile::InputCallable(challenge::challenge)),
|
||||
)]),
|
||||
parent: Some(root.clone()),
|
||||
}));
|
||||
|
||||
add_child(&root, "challenge", VfsEntry::Dir(challenge));
|
||||
|
||||
(root, kpl)
|
||||
}
|
||||
|
||||
fn build_bin() -> HashMap<String, VfsEntry> {
|
||||
HashMap::from_iter(vec![
|
||||
("ls".to_string(), VfsEntry::File(VfsFile::VfsCallable(coreutils::ls))),
|
||||
("cd".to_string(), VfsEntry::File(VfsFile::VfsCallable(coreutils::cd))),
|
||||
("clear".to_string(), VfsEntry::File(VfsFile::Callable(coreutils::clear))),
|
||||
("pwd".to_string(), VfsEntry::File(VfsFile::VfsCallable(coreutils::pwd))),
|
||||
(
|
||||
"whoami".to_string(),
|
||||
VfsEntry::File(VfsFile::Callable(coreutils::whoami)),
|
||||
),
|
||||
(
|
||||
"mkdir".to_string(),
|
||||
VfsEntry::File(VfsFile::VfsCallable(coreutils::mkdir)),
|
||||
),
|
||||
("rm".to_string(), VfsEntry::File(VfsFile::VfsCallable(coreutils::rm))),
|
||||
(
|
||||
"touch".to_string(),
|
||||
VfsEntry::File(VfsFile::VfsCallable(coreutils::touch)),
|
||||
),
|
||||
("cat".to_string(), VfsEntry::File(VfsFile::VfsCallable(coreutils::cat))),
|
||||
("arch".to_string(), VfsEntry::File(VfsFile::Callable(coreutils::arch))),
|
||||
("uname".to_string(), VfsEntry::File(VfsFile::Callable(coreutils::uname))),
|
||||
(
|
||||
"help".to_string(),
|
||||
VfsEntry::File(VfsFile::VfsCallable(coreutils::help)),
|
||||
),
|
||||
])
|
||||
}
|
||||
13
shell/src/ffi/console.rs
Normal file
13
shell/src/ffi/console.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
pub fn log(s: &str);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = log)]
|
||||
pub fn log_u32(a: u32);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = log)]
|
||||
pub fn log_many(a: &str, b: &str);
|
||||
}
|
||||
24
shell/src/ffi/input.rs
Normal file
24
shell/src/ffi/input.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen(raw_module = "./src/handler.js")]
|
||||
extern "C" {
|
||||
pub type Keypress;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = "key")]
|
||||
pub fn key(this: &Keypress) -> char;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = "keyCode")]
|
||||
pub fn key_code(this: &Keypress) -> u32;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = "altKey")]
|
||||
pub fn alt_key(this: &Keypress) -> bool;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = "ctrlKey")]
|
||||
pub fn ctrl_key(this: &Keypress) -> bool;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = "metaKey")]
|
||||
pub fn meta_key(this: &Keypress) -> bool;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = "shiftKey")]
|
||||
pub fn shift_key(this: &Keypress) -> bool;
|
||||
}
|
||||
3
shell/src/ffi/mod.rs
Normal file
3
shell/src/ffi/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod console;
|
||||
pub mod input;
|
||||
pub mod xterm;
|
||||
27
shell/src/ffi/xterm.rs
Normal file
27
shell/src/ffi/xterm.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const TERMINAL: &'static str = r#"
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
"#;
|
||||
|
||||
#[wasm_bindgen(module = "@xterm/xterm")]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(typescript_type = "Terminal")]
|
||||
pub type Terminal;
|
||||
|
||||
#[wasm_bindgen(constructor)]
|
||||
fn new() -> Terminal;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn writeln(this: &Terminal, data: &str);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn write(this: &Terminal, data: &str);
|
||||
|
||||
#[wasm_bindgen(method, js_name = write)]
|
||||
pub fn write_char(this: &Terminal, data: char);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn clear(this: &Terminal);
|
||||
}
|
||||
134
shell/src/lib.rs
Normal file
134
shell/src/lib.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
#![feature(let_chains)]
|
||||
|
||||
mod core;
|
||||
mod ffi;
|
||||
mod utils;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Shell {
|
||||
term: ffi::xterm::Terminal,
|
||||
|
||||
buffer: String,
|
||||
history: Vec<String>,
|
||||
|
||||
vfs: core::vfs::VirtualFileSystem,
|
||||
|
||||
program_state: core::program::ProgramState,
|
||||
program: Option<core::program::Program>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Shell {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(term: ffi::xterm::Terminal) -> Shell {
|
||||
let shell = Shell {
|
||||
term,
|
||||
|
||||
buffer: String::new(),
|
||||
history: Vec::new(),
|
||||
|
||||
vfs: core::vfs::VirtualFileSystem::new(),
|
||||
|
||||
program_state: core::program::ProgramState::default(),
|
||||
program: None,
|
||||
};
|
||||
|
||||
shell.prompt();
|
||||
|
||||
shell
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = handleKey)]
|
||||
pub fn handle_key(&mut self, event: ffi::input::Keypress) {
|
||||
let key = event.key();
|
||||
crate::ffi::console::log(&format!("key: {:?}", key));
|
||||
|
||||
if key == '\u{3}' {
|
||||
self.term.write_char('\n');
|
||||
self.buffer.clear();
|
||||
|
||||
self.program = None;
|
||||
self.program_state = core::program::ProgramState::default();
|
||||
|
||||
self.prompt();
|
||||
}
|
||||
|
||||
if let Some(program) = self.program {
|
||||
let result = program(&self.term, &mut self.program_state, key);
|
||||
|
||||
if let Ok(Some(_)) = result {
|
||||
self.program = None;
|
||||
self.program_state = core::program::ProgramState::default();
|
||||
|
||||
self.prompt();
|
||||
} else if result.is_err() {
|
||||
self.program = None;
|
||||
self.program_state = core::program::ProgramState::default();
|
||||
|
||||
self.prompt();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
match key {
|
||||
'\r' => {
|
||||
self.term.write_char('\n');
|
||||
if self.buffer.is_empty() {
|
||||
self.prompt();
|
||||
return;
|
||||
}
|
||||
|
||||
let full_args = self.buffer.split_whitespace().collect::<Vec<_>>();
|
||||
let command = full_args.first().unwrap_or(&"");
|
||||
let args = full_args[1..].to_vec();
|
||||
|
||||
let call = self.vfs.call(&self.term, command, args);
|
||||
if let Ok(Some(channel)) = call {
|
||||
self.program = Some(channel);
|
||||
self.history.push(self.buffer.clone());
|
||||
self.buffer.clear();
|
||||
|
||||
return;
|
||||
} else if call.is_err() {
|
||||
self.term.writeln(&format!("{}: command not found", command));
|
||||
}
|
||||
|
||||
self.history.push(self.buffer.clone());
|
||||
self.buffer.clear();
|
||||
self.prompt();
|
||||
}
|
||||
|
||||
'\u{7f}' => {
|
||||
if self.buffer.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.buffer.pop();
|
||||
self.term.write("\u{8} \u{8}");
|
||||
}
|
||||
_ => {
|
||||
self.buffer.push(key);
|
||||
self.term.write_char(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = prompt)]
|
||||
pub fn prompt(&self) {
|
||||
let mut cwd = self.vfs.cwd();
|
||||
|
||||
if cwd.starts_with("/home/kpl") {
|
||||
cwd = cwd.replace("/home/kpl", "~");
|
||||
}
|
||||
|
||||
use yansi::Paint;
|
||||
self.term.write(&format!("{}:{}$ ", "kpl@jsh".green(), cwd.cyan()));
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
fn start() {
|
||||
utils::set_panic_hook();
|
||||
}
|
||||
4
shell/src/utils.rs
Normal file
4
shell/src/utils.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub fn set_panic_hook() {
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
Reference in New Issue
Block a user