Skip to content

Commit

Permalink
- added cmd for keybinds on mac
Browse files Browse the repository at this point in the history
- added touch interaction for web
- added snbt as file type
- added find command to cli
- added help gui to cli
- added reformat command to cli
  • Loading branch information
RealRTTV committed Mar 17, 2024
1 parent af9c38c commit c271ff8
Show file tree
Hide file tree
Showing 15 changed files with 449 additions and 75 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nbtworkbench"
version = "1.2.1"
version = "1.2.2"
edition = "2021"
description = "A modern NBT Editor written in Rust designed for performance and efficiency."
license-file = "LICENSE"
Expand Down Expand Up @@ -62,6 +62,8 @@ wgsl-inline = { version = "0.2.0", features = ["minify"] }
static_assertions = "1.1.0"
anyhow = "1.0.79"
lz4_flex = { version = "0.11.2", default-features = false, features = ["std", "nightly"] }
regex = "1.10.3"
glob = "0.3.1"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
cli-clipboard = "0.4.0"
Expand Down
270 changes: 270 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
use std::fmt::Formatter;
use std::fs::{File, OpenOptions, read};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

use glob::glob;
use regex::{Regex, RegexBuilder};

use crate::{error, log, SortAlgorithm, WindowProperties};
use crate::elements::element::NbtElement;
use crate::search_box::{SearchBox, SearchPredicate};
use crate::tab::FileFormat;
use crate::workbench::Workbench;

struct SearchResult {
path: PathBuf,
lines: Vec<usize>,
}

impl std::fmt::Display for SearchResult {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Found {n} matches in file {path:?} at line numbers:", n = self.lines.len(), path = self.path)?;
for &line in &self.lines {
writeln!(f, "{line}")?;
}
Ok(())
}
}

fn create_regex(mut str: String) -> Option<Regex> {
if !str.starts_with("/") {
return None
}

str = str.split_off(1);

let mut flags = 0_u8;
while let Some(char) = str.pop() {
match char {
'i' => flags |= 0b000001,
'g' => flags |= 0b000010,
'm' => flags |= 0b000100,
's' => flags |= 0b001000,
'u' => flags |= 0b010000,
'y' => flags |= 0b100000,
'/' => break,
_ => return None
}
}

RegexBuilder::new(&str)
.case_insensitive(flags & 0b1 > 0)
.multi_line(flags & 0b100 > 0)
.dot_matches_new_line(flags & 0b1000 > 0)
.unicode(flags & 0b10000 > 0)
.swap_greed(flags & 0b10000 > 0)
.build().ok()
}

fn get_paths(args: &mut Vec<String>) -> Vec<PathBuf> {
if args.is_empty() {
error!("Could not find path argument");
std::process::exit(1);
}
let path = args.remove(0);
match glob(&path) {
Ok(paths) => paths.filter_map(|result| result.ok()).collect::<Vec<_>>(),
Err(e) => {
error!("Glob error: {e}");
std::process::exit(1);
}
}
}

fn get_predicate(mut args: Vec<String>) -> SearchPredicate {
let snbt = {
if let Some("-s" | "--snbt") = args.get(0).map(String::as_str) {
args.remove(0);
true
} else if let Some("-s" | "--snbt") = args.get(1).map(String::as_str) {
args.remove(1);
true
} else {
false
}
};

let predicate = args.as_slice().join(" ");
if predicate.is_empty() {
error!("Predicate cannot be empty");
std::process::exit(1);
}
if snbt && let Some((key, snbt)) = NbtElement::from_str(&predicate, SortAlgorithm::None) {
SearchPredicate::Snbt(key.map(|x| x.into_string()), snbt)
} else if let Some(regex) = create_regex(predicate.clone()) {
SearchPredicate::Regex(regex)
} else {
SearchPredicate::String(predicate)
}
}

fn file_size(path: impl AsRef<Path>) -> Option<u64> {
File::open(path).ok().and_then(|file| file.metadata().ok()).map(|metadata| metadata.len() as u64)
}

fn increment_progress_bar(completed: &AtomicU64, size: u64, total: u64) {
let finished = completed.fetch_add(size, Ordering::Relaxed);
print!("\rSearching... ({n} / {total} bytes) ({p:.1}% complete)", n = finished, p = 100.0 * finished as f64 / total as f64);
let _ = std::io::Write::flush(&mut std::io::stdout());
}

