nushell/src/plugins/binaryview.rs

446 lines
13 KiB
Rust
Raw Normal View History

2019-07-05 06:23:28 +02:00
use crossterm::{cursor, terminal, Attribute, RawScreen};
Add support for ~ expansion This ended up being a bit of a yak shave. The basic idea in this commit is to expand `~` in paths, but only in paths. The way this is accomplished is by doing the expansion inside of the code that parses literal syntax for `SyntaxType::Path`. As a quick refresher: every command is entitled to expand its arguments in a custom way. While this could in theory be used for general-purpose macros, today the expansion facility is limited to syntactic hints. For example, the syntax `where cpu > 0` expands under the hood to `where { $it.cpu > 0 }`. This happens because the first argument to `where` is defined as a `SyntaxType::Block`, and the parser coerces binary expressions whose left-hand-side looks like a member into a block when the command is expecting one. This is mildly more magical than what most programming languages would do, but we believe that it makes sense to allow commands to fine-tune the syntax because of the domain nushell is in (command-line shells). The syntactic expansions supported by this facility are relatively limited. For example, we don't allow `$it` to become a bare word, simply because the command asks for a string in the relevant position. That would quickly become more confusing than it's worth. This PR adds a new `SyntaxType` rule: `SyntaxType::Path`. When a command declares a parameter as a `SyntaxType::Path`, string literals and bare words passed as an argument to that parameter are processed using the path expansion rules. Right now, that only means that `~` is expanded into the home directory, but additional rules are possible in the future. By restricting this expansion to a syntactic expansion when passed as an argument to a command expecting a path, we avoid making `~` a generally reserved character. This will also allow us to give good tab completion for paths with `~` characters in them when a command is expecting a path. In order to accomplish the above, this commit changes the parsing functions to take a `Context` instead of just a `CommandRegistry`. From the perspective of macro expansion, you can think of the `CommandRegistry` as a dictionary of in-scope macros, and the `Context` as the compile-time state used in expansion. This could gain additional functionality over time as we find more uses for the expansion system.
2019-08-26 21:21:03 +02:00
use nu::{serve_plugin, CallInfo, Plugin, ShellError, Signature, SpanSource, Tagged, Value};
2019-07-05 09:53:09 +02:00
use pretty_hex::*;
2019-07-04 07:23:05 +02:00
struct BinaryView;
impl BinaryView {
fn new() -> BinaryView {
BinaryView
}
}
impl Plugin for BinaryView {
2019-08-02 21:15:07 +02:00
fn config(&mut self) -> Result<Signature, ShellError> {
Add support for ~ expansion This ended up being a bit of a yak shave. The basic idea in this commit is to expand `~` in paths, but only in paths. The way this is accomplished is by doing the expansion inside of the code that parses literal syntax for `SyntaxType::Path`. As a quick refresher: every command is entitled to expand its arguments in a custom way. While this could in theory be used for general-purpose macros, today the expansion facility is limited to syntactic hints. For example, the syntax `where cpu > 0` expands under the hood to `where { $it.cpu > 0 }`. This happens because the first argument to `where` is defined as a `SyntaxType::Block`, and the parser coerces binary expressions whose left-hand-side looks like a member into a block when the command is expecting one. This is mildly more magical than what most programming languages would do, but we believe that it makes sense to allow commands to fine-tune the syntax because of the domain nushell is in (command-line shells). The syntactic expansions supported by this facility are relatively limited. For example, we don't allow `$it` to become a bare word, simply because the command asks for a string in the relevant position. That would quickly become more confusing than it's worth. This PR adds a new `SyntaxType` rule: `SyntaxType::Path`. When a command declares a parameter as a `SyntaxType::Path`, string literals and bare words passed as an argument to that parameter are processed using the path expansion rules. Right now, that only means that `~` is expanded into the home directory, but additional rules are possible in the future. By restricting this expansion to a syntactic expansion when passed as an argument to a command expecting a path, we avoid making `~` a generally reserved character. This will also allow us to give good tab completion for paths with `~` characters in them when a command is expecting a path. In order to accomplish the above, this commit changes the parsing functions to take a `Context` instead of just a `CommandRegistry`. From the perspective of macro expansion, you can think of the `CommandRegistry` as a dictionary of in-scope macros, and the `Context` as the compile-time state used in expansion. This could gain additional functionality over time as we find more uses for the expansion system.
2019-08-26 21:21:03 +02:00
Ok(Signature::build("binaryview").switch("lores"))
2019-07-04 07:23:05 +02:00
}
2019-08-01 03:58:42 +02:00
fn sink(&mut self, call_info: CallInfo, input: Vec<Tagged<Value>>) {
2019-07-04 07:23:05 +02:00
for v in input {
let value_origin = v.origin();
2019-08-01 03:58:42 +02:00
match v.item {
Value::Binary(b) => {
let source = value_origin.and_then(|x| call_info.source_map.get(&x));
let _ = view_binary(&b, source, call_info.args.has("lores"));
2019-07-04 07:23:05 +02:00
}
_ => {}
}
}
}
}
fn view_binary(
b: &[u8],
source: Option<&SpanSource>,
lores_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if b.len() > 3 {
match (b[0], b[1], b[2]) {
(0x4e, 0x45, 0x53) => {
2019-08-23 05:29:08 +02:00
#[cfg(feature = "rawkey")]
{
view_contents_interactive(b, source, lores_mode)?;
return Ok(());
}
#[cfg(not(feature = "rawkey"))]
{
println!("Interactive binary viewing currently requires the 'rawkey' feature");
return Ok(());
}
}
_ => {}
}
}
view_contents(b, source, lores_mode)?;
Ok(())
}
2019-07-14 20:38:03 +02:00
pub struct RenderContext {
pub width: usize,
pub height: usize,
2019-07-14 09:54:30 +02:00
pub frame_buffer: Vec<(u8, u8, u8)>,
pub since_last_button: Vec<usize>,
2019-07-16 05:25:36 +02:00
pub lores_mode: bool,
}
2019-07-14 20:38:03 +02:00
impl RenderContext {
2019-07-16 05:25:36 +02:00
pub fn blank(lores_mode: bool) -> RenderContext {
2019-07-14 20:38:03 +02:00
RenderContext {
width: 0,
height: 0,
frame_buffer: vec![],
since_last_button: vec![0; 8],
2019-07-16 05:25:36 +02:00
lores_mode,
}
}
pub fn clear(&mut self) {
2019-07-14 09:54:30 +02:00
self.frame_buffer = vec![(0, 0, 0); self.width * self.height as usize];
}
2019-07-14 20:38:03 +02:00
fn render_to_screen_lores(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let mut prev_color: Option<(u8, u8, u8)> = None;
let mut prev_count = 1;
let cursor = cursor();
cursor.goto(0, 0)?;
2019-07-14 20:38:03 +02:00
for pixel in &self.frame_buffer {
match prev_color {
Some(c) if c == *pixel => {
prev_count += 1;
}
Some(c) => {
print!(
"{}",
ansi_term::Colour::RGB(c.0, c.1, c.2)
.paint((0..prev_count).map(|_| "").collect::<String>())
);
prev_color = Some(*pixel);
prev_count = 1;
}
_ => {
prev_color = Some(*pixel);
prev_count = 1;
}
2019-07-14 09:54:30 +02:00
}
}
2019-07-14 20:38:03 +02:00
if prev_count > 0 {
if let Some(color) = prev_color {
print!(
"{}",
ansi_term::Colour::RGB(color.0, color.1, color.2)
.paint((0..prev_count).map(|_| "").collect::<String>())
);
}
}
println!("{}", Attribute::Reset);
Ok(())
}
fn render_to_screen_hires(&mut self) -> Result<(), Box<dyn std::error::Error>> {
2019-07-14 09:28:56 +02:00
let mut prev_fg: Option<(u8, u8, u8)> = None;
let mut prev_bg: Option<(u8, u8, u8)> = None;
2019-07-05 06:23:28 +02:00
let mut prev_count = 1;
2019-07-14 09:28:56 +02:00
let mut pos = 0;
let fb_len = self.frame_buffer.len();
2019-07-14 20:38:03 +02:00
let cursor = cursor();
cursor.goto(0, 0)?;
2019-07-14 09:28:56 +02:00
while pos < (fb_len - self.width) {
2019-07-14 09:54:30 +02:00
let top_pixel = self.frame_buffer[pos];
let bottom_pixel = self.frame_buffer[pos + self.width];
2019-07-14 09:28:56 +02:00
match (prev_fg, prev_bg) {
(Some(c), Some(d)) if c == top_pixel && d == bottom_pixel => {
2019-07-05 06:23:28 +02:00
prev_count += 1;
}
2019-07-14 09:28:56 +02:00
(Some(c), Some(d)) => {
print!(
2019-07-05 06:23:28 +02:00
"{}",
ansi_term::Colour::RGB(c.0, c.1, c.2)
2019-07-14 09:28:56 +02:00
.on(ansi_term::Colour::RGB(d.0, d.1, d.2,))
.paint((0..prev_count).map(|_| "").collect::<String>())
2019-07-05 06:23:28 +02:00
);
2019-07-14 09:28:56 +02:00
prev_fg = Some(top_pixel);
prev_bg = Some(bottom_pixel);
2019-07-05 06:23:28 +02:00
prev_count = 1;
}
2019-07-05 06:23:28 +02:00
_ => {
2019-07-14 09:28:56 +02:00
prev_fg = Some(top_pixel);
prev_bg = Some(bottom_pixel);
2019-07-05 06:23:28 +02:00
prev_count = 1;
}
}
2019-07-14 09:28:56 +02:00
pos += 1;
if pos % self.width == 0 {
pos += self.width;
}
2019-07-05 06:23:28 +02:00
}
if prev_count > 0 {
2019-07-14 09:28:56 +02:00
match (prev_fg, prev_bg) {
(Some(c), Some(d)) => {
print!(
"{}",
ansi_term::Colour::RGB(c.0, c.1, c.2)
.on(ansi_term::Colour::RGB(d.0, d.1, d.2,))
.paint((0..prev_count).map(|_| "").collect::<String>())
);
}
_ => {}
}
}
println!("{}", Attribute::Reset);
Ok(())
}
2019-07-14 20:38:03 +02:00
pub fn flush(&mut self) -> Result<(), Box<dyn std::error::Error>> {
2019-07-16 05:25:36 +02:00
if self.lores_mode {
2019-07-14 20:38:03 +02:00
self.render_to_screen_lores()
2019-07-16 05:25:36 +02:00
} else {
self.render_to_screen_hires()
2019-07-14 20:38:03 +02:00
}
}
pub fn update(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let terminal = terminal();
let terminal_size = terminal.terminal_size();
if (self.width != terminal_size.0 as usize) || (self.height != terminal_size.1 as usize) {
let cursor = cursor();
cursor.hide()?;
2019-07-29 09:46:24 +02:00
self.width = terminal_size.0 as usize;
2019-07-16 05:25:36 +02:00
self.height = if self.lores_mode {
2019-07-29 09:46:24 +02:00
terminal_size.1 as usize - 1
2019-07-16 05:25:36 +02:00
} else {
2019-07-29 09:46:24 +02:00
(terminal_size.1 as usize - 1) * 2
2019-07-14 20:38:03 +02:00
};
}
Ok(())
}
}
2019-07-05 09:53:09 +02:00
#[derive(Debug)]
struct RawImageBuffer {
dimensions: (u64, u64),
colortype: image::ColorType,
buffer: Vec<u8>,
}
fn load_from_png_buffer(buffer: &[u8]) -> Option<(RawImageBuffer)> {
use image::ImageDecoder;
let decoder = image::png::PNGDecoder::new(buffer);
if decoder.is_err() {
return None;
}
let decoder = decoder.unwrap();
let dimensions = decoder.dimensions();
let colortype = decoder.colortype();
let buffer = decoder.read_image().unwrap();
Some(RawImageBuffer {
dimensions,
colortype,
buffer,
})
}
fn load_from_jpg_buffer(buffer: &[u8]) -> Option<(RawImageBuffer)> {
use image::ImageDecoder;
let decoder = image::jpeg::JPEGDecoder::new(buffer);
if decoder.is_err() {
return None;
}
let decoder = decoder.unwrap();
let dimensions = decoder.dimensions();
let colortype = decoder.colortype();
let buffer = decoder.read_image().unwrap();
Some(RawImageBuffer {
dimensions,
colortype,
buffer,
})
}
pub fn view_contents(
buffer: &[u8],
_source: Option<&SpanSource>,
lores_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
2019-07-05 09:53:09 +02:00
let mut raw_image_buffer = load_from_png_buffer(buffer);
if raw_image_buffer.is_none() {
raw_image_buffer = load_from_jpg_buffer(buffer);
}
if raw_image_buffer.is_none() {
//Not yet supported
println!("{:?}", buffer.hex_dump());
return Ok(());
}
let raw_image_buffer = raw_image_buffer.unwrap();
2019-07-16 05:25:36 +02:00
let mut render_context: RenderContext = RenderContext::blank(lores_mode);
2019-07-14 20:38:03 +02:00
let _ = render_context.update();
render_context.clear();
2019-07-05 09:53:09 +02:00
match raw_image_buffer.colortype {
image::ColorType::RGBA(8) => {
let img = image::ImageBuffer::<image::Rgba<u8>, Vec<u8>>::from_vec(
raw_image_buffer.dimensions.0 as u32,
raw_image_buffer.dimensions.1 as u32,
raw_image_buffer.buffer,
)
.unwrap();
let resized_img = image::imageops::resize(
&img,
2019-07-14 20:38:03 +02:00
render_context.width as u32,
render_context.height as u32,
2019-07-05 09:53:09 +02:00
image::FilterType::Lanczos3,
);
let mut count = 0;
for pixel in resized_img.pixels() {
use image::Pixel;
let rgb = pixel.to_rgb();
2019-07-14 20:38:03 +02:00
render_context.frame_buffer[count] = (rgb[0], rgb[1], rgb[2]);
2019-07-05 09:53:09 +02:00
count += 1;
}
}
image::ColorType::RGB(8) => {
let img = image::ImageBuffer::<image::Rgb<u8>, Vec<u8>>::from_vec(
raw_image_buffer.dimensions.0 as u32,
raw_image_buffer.dimensions.1 as u32,
raw_image_buffer.buffer,
)
.unwrap();
let resized_img = image::imageops::resize(
&img,
2019-07-14 20:38:03 +02:00
render_context.width as u32,
render_context.height as u32,
2019-07-05 09:53:09 +02:00
image::FilterType::Lanczos3,
);
let mut count = 0;
for pixel in resized_img.pixels() {
use image::Pixel;
let rgb = pixel.to_rgb();
2019-07-14 20:38:03 +02:00
render_context.frame_buffer[count] = (rgb[0], rgb[1], rgb[2]);
2019-07-05 09:53:09 +02:00
count += 1;
}
}
_ => {
//Not yet supported
println!("{:?}", buffer.hex_dump());
return Ok(());
}
}
2019-07-14 20:38:03 +02:00
render_context.flush()?;
2019-07-05 09:53:09 +02:00
let cursor = cursor();
let _ = cursor.show();
#[allow(unused)]
let screen = RawScreen::disable_raw_mode();
Ok(())
}
2019-08-23 05:29:08 +02:00
#[cfg(feature = "rawkey")]
2019-07-14 20:38:03 +02:00
pub fn view_contents_interactive(
buffer: &[u8],
source: Option<&SpanSource>,
2019-07-16 05:25:36 +02:00
lores_mode: bool,
2019-07-14 20:38:03 +02:00
) -> Result<(), Box<dyn std::error::Error>> {
use rawkey::{KeyCode, RawKey};
let sav_path = if let Some(SpanSource::File(f)) = source {
let mut path = std::path::PathBuf::from(f);
path.set_extension("sav");
Some(path)
} else {
None
};
2019-07-27 22:09:25 +02:00
let mut nes = neso::Nes::new(0.0);
let rawkey = RawKey::new();
nes.load_rom(&buffer);
if let Some(ref sav_path) = sav_path {
if let Ok(contents) = std::fs::read(sav_path) {
let _ = nes.load_state(&contents);
}
}
nes.reset();
if let Ok(_raw) = RawScreen::into_raw_mode() {
2019-07-16 05:25:36 +02:00
let mut render_context: RenderContext = RenderContext::blank(lores_mode);
let input = crossterm::input();
let _ = input.read_async();
let cursor = cursor();
let buttons = vec![
KeyCode::Alt,
KeyCode::LeftControl,
KeyCode::Tab,
KeyCode::BackSpace,
KeyCode::UpArrow,
KeyCode::DownArrow,
KeyCode::LeftArrow,
KeyCode::RightArrow,
];
cursor.hide()?;
'gameloop: loop {
2019-07-14 20:38:03 +02:00
let _ = render_context.update();
nes.step_frame();
let image_buffer = nes.image_buffer();
let slice = unsafe { std::slice::from_raw_parts(image_buffer, 256 * 240 * 4) };
2019-07-05 09:53:09 +02:00
let img =
image::ImageBuffer::<image::Rgba<u8>, &[u8]>::from_raw(256, 240, slice).unwrap();
let resized_img = image::imageops::resize(
&img,
2019-07-14 20:38:03 +02:00
render_context.width as u32,
render_context.height as u32,
2019-07-05 09:53:09 +02:00
image::FilterType::Lanczos3,
);
2019-07-14 20:38:03 +02:00
render_context.clear();
2019-07-05 09:53:09 +02:00
let mut count = 0;
for pixel in resized_img.pixels() {
use image::Pixel;
let rgb = pixel.to_rgb();
2019-07-14 20:38:03 +02:00
render_context.frame_buffer[count] = (rgb[0], rgb[1], rgb[2]);
2019-07-05 09:53:09 +02:00
count += 1;
}
2019-07-14 20:38:03 +02:00
render_context.flush()?;
if rawkey.is_pressed(rawkey::KeyCode::Escape) {
break 'gameloop;
} else {
for i in 0..buttons.len() {
if rawkey.is_pressed(buttons[i]) {
nes.press_button(0, i as u8);
} else {
nes.release_button(0, i as u8);
}
}
}
}
}
if let Some(ref sav_path) = sav_path {
let buffer = nes.save_state();
if let Ok(buffer) = buffer {
let _ = std::fs::write(sav_path, buffer);
}
}
let cursor = cursor();
let _ = cursor.show();
#[allow(unused)]
let screen = RawScreen::disable_raw_mode();
Ok(())
2019-07-04 07:23:05 +02:00
}
fn main() {
serve_plugin(&mut BinaryView::new());
}