#[inline]
#[cfg(not(target_arch = "wasm32"))]
pub fn find() -> ! {
let mut args = std::env::args().collect::<Vec<_>>();
// one for the exe, one for the `find`
args.drain(..2).for_each(|_| ());

let paths = get_paths(&mut args);
let predicate = get_predicate(args);

let completed = AtomicU64::new(0);
let total_size = paths.iter().filter_map(file_size).sum::<u64>();

print!("Searching... (0 / {total_size} bytes) (0.0% complete)");
let _ = std::io::Write::flush(&mut std::io::stdout());
let results = std::thread::scope(|s| {
let mut results = Vec::new();
for path in paths {
results.push(s.spawn(|| 'a: {
let mut workbench = Workbench::new(&mut WindowProperties::Fake);
workbench.tabs.clear();

let bytes = match read(&path) {
Ok(bytes) => bytes,
Err(e) => {
error!("File read error: {e}");
increment_progress_bar(&completed, file_size(&path).unwrap_or(0), total_size);
break 'a None
}
};

let len = bytes.len() as u64;

if let Err(e) = workbench.on_open_file(&path, bytes, &mut WindowProperties::Fake) {
error!("File parse error: {e}");
increment_progress_bar(&completed, len, total_size);
break 'a None
}

let tab = workbench.tabs.remove(0);
let bookmarks = SearchBox::search0(&tab.value, &predicate);
std::thread::spawn(move || drop(tab));
increment_progress_bar(&completed, len, total_size);
if !bookmarks.is_empty() {
Some(SearchResult {
path,
lines: bookmarks.into_iter().map(|x| x.true_line_number).collect(),
})
} else {
None
}
}));
}

results.into_iter().filter_map(|x| x.join().ok()).filter_map(std::convert::identity).collect::<Vec<_>>()
});

log!("\rSearching... ({total_size} / {total_size} bytes) (100.0% complete)");

if results.is_empty() {
log!("No results found.")
}

for result in results {
log!("{result}")
}

std::process::exit(0);
}

#[inline]
#[cfg(not(target_arch = "wasm32"))]
pub fn reformat() -> ! {
let mut args = std::env::args().collect::<Vec<_>>();
args.drain(..2);

let remap_extension = {
if let Some("--remap-extension" | "-re") = args.get(0).map(String::as_str) {
args.remove(0);
true
} else {
false
}
};

let paths = get_paths(&mut args);

let (extension, format) = {
match args.get(0).map(String::as_str) {
Some(x @ "nbt") => (x, FileFormat::Nbt),
Some(x @ ("dat" | "dat_old" | "gzip")) => (if x == "gzip" { "dat" } else { x }, FileFormat::Gzip),
Some(x @ "zlib") => (x, FileFormat::Zlib),
Some(x @ "snbt") => (x, FileFormat::Snbt),
Some(format) => {
error!("Unknown format '{format}'");
std::process::exit(1);
}
None => {
error!("No format supplied");
std::process::exit(1);
}
}
};

let completed = AtomicU64::new(0);
let total_size = paths.iter().filter_map(file_size).sum::<u64>();

print!("Reformatting... (0 / {total_size} bytes) (0.0% complete)");
let _ = std::io::Write::flush(&mut std::io::stdout());
std::thread::scope(|s| {
for path in paths {
s.spawn(|| 'a: {
let mut workbench = Workbench::new(&mut WindowProperties::Fake);
workbench.tabs.clear();

let bytes = match read(&path) {
Ok(bytes) => bytes,
Err(e) => {
error!("File read error: {e}");
increment_progress_bar(&completed, file_size(&path).unwrap_or(0), total_size);
break 'a
}
};

let len = bytes.len() as u64;

if let Err(e) = workbench.on_open_file(&path, bytes, &mut WindowProperties::Fake) {
error!("File parse error: {e}");
increment_progress_bar(&completed, len, total_size);
break 'a
}

let mut tab = workbench.tabs.remove(0);
if let FileFormat::Nbt | FileFormat::Snbt | FileFormat::Gzip | FileFormat::Zlib = tab.compression {} else {
error!("Tab had invalid file format {}", tab.compression.to_string());
}

let out = format.encode(&tab.value);
std::thread::spawn(move || drop(tab));

let path = if remap_extension {
path.with_extension(extension)
} else {
path
};

if let Err(e) = std::fs::write(path, out) {
error!("File write error: {e}")
}

increment_progress_bar(&completed, len, total_size);
});
}
});

log!("\rReformatting... ({total_size} / {total_size} bytes) (100.0% complete)");

std::process::exit(0);
}
1 change: 1 addition & 0 deletions src/elements/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ macro_rules! array {
($element_field:ident, $name:ident, $t:ty, $my_id:literal, $id:literal, $char:literal, $uv:ident, $element_uv:ident) => {
#[derive(Default)]
#[repr(C)]
#[derive(PartialEq)]
pub struct $name {
values: Box<Vec<NbtElement>>,
max_depth: u32,
Expand Down
2 changes: 2 additions & 0 deletions src/elements/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{DropFn, RenderContext, SortAlgorithm, StrExt};
use crate::color::TextColor;

#[repr(C)]
#[derive(PartialEq)]
pub struct NbtRegion {
pub chunks: Box<(Vec<u16>, [NbtElement; 32 * 32])>,
height: u32,
Expand Down Expand Up @@ -704,6 +705,7 @@ impl Debug for NbtRegion {

#[repr(C)]
#[allow(clippy::module_name_repetitions)]
#[derive(PartialEq)]
pub struct NbtChunk {
inner: Box<NbtCompound>,
last_modified: u32,
Expand Down
19 changes: 19 additions & 0 deletions src/elements/compound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::color::TextColor;

#[allow(clippy::module_name_repetitions)]
#[repr(C)]
#[derive(PartialEq)]
pub struct NbtCompound {
pub entries: Box<CompoundMap>,
height: u32,
Expand Down Expand Up @@ -691,6 +692,24 @@ pub struct CompoundMap {
pub entries: Vec<Entry>,
}

impl PartialEq for CompoundMap {
fn eq(&self, other: &Self) -> bool {
if self.entries.len() != other.entries.len() { return false }

for entry in &self.entries {
if let Some(idx) = other.idx_of(&entry.key) {
if other.get_idx(idx) != Some((&entry.key, &entry.value)) {
return false
}
} else {
return false
}
}

true
}
}

impl Clone for CompoundMap {
#[allow(clippy::cast_ptr_alignment)]
fn clone(&self) -> Self {
Expand Down
28 changes: 27 additions & 1 deletion src/elements/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,32 @@ pub union NbtElement {
id: NbtElementDiscriminant,
}

impl PartialEq for NbtElement {
fn eq(&self, other: &Self) -> bool {
if self.id() != other.id() { return false }

unsafe {
match self.id() {
NbtChunk::ID => self.chunk == other.chunk,
NbtRegion::ID => self.region == other.region,
NbtByte::ID => self.byte == other.byte,
NbtShort::ID => self.short == other.short,
NbtInt::ID => self.int == other.int,
NbtLong::ID => self.long == other.long,
NbtFloat::ID => self.float == other.float,
NbtDouble::ID => self.double == other.double,
NbtByteArray::ID => self.byte_array == other.byte_array,
NbtString::ID => self.string == other.string,
NbtList::ID => self.list == other.list,
NbtCompound::ID => self.compound == other.compound,
NbtIntArray::ID => self.int_array == other.int_array,
NbtLongArray::ID => self.long_array == other.long_array,
_ => core::hint::unreachable_unchecked(),
}
}
}
}

impl Clone for NbtElement {
#[inline(never)]
fn clone(&self) -> Self {
Expand Down Expand Up @@ -264,7 +290,7 @@ impl NbtElement {
if s.is_empty() { return None }

let prefix = s.snbt_string_read().and_then(|(prefix, s2)| {
s2.trim_start().strip_prefix(':').map(|s2| {
s2.trim_start().strip_prefix(':').filter(|s| !s.is_empty()).map(|s2| {
s = s2.trim_start();
prefix
})
Expand Down
1 change: 1 addition & 0 deletions src/elements/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::color::TextColor;

#[allow(clippy::module_name_repetitions)]
#[repr(C)]
#[derive(PartialEq)]
pub struct NbtList {
pub elements: Box<Vec<NbtElement>>,
height: u32,
Expand Down
Loading

0 comments on commit c271ff8

Please sign in to comment.