refactor: A new beginning
This commit is contained in:
parent
fc43a7454a
commit
6ce3e627cc
33 changed files with 21 additions and 7657 deletions
|
@ -40,7 +40,7 @@ jobs:
|
|||
sh get-docker.sh
|
||||
|
||||
# Start Docker daemon directly (instead of using service)
|
||||
dockerd &
|
||||
sudo dockerd &
|
||||
|
||||
# Give the Docker daemon a moment to initialize
|
||||
sleep 10
|
||||
|
|
64
.vscode/launch.json
vendored
64
.vscode/launch.json
vendored
|
@ -1,64 +0,0 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'zenyx'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=zenyx",
|
||||
"--package=zenyx"
|
||||
],
|
||||
"filter": {
|
||||
"name": "zenyx",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'zenyx'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=zenyx",
|
||||
"--package=zenyx"
|
||||
],
|
||||
"filter": {
|
||||
"name": "zenyx",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'zen_core'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=zen_core"
|
||||
],
|
||||
"filter": {
|
||||
"name": "zen_core",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
3689
Cargo.lock
generated
3689
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
@ -1,11 +1,20 @@
|
|||
[package]
|
||||
name = "zenyx"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["Caznix (Chance) <Caznix01@gmail.com>"]
|
||||
description = "A memory safe, opinionated Game Engine/Framework, written in Rust."
|
||||
keywords = ["engine", "graphics", "game"]
|
||||
categories = ["game-development", "graphics"]
|
||||
license = "MIT"
|
||||
homepage = "https://zenyx-engine.github.io/"
|
||||
repository = "https://codeberg.org/Caznix/Zenyx"
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["engine","subcrates/zen_core"]
|
||||
# members = ["engine","subcrates/zen_core"]
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
parking_lot = "0.12.3"
|
||||
zen_core = { path = "./subcrates/zen_core" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
22
Pumpkin.mtl
22
Pumpkin.mtl
|
@ -1,22 +0,0 @@
|
|||
# Blender 4.2.3 LTS MTL File: 'None'
|
||||
# www.blender.org
|
||||
|
||||
newmtl Material.001
|
||||
Ns 0.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.800000 0.800000 0.800000
|
||||
Ks 0.000000 0.000000 0.000000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.500000
|
||||
d 1.000000
|
||||
illum 1
|
||||
|
||||
newmtl Material.003
|
||||
Ns 0.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.800000 0.800000 0.800000
|
||||
Ks 0.000000 0.000000 0.000000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.500000
|
||||
d 1.000000
|
||||
illum 1
|
BIN
Pumpkin.obj
(Stored with Git LFS)
BIN
Pumpkin.obj
(Stored with Git LFS)
Binary file not shown.
|
@ -1,42 +0,0 @@
|
|||
[package]
|
||||
name = "zenyx"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["Caznix (Chance) <Caznix01@gmail.com>"]
|
||||
description = "A memory safe, opinionated Game Engine/Framework, written in Rust."
|
||||
keywords = ["engine", "graphics", "game"]
|
||||
categories = ["game-development", "graphics"]
|
||||
license = "MIT"
|
||||
homepage = "https://zenyx-engine.github.io/"
|
||||
documentation = "https://zenyx-engine.github.io/docs"
|
||||
repository = "https://codeberg.org/Caznix/Zenyx"
|
||||
|
||||
[dependencies]
|
||||
colored = { version = "3.0.0", default-features = false }
|
||||
parking_lot.workspace = true
|
||||
rustyline = { version = "15.0.0", default-features = false, features = ["custom-bindings", "derive","with-file-history"] }
|
||||
thiserror = { version = "2.0.11", default-features = false }
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["macros", "rt", "rt-multi-thread"] }
|
||||
# Will be updated to 25.x.x when other dependencies are updated to be supported
|
||||
wgpu = { version = "24.0.3", default-features = false }
|
||||
winit = { version = "0.30.9", default-features = false, features = ["rwh_06", "wayland"] }
|
||||
bytemuck = { version = "1.21.0", default-features = false }
|
||||
futures = { version = "0.3.31", default-features = false, features = ["executor"] }
|
||||
cgmath = { version = "0.18.0", default-features = false }
|
||||
tracing = { version = "0.1.41", default-features = false }
|
||||
tracing-subscriber = { version = "0.3.19", default-features = false, features = ["ansi", "fmt"] }
|
||||
tobj = { version = "4.0.3", default-features = false }
|
||||
ahash = { version = "0.8.11", default-features = false, features = ["std"] }
|
||||
wgpu_text = { version = "0.9.2", default-features = false }
|
||||
toml = { version = "0.8.20", default-features = false }
|
||||
serde = { version = "1.0.219", default-features = false, features = ["derive"] }
|
||||
native-dialog = { version = "0.7.0", default-features = false }
|
||||
sysinfo = { version = "0.34.2", default-features = false, features = ["system"] }
|
||||
raw-cpuid = { version = "11.5.0", default-features = false }
|
||||
image = { version = "0.25.6", default-features = false, features = ["png"] }
|
||||
clap = { version = "4.5.35", default-features = false, features = ["std"] }
|
||||
|
||||
[build-dependencies]
|
||||
built = { version = "0.7.7", default-features = false, features = ["cargo-lock", "chrono", "git2"] }
|
||||
build-print = { version = "0.1.1", default-features = false }
|
||||
cargo-lock = { version = "10.1.0", default-features = false }
|
|
@ -1,6 +0,0 @@
|
|||
[build]
|
||||
pre-build = [
|
||||
# "dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||
"apt-get update",
|
||||
"apt-get --assume-yes install libwayland-dev:$CROSS_DEB_ARCH"
|
||||
]
|
112
engine/build.rs
112
engine/build.rs
|
@ -1,112 +0,0 @@
|
|||
use build_print::*;
|
||||
use cargo_lock::Lockfile;
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
fn get_git_info() -> String {
|
||||
match Command::new("git")
|
||||
.arg("describe")
|
||||
.arg("--always")
|
||||
.arg("--dirty")
|
||||
.output()
|
||||
{
|
||||
Ok(output) if output.status.success() => {
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
}
|
||||
_ => {
|
||||
match Command::new("git")
|
||||
.arg("rev-parse")
|
||||
.arg("--abbrev-ref")
|
||||
.arg("HEAD")
|
||||
.output()
|
||||
{
|
||||
Ok(output) if output.status.success() => {
|
||||
let head = String::from_utf8_lossy(&output.stdout);
|
||||
let head = head.trim();
|
||||
if head == "HEAD" {
|
||||
"DETACHED".to_string()
|
||||
} else {
|
||||
//TDDO: properly parse branch hashes
|
||||
format!("BRANCH: {}", head)
|
||||
}
|
||||
}
|
||||
_ => "UNKNOWN".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static ALLOW_DEAD_CODE: &str = "#[allow(dead_code)]\n";
|
||||
fn main() {
|
||||
if let Err(e) = built::write_built_file() {
|
||||
panic!("{e}");
|
||||
}
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join("built.rs");
|
||||
let proj_root = env!("CARGO_MANIFEST_DIR").to_string();
|
||||
let tmp = format!("{}/../Cargo.lock", proj_root);
|
||||
let lockfile_path = Path::new(&tmp).canonicalize().expect("INvalid");
|
||||
|
||||
let cargo_version = match Command::new("cargo").arg("--version").output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let mut version = String::new();
|
||||
version.push_str(ALLOW_DEAD_CODE);
|
||||
version.push_str(&String::from_utf8_lossy(&output.stdout).trim().to_string());
|
||||
version
|
||||
}
|
||||
_ => "unknown".to_string(),
|
||||
};
|
||||
info!("{cargo_version}");
|
||||
|
||||
let rustc_version = match Command::new("rustc").arg("--version").output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let mut version = String::new();
|
||||
version.push_str(ALLOW_DEAD_CODE);
|
||||
version.push_str(&String::from_utf8_lossy(&output.stdout).trim().to_string());
|
||||
version
|
||||
}
|
||||
_ => "unknown".to_string(),
|
||||
};
|
||||
info!("{rustc_version}");
|
||||
|
||||
let git_info = get_git_info();
|
||||
info!("Git Info: {}", git_info);
|
||||
|
||||
let mut built_rs = match OpenOptions::new().append(true).open(&dest_path) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
error!("Could not open built.rs for appending: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match Lockfile::load(lockfile_path) {
|
||||
Ok(lockfile) => {
|
||||
let dependencies_to_track = ["tokio", "winit", "wgpu"];
|
||||
|
||||
for package in lockfile.packages {
|
||||
let name = package.name.as_str();
|
||||
if dependencies_to_track.contains(&name) {
|
||||
let version = package.version.to_string();
|
||||
writeln!(
|
||||
built_rs,
|
||||
"{}pub static {}_VERSION: &str = \"{}\";",
|
||||
ALLOW_DEAD_CODE,
|
||||
name.to_uppercase().replace('-', "_"),
|
||||
version
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error loading Cargo.lock: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
std::println!("cargo:rerun-if-changed=Cargo.lock");
|
||||
std::println!("cargo:rerun-if-changed=.git/HEAD");
|
||||
std::println!("cargo:rerun-if-changed=.git/index");
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
use clap::{Arg, Command};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Cli {}
|
||||
|
||||
pub fn parse() {
|
||||
let matches = Command::new("zenyx")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.author(env!("CARGO_PKG_AUTHORS"))
|
||||
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||
.arg(
|
||||
Arg::new("name")
|
||||
.short('n')
|
||||
.long("name")
|
||||
.value_name("NAME")
|
||||
.help("Sets a custom name"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("greet").about("Greets the given name").arg(
|
||||
Arg::new("person")
|
||||
.value_name("PERSON")
|
||||
.help("The person to greet")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
.get_matches();
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod ecs;
|
||||
pub mod panic;
|
||||
pub mod repl;
|
||||
pub mod splash;
|
||||
|
||||
pub mod render;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct EngineState {
|
||||
log_level: LogLevel,
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
|
||||
enum LogLevel {
|
||||
Info,
|
||||
Debug,
|
||||
Error,
|
||||
Trace,
|
||||
}
|
||||
impl EngineState {}
|
|
@ -1,225 +0,0 @@
|
|||
use std::str::FromStr;
|
||||
use std::{env, error::Error, path::PathBuf, thread};
|
||||
|
||||
use native_dialog::{MessageDialog, MessageType};
|
||||
use parking_lot::Once;
|
||||
use tracing::error;
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
pub fn set_panic_hook() {
|
||||
INIT.call_once(|| {
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
if let Err(e) = process_panic(info) {
|
||||
eprintln!("Error in panic hook: {}", e);
|
||||
default_hook(info);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box<dyn Error>> {
|
||||
use std::io::Write;
|
||||
|
||||
use colored::Colorize;
|
||||
|
||||
let log_dir = PathBuf::from_str("./").expect("wtf, The current directory no longer exists?");
|
||||
if !log_dir.exists() {
|
||||
std::fs::create_dir_all(&log_dir)?;
|
||||
}
|
||||
let log_path = log_dir.join("panic.log");
|
||||
|
||||
let mut file = std::fs::File::create(&log_path)?;
|
||||
let payload = info.payload();
|
||||
let payload_str = if let Some(s) = payload.downcast_ref::<&str>() {
|
||||
*s
|
||||
} else if let Some(s) = payload.downcast_ref::<String>() {
|
||||
s
|
||||
} else {
|
||||
"<non-string panic payload>"
|
||||
};
|
||||
|
||||
writeln!(file, "Panic Occurred: {}", payload_str)?;
|
||||
|
||||
if let Some(location) = info.location() {
|
||||
writeln!(file, "Panic Location: {}", location)?;
|
||||
}
|
||||
|
||||
writeln!(file, "{}", capture_backtrace().sanitize_path())?;
|
||||
|
||||
// Add more contextual information
|
||||
writeln!(file, "\n--- Additional Information ---")?;
|
||||
|
||||
// Rust Version
|
||||
if let Ok(rust_version) = rust_version() {
|
||||
writeln!(file, "Rust Version: {}", rust_version)?;
|
||||
}
|
||||
|
||||
// Command-line Arguments
|
||||
writeln!(file, "Command-line Arguments:")?;
|
||||
for arg in env::args() {
|
||||
writeln!(file, " {}", arg)?;
|
||||
}
|
||||
|
||||
// Environment Variables (consider filtering sensitive ones)
|
||||
writeln!(file, "\nEnvironment Variables (selected):")?;
|
||||
let interesting_env_vars = ["PATH", "RUST_VERSION", "CARGO_TARGET_DIR", "HOME", "USER"];
|
||||
for (key, value) in env::vars() {
|
||||
if interesting_env_vars.contains(&key.as_str()) {
|
||||
writeln!(file, " {}: {}", key, value)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Current Working Directory
|
||||
if let Ok(cwd) = env::current_dir() {
|
||||
writeln!(file, "\nCurrent Working Directory: {}", cwd.display())?;
|
||||
}
|
||||
|
||||
// Thread Information
|
||||
if let Some(thread) = thread::current().name() {
|
||||
writeln!(file, "\nThread Name: {}", thread)?;
|
||||
} else {
|
||||
writeln!(file, "\nThread ID: {:?}", thread::current().id())?;
|
||||
}
|
||||
|
||||
let panic_msg = format!(
|
||||
r#"Zenyx had a problem and crashed. To help us diagnose the problem you can send us a crash report.
|
||||
|
||||
We have generated a detailed report file at '{}'. Submit an issue or email with the subject of 'Zenyx Crash Report' and include the report as an attachment.
|
||||
|
||||
To submit the crash report:
|
||||
https://codeberg.org/Caznix/Zenyx/issues
|
||||
We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports.
|
||||
|
||||
Thank you kindly!"#,
|
||||
log_path.display()
|
||||
);
|
||||
|
||||
let final_msg = format!(
|
||||
r#"{}
|
||||
|
||||
For future reference, the error summary is as follows:
|
||||
{}
|
||||
|
||||
More details can be found in the crash report file."#,
|
||||
panic_msg, payload_str
|
||||
);
|
||||
|
||||
println!("{}", final_msg.red().bold());
|
||||
|
||||
if let Err(e) = MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
.set_title("A fatal error in Zenyx has occurred")
|
||||
.set_text(&final_msg)
|
||||
.show_confirm()
|
||||
{
|
||||
error!("Failed to show message dialog: {e}")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rust_version() -> Result<String, Box<dyn Error>> {
|
||||
let version = env!("CARGO_PKG_RUST_VERSION");
|
||||
Ok(version.to_string())
|
||||
}
|
||||
|
||||
fn capture_backtrace() -> String {
|
||||
let mut backtrace = String::new();
|
||||
let sysinfo = crate::metadata::SystemMetadata::current();
|
||||
backtrace.push_str(&format!(
|
||||
"--- System Information ---\n{}\n",
|
||||
sysinfo.verbose_summary()
|
||||
));
|
||||
|
||||
let trace = std::backtrace::Backtrace::force_capture();
|
||||
let message = format!("\n--- Backtrace ---\n\n");
|
||||
backtrace.push_str(&message);
|
||||
backtrace.push_str(&format!("{trace:#}"));
|
||||
|
||||
backtrace
|
||||
}
|
||||
|
||||
trait Sanitize {
|
||||
fn sanitize_path(&self) -> String;
|
||||
}
|
||||
|
||||
impl Sanitize for str {
|
||||
fn sanitize_path(&self) -> String {
|
||||
let prefixes = ["/home/", "/Users/", "\\Users\\", "/opt/home/"];
|
||||
let mut result = String::from(self);
|
||||
|
||||
for prefix in prefixes {
|
||||
if let Some(start_index) = result.find(prefix) {
|
||||
let start_of_user = start_index + prefix.len();
|
||||
let mut end_of_user = result[start_of_user..]
|
||||
.find(|c| c == '/' || c == '\\')
|
||||
.map(|i| start_of_user + i)
|
||||
.unwrap_or(result.len());
|
||||
if end_of_user == start_of_user && start_of_user < result.len() {
|
||||
end_of_user = result.len();
|
||||
}
|
||||
result.replace_range(start_of_user..end_of_user, "<USER>");
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_home() {
|
||||
assert_eq!(
|
||||
"/home/<USER>/documents",
|
||||
"/home/john.doe/documents".sanitize_path()
|
||||
);
|
||||
assert_eq!("/home/<USER>", "/home/jane".sanitize_path());
|
||||
assert_eq!("/opt/home/<USER>", "/opt/home/user".sanitize_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_users_unix() {
|
||||
assert_eq!(
|
||||
"/Users/<USER>/desktop",
|
||||
"/Users/alice/desktop".sanitize_path()
|
||||
);
|
||||
assert_eq!("/Users/<USER>/", "/Users/bob/".sanitize_path());
|
||||
assert_eq!("/user/Users/<USER>", "/user/Users/name".sanitize_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_users_windows() {
|
||||
assert_eq!(
|
||||
"\\Users\\<USER>\\documents",
|
||||
"\\Users\\charlie\\documents".sanitize_path()
|
||||
);
|
||||
assert_eq!("\\Users\\<USER>", "\\Users\\david".sanitize_path());
|
||||
assert_eq!(
|
||||
"C:\\Other\\Users\\<USER>",
|
||||
"C:\\Other\\Users\\folder".sanitize_path()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_match() {
|
||||
assert_eq!("/opt/data/file.txt", "/opt/data/file.txt".sanitize_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_separators() {
|
||||
assert_eq!(
|
||||
"/home/<USER>\\documents",
|
||||
"/home/eve\\documents".sanitize_path()
|
||||
);
|
||||
assert_eq!(
|
||||
"\\Users\\<USER>/desktop",
|
||||
"\\Users\\frank/desktop".sanitize_path()
|
||||
);
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1,749 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
use std::mem::offset_of;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use cgmath::{Deg, Matrix4, Point3, Rad, SquareMatrix, Vector3, perspective};
|
||||
use futures::executor::block_on;
|
||||
use tracing::{debug, error, trace};
|
||||
use wgpu::TextureUsages;
|
||||
use wgpu::{Backends, InstanceDescriptor, util::DeviceExt};
|
||||
use wgpu_text::glyph_brush::ab_glyph::FontRef;
|
||||
use wgpu_text::glyph_brush::{HorizontalAlign, Layout, OwnedSection, OwnedText, VerticalAlign};
|
||||
use wgpu_text::{BrushBuilder, TextBrush};
|
||||
use winit::window::Window;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::error::{ZenyxError, ZenyxErrorKind};
|
||||
|
||||
use super::TerminalState;
|
||||
|
||||
const SHADER_SRC: &str = include_str!("shader.wgsl");
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Vertex {
|
||||
pub position: [f32; 3],
|
||||
pub normal: [f32; 3],
|
||||
}
|
||||
|
||||
impl Vertex {
|
||||
const ATTRIBS: [wgpu::VertexAttribute; 2] = [
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: offset_of!(Vertex, normal) as u64,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
];
|
||||
|
||||
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &Self::ATTRIBS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct CameraUniform {
|
||||
view: [[f32; 4]; 4],
|
||||
proj: [[f32; 4]; 4],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct ModelUniform {
|
||||
model: [[f32; 4]; 4],
|
||||
}
|
||||
|
||||
struct Camera {
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
bind_group: wgpu::BindGroup,
|
||||
view: Matrix4<f32>,
|
||||
proj: Matrix4<f32>,
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
fn new(
|
||||
device: &wgpu::Device,
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Self {
|
||||
let view = Matrix4::look_at_rh(
|
||||
Point3::new(0.0, 0.0, 3.0),
|
||||
Point3::new(0.0, 0.0, 0.0),
|
||||
Vector3::unit_y(),
|
||||
);
|
||||
let aspect = width as f32 / height as f32;
|
||||
let proj = perspective(Rad::from(Deg(45.0)), aspect, 0.1, 100.0);
|
||||
|
||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Camera Uniform Buffer"),
|
||||
size: std::mem::size_of::<CameraUniform>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Camera Bind Group"),
|
||||
layout: bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
Self {
|
||||
uniform_buffer,
|
||||
bind_group,
|
||||
view,
|
||||
proj,
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&mut self, width: u32, height: u32) {
|
||||
let aspect = width as f32 / height as f32;
|
||||
self.proj = perspective(Rad::from(Deg(45.0)), aspect, 0.1, 100.0);
|
||||
}
|
||||
|
||||
fn update(&self, queue: &wgpu::Queue) {
|
||||
let view_array: [[f32; 4]; 4] = self.view.into();
|
||||
let proj_array: [[f32; 4]; 4] = self.proj.into();
|
||||
let uniform = CameraUniform {
|
||||
view: view_array,
|
||||
proj: proj_array,
|
||||
};
|
||||
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Model {
|
||||
vertex_buffer: wgpu::Buffer,
|
||||
index_buffer: wgpu::Buffer,
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
bind_group: wgpu::BindGroup,
|
||||
index_count: u32,
|
||||
transform: Matrix4<f32>,
|
||||
version: u32,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn new(
|
||||
device: &wgpu::Device,
|
||||
vertices: &[Vertex],
|
||||
indices: &[u32],
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> Self {
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Vertex Buffer"),
|
||||
contents: bytemuck::cast_slice(vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Index Buffer"),
|
||||
contents: bytemuck::cast_slice(indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
|
||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Model Uniform Buffer"),
|
||||
size: std::mem::size_of::<ModelUniform>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Model Bind Group"),
|
||||
layout: bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
Self {
|
||||
vertex_buffer,
|
||||
index_buffer,
|
||||
uniform_buffer,
|
||||
bind_group,
|
||||
index_count: indices.len() as u32,
|
||||
transform: Matrix4::identity(),
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&self, queue: &wgpu::Queue) {
|
||||
let model_array: [[f32; 4]; 4] = self.transform.into();
|
||||
let uniform = ModelUniform { model: model_array };
|
||||
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
|
||||
}
|
||||
|
||||
fn set_transform(&mut self, transform: Matrix4<f32>) {
|
||||
if self.transform != transform {
|
||||
self.transform = transform;
|
||||
self.version += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Renderer<'window> {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
surface: wgpu::Surface<'window>,
|
||||
surface_config: wgpu::SurfaceConfiguration,
|
||||
camera: Camera,
|
||||
models: Vec<Model>,
|
||||
render_pipeline: wgpu::RenderPipeline,
|
||||
depth_texture: wgpu::Texture,
|
||||
depth_texture_view: wgpu::TextureView,
|
||||
camera_bind_group_layout: wgpu::BindGroupLayout,
|
||||
model_bind_group_layout: wgpu::BindGroupLayout,
|
||||
bg_color: wgpu::Color,
|
||||
start_time: Instant,
|
||||
last_frame_instant: Instant,
|
||||
frame_count: u32,
|
||||
fps: f32,
|
||||
font_state: FontState,
|
||||
model_versions: Vec<u32>,
|
||||
}
|
||||
|
||||
struct FontState {
|
||||
brush: TextBrush<FontRef<'static>>,
|
||||
output_section: OwnedSection,
|
||||
input_section: OwnedSection,
|
||||
fps_section: OwnedSection,
|
||||
scale: f32,
|
||||
color: wgpu::Color,
|
||||
}
|
||||
impl<'window> Renderer<'window> {
|
||||
pub async fn new(window: Arc<Window>) -> Result<Self> {
|
||||
let instance = wgpu::Instance::new(&InstanceDescriptor {
|
||||
backends: Backends::from_comma_list("dx12,metal,opengl,webgpu,vulkan"),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let surface = instance.create_surface(Arc::clone(&window))?;
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
ZenyxError::builder(ZenyxErrorKind::AdapterRequest)
|
||||
.with_message("No suitable adapter found")
|
||||
.build()
|
||||
})?;
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor::default(), None)
|
||||
.await
|
||||
.map_err(ZenyxError::from)?;
|
||||
|
||||
let size = window.inner_size();
|
||||
let width = size.width.max(1);
|
||||
let height = size.height.max(1);
|
||||
|
||||
let camera_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Camera Bind Group Layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: wgpu::BufferSize::new(
|
||||
std::mem::size_of::<CameraUniform>() as u64,
|
||||
),
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let model_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Model Bind Group Layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: wgpu::BufferSize::new(
|
||||
std::mem::size_of::<ModelUniform>() as u64,
|
||||
),
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Pipeline Layout"),
|
||||
bind_group_layouts: &[&camera_bind_group_layout, &model_bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Main Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(SHADER_SRC)),
|
||||
});
|
||||
|
||||
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Main Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Vertex::desc()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: surface.get_capabilities(&adapter).formats[0],
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
// cull_mode: ,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::Less,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let camera = Camera::new(&device, &camera_bind_group_layout, width, height);
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let present_mode = [
|
||||
wgpu::PresentMode::Immediate,
|
||||
wgpu::PresentMode::Mailbox,
|
||||
wgpu::PresentMode::AutoNoVsync,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|mode| surface_caps.present_modes.contains(mode))
|
||||
.unwrap_or(wgpu::PresentMode::Fifo);
|
||||
|
||||
debug!("Using {:#?} present mode.", present_mode);
|
||||
|
||||
let surface_config = wgpu::SurfaceConfiguration {
|
||||
width,
|
||||
height,
|
||||
format: surface_caps.formats[0],
|
||||
present_mode,
|
||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||
view_formats: vec![],
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
desired_maximum_frame_latency: 3,
|
||||
};
|
||||
surface.configure(&device, &surface_config);
|
||||
let (depth_texture, depth_texture_view) =
|
||||
create_depth_texture(&device, surface_config.width, surface_config.height);
|
||||
|
||||
let font_bytes = include_bytes!("DejaVuSans.ttf");
|
||||
let font = FontRef::try_from_slice(font_bytes).map_err(|e| {
|
||||
ZenyxError::builder(ZenyxErrorKind::FontLoading)
|
||||
.with_message("Font loading failed")
|
||||
.with_source(e)
|
||||
.build()
|
||||
})?;
|
||||
|
||||
let brush =
|
||||
BrushBuilder::using_font(font).build(&device, width, height, surface_config.format);
|
||||
let base_width = 1280.0;
|
||||
let base_scale = 30.0;
|
||||
let scale = base_scale * (surface_config.width as f32 / base_width as f32).clamp(0.5, 2.0);
|
||||
let color = wgpu::Color::WHITE;
|
||||
|
||||
let fps_section = OwnedSection::default()
|
||||
.add_text(OwnedText::new("FPS: 0.00").with_scale(scale).with_color([
|
||||
color.r as f32,
|
||||
color.g as f32,
|
||||
color.b as f32,
|
||||
color.a as f32,
|
||||
]))
|
||||
.with_screen_position((10.0, 10.0))
|
||||
.with_bounds((200.0, 50.0))
|
||||
.with_layout(
|
||||
Layout::default()
|
||||
.h_align(HorizontalAlign::Left)
|
||||
.v_align(VerticalAlign::Top),
|
||||
);
|
||||
|
||||
let output_section = OwnedSection::default()
|
||||
.with_screen_position((10.0, 50.0))
|
||||
.with_bounds((width as f32 - 20.0, f32::MAX))
|
||||
.with_layout(
|
||||
Layout::default()
|
||||
.h_align(HorizontalAlign::Left)
|
||||
.v_align(VerticalAlign::Top),
|
||||
);
|
||||
|
||||
let input_section = OwnedSection::default()
|
||||
.with_screen_position((10.0, height as f32 - 50.0))
|
||||
.with_bounds((width as f32 - 20.0, f32::MAX))
|
||||
.with_layout(
|
||||
Layout::default()
|
||||
.h_align(HorizontalAlign::Left)
|
||||
.v_align(VerticalAlign::Top),
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
surface_config,
|
||||
camera,
|
||||
models: Vec::new(),
|
||||
render_pipeline,
|
||||
camera_bind_group_layout,
|
||||
model_bind_group_layout,
|
||||
bg_color: wgpu::Color {
|
||||
r: 0.1,
|
||||
g: 0.1,
|
||||
b: 0.1,
|
||||
a: 1.0,
|
||||
},
|
||||
start_time: Instant::now(),
|
||||
last_frame_instant: Instant::now(),
|
||||
frame_count: 0,
|
||||
depth_texture,
|
||||
depth_texture_view,
|
||||
fps: 0f32,
|
||||
font_state: FontState {
|
||||
brush,
|
||||
fps_section,
|
||||
output_section,
|
||||
input_section,
|
||||
scale,
|
||||
color,
|
||||
},
|
||||
model_versions: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_blocking(window: Arc<Window>) -> Result<Self> {
|
||||
block_on(Self::new(window))
|
||||
}
|
||||
|
||||
pub fn add_model(&mut self, vertices: &[Vertex], indicies: &[u32]) {
|
||||
let model = Model::new(
|
||||
&self.device,
|
||||
vertices,
|
||||
indicies,
|
||||
&self.model_bind_group_layout,
|
||||
);
|
||||
self.models.push(model);
|
||||
self.model_versions.push(0);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, new_size: (u32, u32)) {
|
||||
let (width, height) = new_size;
|
||||
let (depth_texture, depth_view) = create_depth_texture(&self.device, width, height);
|
||||
self.surface_config.width = width.max(1);
|
||||
self.surface_config.height = height.max(1);
|
||||
self.surface.configure(&self.device, &self.surface_config);
|
||||
self.depth_texture = depth_texture;
|
||||
self.depth_texture_view = depth_view;
|
||||
self.font_state
|
||||
.brush
|
||||
.resize_view(width as f32, height as f32, &self.queue);
|
||||
self.font_state.output_section.bounds = (width as f32 - 20.0, height as f32 - 60.0);
|
||||
self.font_state.input_section.screen_position = (10.0, height as f32 - 50.0);
|
||||
self.camera.resize(width, height);
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, terminal_state: Option<&mut TerminalState>) {
|
||||
let elapsed = self.start_time.elapsed().as_secs_f32();
|
||||
if let Some(terminal_state) = terminal_state {
|
||||
let delta_time = self.last_frame_instant.elapsed().as_secs_f32();
|
||||
self.draw_terminal(terminal_state, delta_time);
|
||||
} else {
|
||||
self.camera.update(&self.queue);
|
||||
|
||||
for (i, model) in self.models.iter_mut().enumerate() {
|
||||
let angle = Rad(elapsed * 0.8 + i as f32 * 0.3);
|
||||
if i % 2 == 0 {
|
||||
model.set_transform(Matrix4::from_angle_y(angle));
|
||||
} else {
|
||||
model
|
||||
.set_transform(Matrix4::from_angle_x(angle) * Matrix4::from_angle_y(angle));
|
||||
}
|
||||
}
|
||||
for (i, model) in self.models.iter().enumerate() {
|
||||
if model.version > self.model_versions[i] {
|
||||
model.update(&self.queue);
|
||||
#[cfg(debug_assertions)]
|
||||
trace!("Updating model: {:#?}", model);
|
||||
self.model_versions[i] = model.version;
|
||||
}
|
||||
}
|
||||
}
|
||||
let surface_texture = self
|
||||
.surface
|
||||
.get_current_texture()
|
||||
.map_err(|e| {
|
||||
ZenyxError::builder(ZenyxErrorKind::SurfaceTexture)
|
||||
.with_message("Failed to acquire surface texture")
|
||||
.with_source(e)
|
||||
.build()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let view = surface_texture
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
});
|
||||
let fps_text = format!("FPS: {:.2}", self.fps);
|
||||
self.font_state.fps_section.text.clear();
|
||||
self.font_state.fps_section.text.push(
|
||||
OwnedText::new(fps_text)
|
||||
.with_scale(self.font_state.scale)
|
||||
.with_color([
|
||||
self.font_state.color.r as f32,
|
||||
self.font_state.color.g as f32,
|
||||
self.font_state.color.b as f32,
|
||||
self.font_state.color.a as f32,
|
||||
]),
|
||||
);
|
||||
if let Err(e) = self.font_state.brush.queue(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
&[
|
||||
self.font_state.fps_section.clone(),
|
||||
self.font_state.input_section.clone(),
|
||||
self.font_state.output_section.clone(),
|
||||
],
|
||||
) {
|
||||
error!("Failed to queue text: {}", e);
|
||||
}
|
||||
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Main Render Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(self.bg_color),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.depth_texture_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
});
|
||||
|
||||
render_pass.set_pipeline(&self.render_pipeline);
|
||||
render_pass.set_bind_group(0, &self.camera.bind_group, &[]);
|
||||
|
||||
for model in &self.models {
|
||||
render_pass.set_bind_group(1, &model.bind_group, &[]);
|
||||
render_pass.set_vertex_buffer(0, model.vertex_buffer.slice(..));
|
||||
render_pass
|
||||
.set_index_buffer(model.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||
render_pass.draw_indexed(0..model.index_count, 0, 0..1);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut text_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Text Render Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
});
|
||||
|
||||
self.font_state.brush.draw(&mut text_pass);
|
||||
}
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
surface_texture.present();
|
||||
|
||||
self.frame_count += 1;
|
||||
|
||||
let elapsed_secs = self.last_frame_instant.elapsed().as_secs_f32();
|
||||
if elapsed_secs >= 1.0 {
|
||||
let fps = self.frame_count as f32 / elapsed_secs;
|
||||
// trace!("Renderer FPS: {:.2}", fps);
|
||||
self.fps = fps;
|
||||
self.frame_count = 0;
|
||||
self.last_frame_instant = Instant::now();
|
||||
}
|
||||
}
|
||||
fn draw_terminal(&mut self, terminal_state: &mut TerminalState, delta_time: f32) {
|
||||
terminal_state.cursor_blink_timer += delta_time;
|
||||
if terminal_state.cursor_blink_timer >= 0.5 {
|
||||
terminal_state.show_cursor = !terminal_state.show_cursor;
|
||||
terminal_state.cursor_blink_timer = 0.0;
|
||||
}
|
||||
|
||||
let line_height = self.font_state.scale * 1.5;
|
||||
let max_visible_lines = (self.surface_config.height as f32 / line_height) as usize;
|
||||
terminal_state.max_history_lines = max_visible_lines;
|
||||
|
||||
self.font_state.output_section.text.clear();
|
||||
let mut current_line = 0;
|
||||
let mut output_y = 0.0;
|
||||
|
||||
for line in terminal_state.output_history.iter().rev() {
|
||||
let sublines = line.split('\n').collect::<Vec<_>>();
|
||||
|
||||
for subline in sublines.iter().rev() {
|
||||
if current_line >= terminal_state.scroll_offset + max_visible_lines {
|
||||
break;
|
||||
}
|
||||
|
||||
if current_line >= terminal_state.scroll_offset {
|
||||
let processed = subline.replace('\t', " ");
|
||||
self.font_state.output_section.text.push(
|
||||
OwnedText::new(processed)
|
||||
.with_scale(self.font_state.scale)
|
||||
.with_color([1.0, 1.0, 1.0, 1.0]),
|
||||
);
|
||||
output_y += line_height;
|
||||
}
|
||||
|
||||
current_line += 1;
|
||||
}
|
||||
|
||||
if current_line >= terminal_state.scroll_offset + max_visible_lines {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.font_state.input_section.text.clear();
|
||||
let mut input_y = 0.0;
|
||||
let input_text = format!(
|
||||
"> {}{}",
|
||||
terminal_state.input_buffer.replace('\t', " "),
|
||||
if terminal_state.show_cursor { "_" } else { "" }
|
||||
);
|
||||
|
||||
for (line_num, subline) in input_text.split('\n').enumerate() {
|
||||
self.font_state.input_section.text.push(
|
||||
OwnedText::new(subline)
|
||||
.with_scale(self.font_state.scale)
|
||||
.with_color([0.0, 1.0, 0.0, 1.0]), // .with_position((0.0, input_y)),
|
||||
);
|
||||
input_y += line_height;
|
||||
|
||||
if line_num >= 2 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.font_state.output_section.bounds = (
|
||||
self.surface_config.width as f32 - 20.0,
|
||||
self.surface_config.height as f32 - 60.0,
|
||||
);
|
||||
|
||||
self.font_state.input_section.bounds =
|
||||
(self.surface_config.width as f32 - 20.0, line_height * 3.0);
|
||||
|
||||
if let Err(e) = self.font_state.brush.queue(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
&[
|
||||
self.font_state.output_section.clone(),
|
||||
self.font_state.input_section.clone(),
|
||||
self.font_state.fps_section.clone(),
|
||||
],
|
||||
) {
|
||||
error!("Failed to queue text: {}", e);
|
||||
}
|
||||
}
|
||||
pub fn set_bg_color(&mut self, color: wgpu::Color) {
|
||||
self.bg_color = color;
|
||||
}
|
||||
|
||||
pub fn bg_color(&self) -> &wgpu::Color {
|
||||
&self.bg_color
|
||||
}
|
||||
|
||||
pub fn text_color(&self) -> &wgpu::Color {
|
||||
&self.font_state.color
|
||||
}
|
||||
|
||||
pub fn set_text_color(&mut self, color: wgpu::Color) {
|
||||
self.font_state.color = color;
|
||||
}
|
||||
}
|
||||
fn create_depth_texture(
|
||||
device: &wgpu::Device,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> (wgpu::Texture, wgpu::TextureView) {
|
||||
let size = wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let desc = wgpu::TextureDescriptor {
|
||||
label: Some("Depth Texture"),
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
};
|
||||
|
||||
let texture = device.create_texture(&desc);
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
(texture, view)
|
||||
}
|
|
@ -1,582 +0,0 @@
|
|||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Cursor;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ctx::{Renderer, Vertex};
|
||||
use image::ImageDecoder;
|
||||
use image::ImageFormat;
|
||||
use tobj::{LoadOptions, Model};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
use wgpu::rwh::HasWindowHandle;
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::dpi::Size;
|
||||
use winit::event::{KeyEvent, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::keyboard::NamedKey;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winit::platform::windows::WindowAttributesExtWindows;
|
||||
use winit::window::Fullscreen;
|
||||
use winit::window::Icon;
|
||||
use winit::window::Window;
|
||||
use winit::window::WindowId;
|
||||
|
||||
use super::repl::input::evaluate_command;
|
||||
|
||||
pub mod ctx;
|
||||
|
||||
pub struct WindowContext<'window> {
|
||||
window: Arc<Window>,
|
||||
ctx: Renderer<'window>,
|
||||
main_window: bool,
|
||||
terminal_state: Option<TerminalState>,
|
||||
}
|
||||
|
||||
impl Deref for WindowContext<'_> {
|
||||
type Target = winit::window::Window;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.window.as_ref()
|
||||
}
|
||||
}
|
||||
impl WindowContext<'_> {
|
||||
pub fn is_main_window(&self) -> bool {
|
||||
self.main_window
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct TerminalState {
|
||||
input_buffer: String,
|
||||
output_history: Vec<String>,
|
||||
scroll_offset: usize,
|
||||
show_cursor: bool,
|
||||
cursor_blink_timer: f32,
|
||||
command_queue: Vec<String>,
|
||||
needs_execution: bool,
|
||||
max_history_lines: usize,
|
||||
input_history: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct App<'window> {
|
||||
windows: ahash::AHashMap<WindowId, WindowContext<'window>>,
|
||||
// Number of windows that have been dropped but *NOT* destroyed yet
|
||||
windows_in_limbo: usize,
|
||||
}
|
||||
|
||||
impl Default for App<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
windows: ahash::AHashMap::new(),
|
||||
windows_in_limbo: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static CUBE_OBJ: &str = "
|
||||
# Blender 4.2.3 LTS
|
||||
# www.blender.org
|
||||
mtllib untitled.mtl
|
||||
o Cube
|
||||
v 0.645975 0.645975 -0.645975
|
||||
v 0.645975 -0.645975 -0.645975
|
||||
v 0.645975 0.645975 0.645975
|
||||
v 0.645975 -0.645975 0.645975
|
||||
v -0.645975 0.645975 -0.645975
|
||||
v -0.645975 -0.645975 -0.645975
|
||||
v -0.645975 0.645975 0.645975
|
||||
v -0.645975 -0.645975 0.645975
|
||||
vn -0.0000 1.0000 -0.0000
|
||||
vn -0.0000 -0.0000 1.0000
|
||||
vn -1.0000 -0.0000 -0.0000
|
||||
vn -0.0000 -1.0000 -0.0000
|
||||
vn 1.0000 -0.0000 -0.0000
|
||||
vn -0.0000 -0.0000 -1.0000
|
||||
vt 0.625000 0.500000
|
||||
vt 0.875000 0.500000
|
||||
vt 0.875000 0.750000
|
||||
vt 0.625000 0.750000
|
||||
vt 0.375000 0.750000
|
||||
vt 0.625000 1.000000
|
||||
vt 0.375000 1.000000
|
||||
vt 0.375000 0.000000
|
||||
vt 0.625000 0.000000
|
||||
vt 0.625000 0.250000
|
||||
vt 0.375000 0.250000
|
||||
vt 0.125000 0.500000
|
||||
vt 0.375000 0.500000
|
||||
vt 0.125000 0.750000
|
||||
s 0
|
||||
usemtl Material
|
||||
f 1/1/1 5/2/1 7/3/1 3/4/1
|
||||
f 4/5/2 3/4/2 7/6/2 8/7/2
|
||||
f 8/8/3 7/9/3 5/10/3 6/11/3
|
||||
f 6/12/4 2/13/4 4/5/4 8/14/4
|
||||
f 2/13/5 1/1/5 3/4/5 4/5/5
|
||||
f 6/11/6 5/10/6 1/1/6 2/13/6
|
||||
|
||||
";
|
||||
|
||||
impl App<'_> {
|
||||
const ICON: &'static [u8] =
|
||||
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/Badge.png"));
|
||||
|
||||
fn create_main_window(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let icon = self.load_icon_from_bytes(Self::ICON).unwrap();
|
||||
|
||||
let win_attr = Window::default_attributes()
|
||||
.with_title("Zenyx")
|
||||
.with_min_inner_size(Size::Logical(LogicalSize::new(100.0, 100.0)))
|
||||
.with_window_icon(icon.clone());
|
||||
// .with_taskbar_icon(icon);
|
||||
|
||||
match event_loop.create_window(win_attr) {
|
||||
Ok(window) => {
|
||||
let window = Arc::new(window);
|
||||
let window_id = window.id();
|
||||
match Renderer::new_blocking(window.clone()) {
|
||||
Ok(mut wgpu_ctx) => {
|
||||
let obj = match tobj::load_obj(
|
||||
"Pumpkin.obj",
|
||||
&LoadOptions {
|
||||
triangulate: true,
|
||||
single_index: true,
|
||||
..Default::default()
|
||||
},
|
||||
) {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
error!("Failed to load Pumpkin.obj: {e}");
|
||||
let fallback_obj = CUBE_OBJ.to_string();
|
||||
tobj::load_obj_buf(
|
||||
&mut fallback_obj.as_bytes(),
|
||||
&LoadOptions {
|
||||
triangulate: true,
|
||||
single_index: true,
|
||||
..Default::default()
|
||||
},
|
||||
|_| Ok(Default::default()),
|
||||
)
|
||||
.expect("Failed to load fallback CUBE_OBJ")
|
||||
}
|
||||
};
|
||||
|
||||
let (combined_vertices, combined_indices) = parse_obj(&obj.0);
|
||||
|
||||
wgpu_ctx.add_model(&combined_vertices, &combined_indices);
|
||||
|
||||
self.windows.insert(
|
||||
window_id,
|
||||
WindowContext {
|
||||
window,
|
||||
ctx: wgpu_ctx,
|
||||
main_window: true,
|
||||
terminal_state: None,
|
||||
},
|
||||
);
|
||||
info!("Main window created: {:?}", window_id);
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
Err(e) => error!("Failed to create WGPU context: {:#}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to create main window: {:#}", e),
|
||||
=======
|
||||
Err(e) => error!("Failed to create WGPU context: {:}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to create main window: {}", e),
|
||||
>>>>>>> refs/rewritten/main-6
|
||||
}
|
||||
}
|
||||
|
||||
fn create_terminal_window(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let icon = self.load_icon_from_bytes(Self::ICON).unwrap();
|
||||
|
||||
let win_attr = Window::default_attributes()
|
||||
.with_title("Zenyx Terminal")
|
||||
.with_inner_size(Size::Logical(LogicalSize::new(800.0, 600.0)))
|
||||
.with_window_icon(icon);
|
||||
|
||||
match event_loop.create_window(win_attr) {
|
||||
Ok(window) => {
|
||||
let window = Arc::new(window);
|
||||
let window_id = window.id();
|
||||
match Renderer::new_blocking(window.clone()) {
|
||||
Ok(mut wgpu_ctx) => {
|
||||
wgpu_ctx.set_bg_color(wgpu::Color::BLACK);
|
||||
wgpu_ctx.set_text_color(wgpu::Color::GREEN);
|
||||
self.windows.insert(
|
||||
window_id,
|
||||
WindowContext {
|
||||
window,
|
||||
ctx: wgpu_ctx,
|
||||
main_window: false,
|
||||
terminal_state: Some(TerminalState::default()),
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(e) => error!("Failed to create terminal WGPU context: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to create terminal window: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_terminal_input(&mut self, window_id: WindowId, key_event: KeyEvent) {
|
||||
let Some(window_context) = self.windows.get_mut(&window_id) else {
|
||||
return;
|
||||
};
|
||||
let state = window_context.terminal_state.as_mut().unwrap();
|
||||
|
||||
if key_event.state.is_pressed() {
|
||||
match key_event.logical_key {
|
||||
winit::keyboard::Key::Named(NamedKey::Enter) => {
|
||||
if !state.input_buffer.is_empty() {
|
||||
state.command_queue.push(state.input_buffer.clone());
|
||||
state.input_history.push("\n\n".to_string());
|
||||
state.input_buffer.clear();
|
||||
state.needs_execution = true;
|
||||
}
|
||||
}
|
||||
winit::keyboard::Key::Named(NamedKey::Backspace) => {
|
||||
state.input_buffer.pop();
|
||||
}
|
||||
winit::keyboard::Key::Named(NamedKey::Space) => {
|
||||
state.input_buffer.push(' ');
|
||||
}
|
||||
winit::keyboard::Key::Character(c) => {
|
||||
state.input_buffer.push_str(&c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_close_requested(&mut self, window_id: WindowId) {
|
||||
if let Some(window) = self.windows.remove(&window_id) {
|
||||
debug!("Window {:?} closed", window_id);
|
||||
self.windows_in_limbo += 1;
|
||||
if window.is_main_window() {
|
||||
self.windows_in_limbo += self.windows.len();
|
||||
self.windows.clear();
|
||||
}
|
||||
} else {
|
||||
warn!("Tried to close non-existent window {:?}", window_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_keyboard_input(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_id: WindowId,
|
||||
key_event: KeyEvent,
|
||||
) {
|
||||
if !key_event.state.is_pressed() || key_event.repeat {
|
||||
return;
|
||||
}
|
||||
if let Some(window_context) = self.windows.get(&window_id) {
|
||||
if window_context.terminal_state.is_some() {
|
||||
self.handle_terminal_input(window_id, key_event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
match key_event.physical_key {
|
||||
winit::keyboard::PhysicalKey::Code(code) => match code {
|
||||
winit::keyboard::KeyCode::Space => {
|
||||
self.toggle_background(window_id);
|
||||
}
|
||||
winit::keyboard::KeyCode::Escape => {
|
||||
self.spawn_child_window(event_loop);
|
||||
}
|
||||
winit::keyboard::KeyCode::F11 => self.toggle_fullscreen(window_id),
|
||||
winit::keyboard::KeyCode::F12 => self.create_terminal_window(event_loop),
|
||||
other => error!("Unimplemented keycode: {:?}", other),
|
||||
},
|
||||
_ => error!("Unhandled key event: {:?}", key_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_background(&mut self, window_id: WindowId) {
|
||||
if let Some(window_context) = self.windows.get_mut(&window_id) {
|
||||
let current_color = window_context.ctx.bg_color();
|
||||
|
||||
let new_color = match current_color {
|
||||
&wgpu::Color::WHITE => wgpu::Color::BLACK,
|
||||
&wgpu::Color::BLACK => wgpu::Color::WHITE,
|
||||
_ => wgpu::Color::WHITE,
|
||||
};
|
||||
let new_text_color = match window_context.ctx.text_color() {
|
||||
&wgpu::Color::WHITE => wgpu::Color::BLACK,
|
||||
&wgpu::Color::BLACK => wgpu::Color::WHITE,
|
||||
_ => wgpu::Color::WHITE,
|
||||
};
|
||||
|
||||
println!("new text color {new_text_color:#?}");
|
||||
window_context.ctx.set_bg_color(new_color);
|
||||
window_context.ctx.set_text_color(new_text_color);
|
||||
debug!("Toggled background color for window {:?}", window_id);
|
||||
} else {
|
||||
warn!("No window context for toggling background: {:?}", window_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&mut self, window_id: WindowId) {
|
||||
if let Some(ctx) = self.windows.get_mut(&window_id) {
|
||||
let is_fullscreen = ctx.window.fullscreen().is_some();
|
||||
let fullscreen_mode = if is_fullscreen {
|
||||
None
|
||||
} else {
|
||||
ctx.window
|
||||
.current_monitor()
|
||||
.map(|monitor| Fullscreen::Borderless(Some(monitor)))
|
||||
};
|
||||
|
||||
ctx.window.set_fullscreen(fullscreen_mode);
|
||||
debug!("Fullscreen toggled for window: {:?}", window_id);
|
||||
} else {
|
||||
warn!("No window found for fullscreen toggle: {:?}", window_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_icon_from_bytes(&self, bytes: &[u8]) -> Result<Option<Icon>, String> {
|
||||
let cursor = Cursor::new(bytes);
|
||||
let format = image::guess_format(bytes).map_err(|_| "Failed to guess image format")?;
|
||||
let decoder = match format {
|
||||
ImageFormat::Png => image::codecs::png::PngDecoder::new(cursor)
|
||||
.map_err(|e| format!("Failed to decode PNG: {}", e))?,
|
||||
_ => {
|
||||
let img = image::load_from_memory(bytes)
|
||||
.map_err(|e| format!("Failed to load image: {}", e))?
|
||||
.into_rgba8();
|
||||
let (width, height) = img.dimensions();
|
||||
return Icon::from_rgba(img.into_raw(), width, height)
|
||||
.map(Some)
|
||||
.map_err(|e| format!("Failed to create icon from bytes: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let (width, height) = decoder.dimensions();
|
||||
let mut image_data = vec![0; decoder.total_bytes() as usize];
|
||||
decoder
|
||||
.read_image(&mut image_data)
|
||||
.map_err(|e| format!("Failed to read image data: {}", e))?;
|
||||
|
||||
Icon::from_rgba(image_data, width, height)
|
||||
.map(Some)
|
||||
.map_err(|e| format!("Failed to create icon from bytes: {}", e))
|
||||
}
|
||||
|
||||
fn spawn_child_window(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if let Some(main_ctx) = self.windows.values().find(|ctx| ctx.is_main_window()) {
|
||||
let title = format!("Zenyx - New Window {}", self.windows.len());
|
||||
let icon = self.load_icon_from_bytes(Self::ICON).unwrap();
|
||||
|
||||
let win_attr = unsafe {
|
||||
let base = Window::default_attributes()
|
||||
.with_title(title)
|
||||
// .with_taskbar_icon(icon)
|
||||
.with_min_inner_size(Size::Logical(LogicalSize::new(100.0, 100.0)))
|
||||
.with_window_icon(icon.clone());
|
||||
|
||||
match main_ctx.window_handle() {
|
||||
Ok(handle) => {
|
||||
if !cfg!(target_os = "windows") {
|
||||
base.with_parent_window(Some(handle.as_raw()))
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
base
|
||||
}
|
||||
}
|
||||
};
|
||||
match event_loop.create_window(win_attr) {
|
||||
Ok(window) => {
|
||||
let window = Arc::new(window);
|
||||
let window_id = window.id();
|
||||
match Renderer::new_blocking(window.clone()) {
|
||||
Ok(mut wgpu_ctx) => {
|
||||
{
|
||||
let mut tmp_path: PathBuf = env::temp_dir();
|
||||
tmp_path.push("cube.obj");
|
||||
if let Err(e) = fs::write(&tmp_path, CUBE_OBJ) {
|
||||
error!("Failed to write cube OBJ to temp: {}", e);
|
||||
}
|
||||
|
||||
let load_options = tobj::LoadOptions {
|
||||
triangulate: true,
|
||||
single_index: true,
|
||||
..Default::default()
|
||||
};
|
||||
match tobj::load_obj(tmp_path.to_str().unwrap(), &load_options) {
|
||||
Ok(cube_model) => {
|
||||
let (cube_vertices, cube_indices) =
|
||||
parse_obj(&cube_model.0);
|
||||
wgpu_ctx.add_model(&cube_vertices, &cube_indices);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to load cube OBJ from temp file: {:#}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.windows.insert(
|
||||
window_id,
|
||||
WindowContext {
|
||||
window,
|
||||
ctx: wgpu_ctx,
|
||||
main_window: false,
|
||||
terminal_state: None,
|
||||
},
|
||||
);
|
||||
debug!("Spawned new child window: {:?}", window_id);
|
||||
}
|
||||
Err(e) => error!("Failed to create WGPU context for child window: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to create child window: {}", e),
|
||||
}
|
||||
} else {
|
||||
error!("No main window found. Cannot spawn a child window.");
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_redraw_requested(&mut self, window_id: WindowId) {
|
||||
if let Some(window_context) = self.windows.get_mut(&window_id) {
|
||||
if let Some(terminal_state) = window_context.terminal_state.as_mut() {
|
||||
if terminal_state.needs_execution {
|
||||
for command in terminal_state.command_queue.drain(..) {
|
||||
match evaluate_command(&command) {
|
||||
Ok(output) => {
|
||||
for line in output.lines() {
|
||||
terminal_state.output_history.push(line.to_string());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
terminal_state.output_history.push(format!("Error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
terminal_state.needs_execution = false;
|
||||
|
||||
terminal_state.scroll_offset = terminal_state
|
||||
.output_history
|
||||
.len()
|
||||
.saturating_sub(terminal_state.max_history_lines);
|
||||
}
|
||||
}
|
||||
|
||||
let terminal_state = window_context.terminal_state.as_mut();
|
||||
window_context.ctx.draw(terminal_state);
|
||||
window_context.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_resize(&mut self, window_id: WindowId, new_size: winit::dpi::PhysicalSize<u32>) {
|
||||
if let Some(window_context) = self.windows.get_mut(&window_id) {
|
||||
// if we dont ignore size 0 this WILL cause a crash. DO NOT REMOVE
|
||||
|
||||
if new_size.height == 0 || new_size.width == 0 {
|
||||
error!("Attempted to resize a window to 0x0!");
|
||||
return;
|
||||
}
|
||||
window_context.ctx.resize(new_size.into());
|
||||
window_context.window.request_redraw();
|
||||
debug!(
|
||||
"Resized window {:?} to {}x{}",
|
||||
window_id, new_size.width, new_size.height
|
||||
);
|
||||
} else {
|
||||
warn!("Received resize for unknown window {:?}", window_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_destroyed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.windows_in_limbo -= 1;
|
||||
if self.windows_in_limbo == 0 && self.windows.is_empty() {
|
||||
debug!("All windows are closed. Exiting event loop.");
|
||||
event_loop.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_obj(obj: &Vec<Model>) -> (Vec<Vertex>, Vec<u32>) {
|
||||
let mut combined_vertices = Vec::new();
|
||||
let mut combined_indices = Vec::new();
|
||||
let mut vertex_offset = 0;
|
||||
|
||||
for object in obj {
|
||||
let mesh: &_ = &object.mesh;
|
||||
let vertices: Vec<Vertex> = (0..mesh.positions.len() / 3)
|
||||
.map(|i| Vertex {
|
||||
position: [
|
||||
mesh.positions[i * 3],
|
||||
mesh.positions[i * 3 + 1],
|
||||
mesh.positions[i * 3 + 2],
|
||||
],
|
||||
normal: if !mesh.normals.is_empty() {
|
||||
[
|
||||
mesh.normals[i * 3],
|
||||
mesh.normals[i * 3 + 1],
|
||||
mesh.normals[i * 3 + 2],
|
||||
]
|
||||
} else {
|
||||
[0.0; 3]
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
combined_vertices.extend(vertices);
|
||||
combined_indices.extend(mesh.indices.iter().map(|&index| index + vertex_offset));
|
||||
vertex_offset += (mesh.positions.len() as u32) / 3;
|
||||
}
|
||||
|
||||
(combined_vertices, combined_indices)
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App<'_> {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.windows.is_empty() {
|
||||
self.create_main_window(event_loop);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
self.handle_close_requested(window_id);
|
||||
}
|
||||
WindowEvent::KeyboardInput {
|
||||
event: key_event, ..
|
||||
} => {
|
||||
self.handle_keyboard_input(event_loop, window_id, key_event);
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
self.handle_redraw_requested(window_id);
|
||||
}
|
||||
WindowEvent::Resized(new_size) => {
|
||||
self.handle_resize(window_id, new_size);
|
||||
}
|
||||
WindowEvent::Destroyed => {
|
||||
self.handle_destroyed(event_loop);
|
||||
}
|
||||
_ => trace!("Unhandled window event for window {:?}", window_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_renderer(event_loop: EventLoop<()>) {
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
let mut app = App::default();
|
||||
if let Err(e) = event_loop.run_app(&mut app) {
|
||||
error!("Failed to run application: {}", e);
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
struct CameraUniform {
|
||||
view: mat4x4<f32>,
|
||||
proj: mat4x4<f32>,
|
||||
};
|
||||
|
||||
struct ModelUniform {
|
||||
model: mat4x4<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> camera: CameraUniform;
|
||||
|
||||
@group(1) @binding(0)
|
||||
var<uniform> model: ModelUniform;
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
};
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) normal: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(input: VertexInput) -> VertexOutput {
|
||||
var output: VertexOutput;
|
||||
let model_pos = model.model * vec4<f32>(input.position, 1.0);
|
||||
output.clip_position = camera.proj * camera.view * model_pos;
|
||||
output.normal = input.normal;
|
||||
return output;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let ambient: f32 = 0.2;
|
||||
let light_dir = normalize(vec3<f32>(0.5, 1.0, 0.5));
|
||||
let diffuse = clamp(dot(normalize(input.normal), light_dir), 0.0, 1.0);
|
||||
let brightness = ambient + (1.0 - ambient) * diffuse;
|
||||
return vec4<f32>(0.7 * brightness, 0.7 * brightness, 0.9 * brightness, 1.0);
|
||||
}
|
|
@ -1,410 +0,0 @@
|
|||
use std::{collections::VecDeque, fs, path::PathBuf, str::FromStr};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use super::{handler::Command, input::tokenize};
|
||||
use crate::core::repl::handler::COMMAND_MANAGER;
|
||||
use crate::error::{ZenyxError, ZenyxErrorKind};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HelpCommand;
|
||||
|
||||
impl Command for HelpCommand {
|
||||
fn execute(&self, _args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
let manager = COMMAND_MANAGER.read();
|
||||
let mut output = String::new();
|
||||
output.push_str("Available commands:\n\n");
|
||||
|
||||
for (_, command) in manager.get_commands() {
|
||||
output.push_str(&format!(
|
||||
"Command: {}\n\tDescription: {}\n\tParameters: {}\n\tHelp: {}\n\n",
|
||||
command.get_name().to_lowercase(),
|
||||
command.get_description(),
|
||||
command.get_params(),
|
||||
command.get_help()
|
||||
));
|
||||
}
|
||||
|
||||
if !manager.aliases.is_empty() {
|
||||
output.push_str("Aliases:\n");
|
||||
for (alias, command) in &manager.aliases {
|
||||
output.push_str(&format!("\t{} -> {}\n", alias, command));
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn undo(&self) {}
|
||||
|
||||
fn redo(&self) {}
|
||||
|
||||
fn get_description(&self) -> String {
|
||||
String::from("help")
|
||||
}
|
||||
|
||||
fn get_help(&self) -> String {
|
||||
String::from("Displays a list of available commands and their descriptions.")
|
||||
}
|
||||
|
||||
fn get_params(&self) -> String {
|
||||
String::from("No parameters required.")
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
String::from("Help")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClearCommand;
|
||||
|
||||
impl Command for ClearCommand {
|
||||
fn execute(&self, _args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
let _result = if cfg!(target_os = "windows") {
|
||||
std::process::Command::new("cmd")
|
||||
.args(["/c", "cls"])
|
||||
.spawn()
|
||||
} else {
|
||||
std::process::Command::new("clear").spawn()
|
||||
};
|
||||
Ok(String::from("Screen cleared."))
|
||||
}
|
||||
|
||||
fn undo(&self) {}
|
||||
|
||||
fn redo(&self) {}
|
||||
|
||||
fn get_description(&self) -> String {
|
||||
String::from("A simple command that clears the terminal")
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
String::from("clear")
|
||||
}
|
||||
|
||||
fn get_help(&self) -> String {
|
||||
String::from("Clears the terminal")
|
||||
}
|
||||
|
||||
fn get_params(&self) -> String {
|
||||
String::from("None")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExitCommand;
|
||||
|
||||
impl Command for ExitCommand {
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
match args {
|
||||
Some(args) => {
|
||||
let exit_code = args[0].parse().map_err(|e| {
|
||||
ZenyxError::builder(ZenyxErrorKind::CommandParsing)
|
||||
.with_message("Failed to parse exit code")
|
||||
.with_source(e)
|
||||
.build()
|
||||
})?;
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
None => {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn undo(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn redo(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_description(&self) -> String {
|
||||
String::from("Gracefully exists the program")
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
String::from("exit")
|
||||
}
|
||||
|
||||
fn get_help(&self) -> String {
|
||||
String::from("Exits, probably")
|
||||
}
|
||||
|
||||
fn get_params(&self) -> String {
|
||||
String::from("None")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExecFile;
|
||||
|
||||
impl Command for ExecFile {
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
match args {
|
||||
Some(args) => {
|
||||
let file_path = PathBuf::from_str(&args[0]).map_err(|e| {
|
||||
ZenyxError::builder(ZenyxErrorKind::CommandParsing)
|
||||
.with_message("Invalid file path")
|
||||
.with_source(e)
|
||||
.build()
|
||||
})?;
|
||||
if file_path.extension().is_some() && file_path.extension().unwrap() != "zensh" {
|
||||
Err(ZenyxError::builder(ZenyxErrorKind::CommandParsing)
|
||||
.with_message("Selected file was not a zensh file")
|
||||
.build())
|
||||
} else {
|
||||
let mut script_output = String::new();
|
||||
match self.evaluate_script_heap_based(file_path) {
|
||||
Ok(commands_to_execute) => {
|
||||
for (cmd_name, cmd_args) in commands_to_execute {
|
||||
match COMMAND_MANAGER.read().execute(&cmd_name, cmd_args) {
|
||||
Ok(output) => script_output.push_str(&output),
|
||||
Err(e) => {
|
||||
return Err(ZenyxError::builder(
|
||||
ZenyxErrorKind::CommandExecution,
|
||||
)
|
||||
.with_message(format!(
|
||||
"Error executing command '{}' in script: {}",
|
||||
cmd_name, e
|
||||
))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(script_output)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(ZenyxError::builder(ZenyxErrorKind::CommandParsing)
|
||||
.with_message("Not enough arguments")
|
||||
.build()),
|
||||
}
|
||||
}
|
||||
|
||||
fn undo(&self) {}
|
||||
|
||||
fn redo(&self) {}
|
||||
|
||||
fn get_description(&self) -> String {
|
||||
String::from("Executes a file path")
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
String::from("exec")
|
||||
}
|
||||
|
||||
fn get_help(&self) -> String {
|
||||
String::from("this will read the contents of a .zensh file, evaluate it, and run its input")
|
||||
}
|
||||
|
||||
fn get_params(&self) -> String {
|
||||
String::from("1: File path")
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecFile {
|
||||
const MAX_RECURSION_DEPTH: usize = 100; // Increased for heap-based approach
|
||||
|
||||
fn evaluate_script_heap_based(
|
||||
&self,
|
||||
initial_file_path: PathBuf,
|
||||
) -> Result<Vec<(String, Option<Vec<String>>)>, ZenyxError> {
|
||||
let mut command_queue: VecDeque<(PathBuf, usize)> = VecDeque::new();
|
||||
let mut collected_commands = Vec::new();
|
||||
command_queue.push_back((initial_file_path, 0));
|
||||
|
||||
while let Some((current_file_path, current_depth)) = command_queue.pop_front() {
|
||||
if current_depth > Self::MAX_RECURSION_DEPTH {
|
||||
return Err(ZenyxError::builder(ZenyxErrorKind::CommandExecution)
|
||||
.with_message(format!(
|
||||
"Recursion limit of {} exceeded while executing script.",
|
||||
Self::MAX_RECURSION_DEPTH
|
||||
))
|
||||
.build());
|
||||
}
|
||||
|
||||
let zscript_result = fs::read_to_string(¤t_file_path).map_err(|e| {
|
||||
ZenyxError::builder(ZenyxErrorKind::Io)
|
||||
.with_message(format!("Failed to read file: {}", current_file_path.display()))
|
||||
.with_source(e)
|
||||
.build()
|
||||
});
|
||||
|
||||
match zscript_result {
|
||||
Ok(zscript) => {
|
||||
let commands: Vec<&str> = zscript.split(|c| c == ';' || c == '\n').collect();
|
||||
for command_str in commands {
|
||||
let command_str = command_str.trim();
|
||||
if command_str.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tokens = tokenize(command_str);
|
||||
if tokens.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cmd_name = &tokens[0];
|
||||
let args: Option<Vec<String>> = if tokens.len() > 1 {
|
||||
Some(tokens[1..].iter().map(|s| s.to_string()).collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if cmd_name.to_lowercase() == "exec" {
|
||||
if let Some(ref file_args) = args {
|
||||
if let Some(nested_file_path_str) = file_args.first() {
|
||||
let nested_file_path =
|
||||
PathBuf::from_str(nested_file_path_str).map_err(|e| {
|
||||
ZenyxError::builder(ZenyxErrorKind::CommandParsing)
|
||||
.with_message("Invalid file path in nested exec command")
|
||||
.with_source(e)
|
||||
.build()
|
||||
})?;
|
||||
if nested_file_path.extension().is_some()
|
||||
&& nested_file_path.extension().unwrap() != "zensh"
|
||||
{
|
||||
return Err(ZenyxError::builder(
|
||||
ZenyxErrorKind::CommandParsing,
|
||||
)
|
||||
.with_message("Nested exec file was not a zensh file")
|
||||
.build());
|
||||
}
|
||||
command_queue.push_back((nested_file_path, current_depth + 1));
|
||||
} else {
|
||||
return Err(ZenyxError::builder(
|
||||
ZenyxErrorKind::CommandParsing,
|
||||
)
|
||||
.with_message("Not enough arguments for nested exec command")
|
||||
.build());
|
||||
}
|
||||
} else {
|
||||
return Err(ZenyxError::builder(
|
||||
ZenyxErrorKind::CommandParsing,
|
||||
)
|
||||
.with_message("Not enough arguments for nested exec command")
|
||||
.build());
|
||||
}
|
||||
} else {
|
||||
collected_commands.push((cmd_name.to_owned(), args));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(collected_commands)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CounterCommand {
|
||||
counter: RwLock<u32>,
|
||||
}
|
||||
|
||||
impl Command for CounterCommand {
|
||||
fn execute(&self, _args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
let mut count = self.counter.write();
|
||||
*count += 1;
|
||||
Ok(format!(
|
||||
"CounterCommand executed. Current count: {}",
|
||||
*count
|
||||
))
|
||||
}
|
||||
|
||||
fn undo(&self) {
|
||||
println!("Undo CounterCommand.");
|
||||
}
|
||||
|
||||
fn redo(&self) {
|
||||
println!("Redo CounterCommand.");
|
||||
}
|
||||
|
||||
fn get_description(&self) -> String {
|
||||
String::from("counter")
|
||||
}
|
||||
|
||||
fn get_help(&self) -> String {
|
||||
String::from("Increments a counter every time it's executed.")
|
||||
}
|
||||
|
||||
fn get_params(&self) -> String {
|
||||
String::from("No parameters for CounterCommand.")
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
String::from("count")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PanicCommmand;
|
||||
impl Command for PanicCommmand {
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
if let Some(args) = args {
|
||||
let panic_msg = &args[0];
|
||||
panic!("{}", panic_msg);
|
||||
} else {
|
||||
let option: Option<i32> = None;
|
||||
println!("Unwrapping None: {}", option.unwrap());
|
||||
panic!("Panic command was called");
|
||||
}
|
||||
}
|
||||
|
||||
fn undo(&self) {}
|
||||
|
||||
fn redo(&self) {}
|
||||
|
||||
fn get_description(&self) -> String {
|
||||
String::from("causes a panic with your provided message")
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
String::from("panic")
|
||||
}
|
||||
|
||||
fn get_help(&self) -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
fn get_params(&self) -> String {
|
||||
String::from("optional: panic msg")
|
||||
}
|
||||
}
|
||||
|
||||
fn eval(input: String) -> Result<Vec<(String, Option<Vec<String>>)>, ZenyxError> {
|
||||
if input.trim().is_empty() {
|
||||
return Err(ZenyxError::builder(ZenyxErrorKind::CommandParsing)
|
||||
.with_message("Input was empty")
|
||||
.build());
|
||||
}
|
||||
let commands: Vec<&str> = input.split(|c| c == ';' || c == '\n').collect();
|
||||
let mut evaluted = vec![];
|
||||
|
||||
for command in commands {
|
||||
let command = command.trim();
|
||||
if command.is_empty() {
|
||||
println!("Empty command, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
let tokens = tokenize(command);
|
||||
if tokens.is_empty() {
|
||||
println!("Empty command, skipping.");
|
||||
continue;
|
||||
}
|
||||
let cmd_name = &tokens[0];
|
||||
let args: Option<Vec<String>> = if tokens.len() > 1 {
|
||||
Some(tokens[1..].iter().map(|s| s.to_string()).collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
evaluted.push((cmd_name.to_owned(), args));
|
||||
}
|
||||
Ok(evaluted)
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use ahash::AHashMap;
|
||||
use colored::Colorize;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::error::{ZenyxError, ZenyxErrorKind};
|
||||
|
||||
pub static COMMAND_MANAGER: LazyLock<RwLock<CommandManager>> =
|
||||
LazyLock::new(|| RwLock::new(CommandManager::init()));
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! commands {
|
||||
[$($command:ty),*] => [
|
||||
$(
|
||||
{
|
||||
let mut manager = $crate::core::repl::handler::COMMAND_MANAGER.write();
|
||||
manager.add_command(Box::new(<$command>::default()));
|
||||
}
|
||||
)*
|
||||
];
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! alias {
|
||||
($($alias:expr => $command:expr),*) => {
|
||||
$(
|
||||
{
|
||||
let mut manager = $crate::COMMAND_MANAGER.write();
|
||||
manager.add_alias($alias, $command);
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
fn hamming_distance(a: &str, b: &str) -> Option<usize> {
|
||||
if a.len() != b.len() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
a.chars()
|
||||
.zip(b.chars())
|
||||
.filter(|(char_a, char_b)| char_a != char_b)
|
||||
.count(),
|
||||
)
|
||||
}
|
||||
|
||||
fn edit_distance(a: &str, b: &str) -> usize {
|
||||
let m = a.len();
|
||||
let n = b.len();
|
||||
|
||||
let mut dp = vec![vec![0; n + 1]; m + 1];
|
||||
|
||||
for i in 0..=m {
|
||||
for j in 0..=n {
|
||||
if i == 0 {
|
||||
dp[i][j] = j;
|
||||
} else if j == 0 {
|
||||
dp[i][j] = i;
|
||||
} else if a.chars().nth(i - 1) == b.chars().nth(j - 1) {
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
dp[i][j] = 1 + dp[i - 1][j - 1].min(dp[i - 1][j]).min(dp[i][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dp[m][n]
|
||||
}
|
||||
|
||||
fn check_similarity(target: &str) -> Option<String> {
|
||||
let max_hamming_distance: usize = 2;
|
||||
let max_edit_distance: usize = 2;
|
||||
let mut best_match: Option<String> = None;
|
||||
let mut best_distance = usize::MAX;
|
||||
|
||||
for (cmd_name, _) in COMMAND_MANAGER.read().get_commands() {
|
||||
if let Some(hamming_dist) = hamming_distance(target, cmd_name) {
|
||||
if hamming_dist <= max_hamming_distance && hamming_dist < best_distance {
|
||||
best_distance = hamming_dist;
|
||||
best_match = Some(String::from(cmd_name));
|
||||
}
|
||||
} else {
|
||||
let edit_dist = edit_distance(target, cmd_name);
|
||||
if edit_dist <= max_edit_distance && edit_dist < best_distance {
|
||||
best_distance = edit_dist;
|
||||
best_match = Some(String::from(cmd_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best_match
|
||||
}
|
||||
|
||||
pub struct CommandManager {
|
||||
pub commands: AHashMap<String, Box<dyn Command>>,
|
||||
pub aliases: AHashMap<String, String>,
|
||||
}
|
||||
|
||||
impl CommandManager {
|
||||
pub fn init() -> CommandManager {
|
||||
CommandManager {
|
||||
commands: AHashMap::new(),
|
||||
aliases: AHashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_commands(&self) -> std::collections::hash_map::Iter<'_, String, Box<dyn Command>> {
|
||||
self.commands.iter()
|
||||
}
|
||||
|
||||
pub fn execute_command(
|
||||
&self,
|
||||
command: &str,
|
||||
args: Option<Vec<String>>,
|
||||
) -> Result<String, ZenyxError> {
|
||||
if let Some(command) = self.commands.get(command) {
|
||||
let output = command.execute(args)?;
|
||||
Ok(output)
|
||||
} else {
|
||||
let corrected_cmd = check_similarity(command);
|
||||
if let Some(corrected_cmd) = corrected_cmd {
|
||||
println!(
|
||||
"Command: {} was not found. Did you mean {}?",
|
||||
command.red().bold(),
|
||||
corrected_cmd.green().bold().italic()
|
||||
);
|
||||
}
|
||||
Err(ZenyxError::builder(ZenyxErrorKind::CommandExecution)
|
||||
.with_message(format!("Command '{}' not found.", command))
|
||||
.build())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(&self, command: &str, args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
match self.aliases.get(command) {
|
||||
Some(command) => self.execute(command, args),
|
||||
None => {
|
||||
let output = self.execute_command(command, args)?;
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_command(&mut self, command: Box<dyn Command>) {
|
||||
self.commands
|
||||
.insert(command.get_name().to_lowercase(), command);
|
||||
}
|
||||
|
||||
pub fn add_alias(&mut self, alias: &str, command: &str) {
|
||||
self.aliases.insert(
|
||||
alias.to_string().to_lowercase(),
|
||||
command.to_string().to_lowercase(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Command: Send + Sync {
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<String, ZenyxError>;
|
||||
fn undo(&self);
|
||||
fn redo(&self);
|
||||
fn get_description(&self) -> String;
|
||||
fn get_name(&self) -> String;
|
||||
fn get_help(&self) -> String;
|
||||
fn get_params(&self) -> String;
|
||||
}
|
|
@ -1,240 +0,0 @@
|
|||
use std::{
|
||||
borrow::Cow::{self, Borrowed, Owned},
|
||||
sync::Arc,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use colored::Colorize;
|
||||
use parking_lot::Mutex;
|
||||
use rustyline::{
|
||||
Cmd, Completer, ConditionalEventHandler, Editor, Event, EventContext, EventHandler, Helper,
|
||||
Hinter, KeyEvent, RepeatCount, Validator, completion::Completer, error::ReadlineError,
|
||||
highlight::Highlighter, hint::HistoryHinter, history::DefaultHistory,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use super::handler::COMMAND_MANAGER;
|
||||
use crate::error::{Result, ZenyxError, ZenyxErrorKind};
|
||||
#[derive(Default)]
|
||||
struct CommandCompleter;
|
||||
|
||||
impl Completer for CommandCompleter {
|
||||
type Candidate = String;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
_ctx: &rustyline::Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
let binding = COMMAND_MANAGER.read();
|
||||
let binding = binding.get_commands();
|
||||
let filtered_commands: Vec<_> = binding
|
||||
.filter(|(command, _)| command.starts_with(line))
|
||||
.collect();
|
||||
|
||||
let completions: Vec<String> = filtered_commands
|
||||
.iter()
|
||||
.filter(|(command, _)| command.starts_with(&line[..pos]))
|
||||
.map(|(command, _)| command[pos..].to_string())
|
||||
.collect();
|
||||
println!("{:#?}", completions);
|
||||
Ok((pos, completions))
|
||||
}
|
||||
}
|
||||
#[derive(Completer, Helper, Hinter, Validator)]
|
||||
struct MyHelper {
|
||||
#[rustyline(Hinter)]
|
||||
hinter: HistoryHinter,
|
||||
#[rustyline(Completer)]
|
||||
completer: CommandCompleter,
|
||||
}
|
||||
|
||||
impl Highlighter for MyHelper {
|
||||
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
|
||||
&'s self,
|
||||
prompt: &'p str,
|
||||
default: bool,
|
||||
) -> Cow<'b, str> {
|
||||
if default {
|
||||
Owned(prompt.bright_black().bold().to_string())
|
||||
} else {
|
||||
Borrowed(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
|
||||
Owned(hint.italic().bright_black().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BacktickEventHandler {
|
||||
toggle_state: Arc<Mutex<bool>>, // Tracks whether logging is enabled or disabled
|
||||
}
|
||||
|
||||
impl ConditionalEventHandler for BacktickEventHandler {
|
||||
fn handle(&self, evt: &Event, _: RepeatCount, _: bool, _: &EventContext) -> Option<Cmd> {
|
||||
if let Some(k) = evt.get(0) {
|
||||
if *k == KeyEvent::from('`') {
|
||||
let mut state = self.toggle_state.lock();
|
||||
println!(
|
||||
"Stdout Logging: {}",
|
||||
if *state { "ON".green() } else { "OFF".red() }
|
||||
);
|
||||
if *state {
|
||||
// LOGGER.write_to_stdout();
|
||||
} else {
|
||||
// LOGGER.write_to_file("z.log");
|
||||
}
|
||||
*state = !*state;
|
||||
Some(Cmd::Noop)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tokenize(command: &str) -> Vec<String> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut current_token = String::new();
|
||||
let mut inside_string = false;
|
||||
|
||||
for char in command.chars() {
|
||||
if char == '"' || char == '\'' {
|
||||
inside_string = !inside_string;
|
||||
} else if char.is_whitespace() && !inside_string {
|
||||
if !current_token.is_empty() {
|
||||
tokens.push(current_token);
|
||||
current_token = String::new();
|
||||
}
|
||||
} else {
|
||||
current_token.push(char);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore the last token if it's empty. Who are we. Mojang? - Caz
|
||||
if !current_token.is_empty() {
|
||||
tokens.push(current_token);
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
pub fn parse_command(input: &str) -> Result<Vec<String>> {
|
||||
let commands = input
|
||||
.split(|c| c == ';' || c == '\n')
|
||||
.map(|slice| slice.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
Ok(commands)
|
||||
}
|
||||
|
||||
pub fn evaluate_command(input: &str) -> std::result::Result<String, ZenyxError> {
|
||||
if input.trim().is_empty() {
|
||||
let err = ZenyxError::builder(ZenyxErrorKind::CommandParsing)
|
||||
.with_message("Input was empty")
|
||||
.build();
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let commands = input
|
||||
.split(|c| c == ';' || c == '\n')
|
||||
.map(|slice| slice.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
let mut output = String::new();
|
||||
|
||||
for command in commands {
|
||||
let command = command.trim();
|
||||
if command.is_empty() {
|
||||
error!("Empty command, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
let tokens = tokenize(command);
|
||||
if tokens.is_empty() {
|
||||
error!("Empty command, skipping.");
|
||||
continue;
|
||||
}
|
||||
let cmd_name = &tokens[0];
|
||||
let args: Option<Vec<String>> = if tokens.len() > 1 {
|
||||
Some(tokens[1..].iter().map(|s| s.to_string()).collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match COMMAND_MANAGER.read().execute(cmd_name, args) {
|
||||
Ok(command_output) => output.push_str(&command_output),
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
pub fn format_time() -> String {
|
||||
let now = SystemTime::now();
|
||||
let duration = now.duration_since(UNIX_EPOCH).unwrap();
|
||||
let total_seconds = duration.as_secs();
|
||||
let nanos = duration.subsec_nanos();
|
||||
let milliseconds = nanos / 1_000_000;
|
||||
|
||||
let seconds_since_midnight_utc = total_seconds % (24 * 3600);
|
||||
let hour = (seconds_since_midnight_utc / 3600) % 24;
|
||||
let minute = (seconds_since_midnight_utc / 60) % 60;
|
||||
let second = seconds_since_midnight_utc % 60;
|
||||
|
||||
format!(
|
||||
"{:02}:{:02}:{:02}.{:03}",
|
||||
hour, minute, second, milliseconds
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn handle_repl() -> Result<()> {
|
||||
let mut rl = Editor::<MyHelper, DefaultHistory>::new()?;
|
||||
rl.set_helper(Some(MyHelper {
|
||||
hinter: HistoryHinter::new(),
|
||||
completer: CommandCompleter::default(),
|
||||
}));
|
||||
|
||||
rl.bind_sequence(
|
||||
KeyEvent::from('`'),
|
||||
EventHandler::Conditional(Box::new(BacktickEventHandler {
|
||||
toggle_state: Arc::new(Mutex::new(false)),
|
||||
})),
|
||||
);
|
||||
|
||||
if rl.load_history("history.txt").is_err() {
|
||||
debug!("No previous history.");
|
||||
}
|
||||
|
||||
loop {
|
||||
let time = format_time();
|
||||
let prompt = format!("[{}/{}] {}", time, "SHELL", ">>\t");
|
||||
|
||||
let sig = rl.readline(&prompt.bright_white());
|
||||
|
||||
match sig {
|
||||
Ok(line) => {
|
||||
rl.add_history_entry(line.as_str())?;
|
||||
match evaluate_command(line.as_str()) {
|
||||
Ok(_) => continue,
|
||||
Err(e) => error!("{e}"),
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
println!("CTRL+C received, exiting...");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(ReadlineError::Eof) => {
|
||||
println!("Error: CTRL+D pressed. Exiting...");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
use commands::{ClearCommand, CounterCommand, ExecFile, ExitCommand, HelpCommand, PanicCommmand};
|
||||
|
||||
use crate::commands;
|
||||
|
||||
pub mod commands;
|
||||
pub mod handler;
|
||||
pub mod input;
|
||||
|
||||
pub fn setup() {
|
||||
commands!(
|
||||
HelpCommand,
|
||||
ExecFile,
|
||||
ClearCommand,
|
||||
ExitCommand,
|
||||
CounterCommand,
|
||||
PanicCommmand
|
||||
);
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
use colored::Colorize;
|
||||
|
||||
pub fn print_splash() {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
r#"
|
||||
▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓ ▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓ ▓▓
|
||||
▓▓▓▓▓▓▓▓▓ ▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓
|
||||
|
||||
Version: {}
|
||||
"#,
|
||||
env!("CARGO_PKG_VERSION").green()
|
||||
)
|
||||
.bright_yellow()
|
||||
);
|
||||
}
|
|
@ -1,259 +0,0 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use colored::Colorize;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum ZenyxErrorKind {
|
||||
#[error("Surface creation failed")]
|
||||
SurfaceCreation,
|
||||
#[error("Surface configuration failed")]
|
||||
SurfaceConfiguration,
|
||||
#[error("Adapter request failed")]
|
||||
AdapterRequest,
|
||||
#[error("Device request failed")]
|
||||
DeviceRequest,
|
||||
#[error("Surface texture acquisition failed")]
|
||||
SurfaceTexture,
|
||||
#[error("Command parsing failed")]
|
||||
CommandParsing,
|
||||
#[error("Command execution failed")]
|
||||
CommandExecution,
|
||||
#[error("Font loading failed")]
|
||||
FontLoading,
|
||||
#[error("Model loading failed")]
|
||||
ModelLoading,
|
||||
#[error("IO operation failed")]
|
||||
Io,
|
||||
#[error("REPL operation failed")]
|
||||
Repl,
|
||||
#[error("Unknown error occurred")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ZenyxError {
|
||||
kind: ZenyxErrorKind,
|
||||
message: Option<String>,
|
||||
context: Option<String>,
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl ZenyxError {
|
||||
pub fn builder(kind: ZenyxErrorKind) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
message: None,
|
||||
context: None,
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &ZenyxErrorKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn with_message(mut self, message: impl Into<String>) -> Self {
|
||||
self.message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_context(mut self, context: impl Into<String>) -> Self {
|
||||
self.context = Some(context.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_source<E>(mut self, source: E) -> Self
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
self.source = Some(Box::new(source));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pretty_print(&self) {
|
||||
let mut output = String::new();
|
||||
let padding_spaces = 2;
|
||||
writeln!(
|
||||
output,
|
||||
"{} {}",
|
||||
"\nERROR:".red().bold(),
|
||||
format!("{}", self.kind).bright_white().bold()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if let Some(msg) = &self.message {
|
||||
let line_padding = " ".repeat(padding_spaces);
|
||||
writeln!(output, "{}>> {}\x1b[0m", line_padding, msg.bright_white()).unwrap();
|
||||
}
|
||||
if let Some(ctx) = &self.context {
|
||||
let line_padding = " ".repeat(padding_spaces);
|
||||
writeln!(output, "{}│\x1b[0m", line_padding.bright_white().bold()).unwrap();
|
||||
writeln!(output, "{}╰─ Note: {}\x1b[0m", line_padding, ctx).unwrap();
|
||||
}
|
||||
if let Some(source) = &self.source {
|
||||
let line_padding = " ".repeat(padding_spaces);
|
||||
writeln!(output, "{}╰─ Caused by: {}\x1b[0m", line_padding, source).unwrap();
|
||||
let mut current = source.source();
|
||||
let mut depth = 1;
|
||||
while let Some(err) = current {
|
||||
let indent = " ".repeat(padding_spaces * depth);
|
||||
writeln!(output, "{}↳ {}\x1b[0m", indent, err).unwrap();
|
||||
depth += 1;
|
||||
current = err.source();
|
||||
}
|
||||
}
|
||||
print!("{}", output);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ZenyxError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}{}{}{}",
|
||||
self.kind,
|
||||
self.message
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |msg| format!(" - {}", msg)),
|
||||
self.context
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |ctx| format!(" [{}]", ctx)),
|
||||
self.source
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |src| format!(" - caused by: {}", src))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ZenyxError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.source
|
||||
.as_ref()
|
||||
.map(|s| s.as_ref() as &(dyn std::error::Error + 'static))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ZenyxError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Self::builder(ZenyxErrorKind::Io)
|
||||
.with_message(err.to_string())
|
||||
.with_source(err)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<wgpu::CreateSurfaceError> for ZenyxError {
|
||||
fn from(err: wgpu::CreateSurfaceError) -> Self {
|
||||
Self::builder(ZenyxErrorKind::SurfaceCreation)
|
||||
.with_message("Failed to create surface")
|
||||
.with_source(err)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<wgpu::RequestDeviceError> for ZenyxError {
|
||||
fn from(err: wgpu::RequestDeviceError) -> Self {
|
||||
Self::builder(ZenyxErrorKind::DeviceRequest)
|
||||
.with_message("Failed to request device")
|
||||
.with_source(err)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rustyline::error::ReadlineError> for ZenyxError {
|
||||
fn from(err: rustyline::error::ReadlineError) -> Self {
|
||||
Self::builder(ZenyxErrorKind::Repl)
|
||||
.with_message("Readline error occurred")
|
||||
.with_source(err)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, ZenyxError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_zenyx_error_builder() {
|
||||
let error = ZenyxError::builder(ZenyxErrorKind::Io)
|
||||
.with_message("An IO error occurred")
|
||||
.with_context("Reading file")
|
||||
.build();
|
||||
|
||||
assert_eq!(error.kind, ZenyxErrorKind::Io);
|
||||
assert_eq!(error.message.as_deref(), Some("An IO error occurred"));
|
||||
assert_eq!(error.context.as_deref(), Some("Reading file"));
|
||||
assert!(error.source.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zenyx_error_with_source() {
|
||||
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
|
||||
let error = ZenyxError::builder(ZenyxErrorKind::Io)
|
||||
.with_message("An IO error occurred")
|
||||
.with_source(io_error)
|
||||
.build();
|
||||
|
||||
assert_eq!(error.kind, ZenyxErrorKind::Io);
|
||||
assert_eq!(error.message.as_deref(), Some("An IO error occurred"));
|
||||
assert!(error.source.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_io_error() {
|
||||
let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
|
||||
let error: ZenyxError = io_error.into();
|
||||
|
||||
assert_eq!(error.kind, ZenyxErrorKind::Io);
|
||||
assert_eq!(error.message.as_deref(), Some("Access denied"));
|
||||
assert!(error.source.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_rustyline_error() {
|
||||
let readline_error = rustyline::error::ReadlineError::Interrupted;
|
||||
let error: ZenyxError = readline_error.into();
|
||||
|
||||
assert_eq!(error.kind, ZenyxErrorKind::Repl);
|
||||
assert_eq!(error.message.as_deref(), Some("Readline error occurred"));
|
||||
assert!(error.source.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_print() {
|
||||
let readline_error = rustyline::error::ReadlineError::Interrupted;
|
||||
let error: ZenyxError = readline_error.into();
|
||||
|
||||
println!("{error}");
|
||||
}
|
||||
#[test]
|
||||
fn test_pretty_print() {
|
||||
let readline_error = rustyline::error::ReadlineError::Interrupted;
|
||||
let error: ZenyxError = readline_error.into();
|
||||
|
||||
error.pretty_print();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_source_chain() {
|
||||
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
|
||||
let error = ZenyxError::builder(ZenyxErrorKind::Io)
|
||||
.with_message("An IO error occurred")
|
||||
.with_source(io_error)
|
||||
.build();
|
||||
|
||||
let mut source = std::error::Error::source(&error);
|
||||
assert!(source.is_some());
|
||||
assert_eq!(source.unwrap().to_string(), "File not found");
|
||||
|
||||
source = source.unwrap().source();
|
||||
assert!(source.is_none());
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
use core::{panic::set_panic_hook, repl::setup, splash};
|
||||
use std::{fs::OpenOptions, io::BufWriter};
|
||||
|
||||
use colored::Colorize;
|
||||
use tokio::runtime;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
#[allow(unused_imports)]
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing_subscriber::{Registry, fmt, layer::SubscriberExt};
|
||||
use winit::event_loop::EventLoop;
|
||||
pub mod cli;
|
||||
pub mod core;
|
||||
pub mod error;
|
||||
pub mod metadata;
|
||||
|
||||
fn init_logger() {
|
||||
let stdout_layer = fmt::layer()
|
||||
.with_level(true)
|
||||
.compact()
|
||||
.pretty()
|
||||
.log_internal_errors(false)
|
||||
.without_time()
|
||||
.with_thread_names(true);
|
||||
|
||||
let file_layer = fmt::layer()
|
||||
.with_level(true)
|
||||
.compact()
|
||||
.with_ansi(false)
|
||||
.log_internal_errors(false)
|
||||
.without_time()
|
||||
.with_writer(|| {
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true)
|
||||
.open("zenyx.log")
|
||||
.unwrap_or_else(|_| {
|
||||
eprintln!("Couldn't open log file, creating a new one.");
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open("zenyx.log")
|
||||
.expect("Failed to create log file")
|
||||
});
|
||||
BufWriter::new(file)
|
||||
})
|
||||
.with_thread_names(true);
|
||||
|
||||
let subscriber = Registry::default()
|
||||
.with(LevelFilter::DEBUG)
|
||||
.with(stdout_layer)
|
||||
.with(file_layer);
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).expect("Failed to set global subscriber");
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
init_logger();
|
||||
cli::parse();
|
||||
let sysinfo = crate::metadata::SystemMetadata::current();
|
||||
|
||||
// set_panic_hook();
|
||||
setup();
|
||||
|
||||
splash::print_splash();
|
||||
if !cfg!(debug_assertions) {
|
||||
info!("{}", "Debug mode disabled".bright_blue());
|
||||
set_panic_hook();
|
||||
} else {
|
||||
println!("{}", sysinfo.verbose_summary());
|
||||
}
|
||||
|
||||
info!("Type 'help' for a list of commands.");
|
||||
let repl_thread = std::thread::spawn(|| {
|
||||
let rt = match runtime::Builder::new_current_thread().enable_all().build() {
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
error!("A fatal error has occured: {e}");
|
||||
std::process::exit(1)
|
||||
}
|
||||
};
|
||||
rt.block_on(core::repl::input::handle_repl())
|
||||
});
|
||||
|
||||
match EventLoop::new() {
|
||||
Ok(event_loop) => {
|
||||
core::render::init_renderer(event_loop);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e}")
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = repl_thread.join() {
|
||||
error!("REPL thread panicked");
|
||||
}
|
||||
}
|
|
@ -1,816 +0,0 @@
|
|||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::{env, error::Error, path::PathBuf, thread};
|
||||
|
||||
use native_dialog::{MessageDialog, MessageType};
|
||||
use parking_lot::Once;
|
||||
use raw_cpuid::CpuId;
|
||||
use sysinfo::{CpuRefreshKind, RefreshKind, System};
|
||||
use tracing::error;
|
||||
use wgpu::DeviceType;
|
||||
|
||||
mod build_info {
|
||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||
}
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
pub fn set_panic_hook() {
|
||||
INIT.call_once(|| {
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
if let Err(e) = process_panic(info) {
|
||||
eprintln!("Error in panic hook: {}", e);
|
||||
default_hook(info);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box<dyn Error>> {
|
||||
use std::io::Write;
|
||||
|
||||
use colored::Colorize;
|
||||
|
||||
let log_dir = PathBuf::from_str("./").expect("wtf, The current directory no longer exists?");
|
||||
if !log_dir.exists() {
|
||||
std::fs::create_dir_all(&log_dir)?;
|
||||
}
|
||||
let log_path = log_dir.join("panic.log");
|
||||
|
||||
let mut file = std::fs::File::create(&log_path)?;
|
||||
let payload = info.payload();
|
||||
let payload_str = if let Some(s) = payload.downcast_ref::<&str>() {
|
||||
*s
|
||||
} else if let Some(s) = payload.downcast_ref::<String>() {
|
||||
s
|
||||
} else {
|
||||
"<non-string panic payload>"
|
||||
};
|
||||
|
||||
writeln!(file, "Panic Occurred: {}", payload_str)?;
|
||||
|
||||
if let Some(location) = info.location() {
|
||||
writeln!(file, "Panic Location: {}", location)?;
|
||||
}
|
||||
|
||||
writeln!(file, "{}", capture_backtrace().sanitize_path())?;
|
||||
|
||||
// Add more contextual information
|
||||
writeln!(file, "\n--- Additional Information ---")?;
|
||||
|
||||
// Rust Version
|
||||
if let Ok(rust_version) = rust_version() {
|
||||
writeln!(file, "Rust Version: {}", rust_version)?;
|
||||
}
|
||||
|
||||
// Command-line Arguments
|
||||
writeln!(file, "Command-line Arguments:")?;
|
||||
for arg in env::args() {
|
||||
writeln!(file, " {}", arg)?;
|
||||
}
|
||||
|
||||
// Environment Variables (consider filtering sensitive ones)
|
||||
writeln!(file, "\nEnvironment Variables (selected):")?;
|
||||
let interesting_env_vars = ["PATH", "RUST_VERSION", "CARGO_TARGET_DIR", "HOME", "USER"];
|
||||
for (key, value) in env::vars() {
|
||||
if interesting_env_vars.contains(&key.as_str()) {
|
||||
writeln!(file, " {}: {}", key, value)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Current Working Directory
|
||||
if let Ok(cwd) = env::current_dir() {
|
||||
writeln!(file, "\nCurrent Working Directory: {}", cwd.display())?;
|
||||
}
|
||||
|
||||
// Thread Information
|
||||
if let Some(thread) = thread::current().name() {
|
||||
writeln!(file, "\nThread Name: {}", thread)?;
|
||||
} else {
|
||||
writeln!(file, "\nThread ID: {:?}", thread::current().id())?;
|
||||
}
|
||||
|
||||
let panic_msg = format!(
|
||||
r#"Zenyx had a problem and crashed. To help us diagnose the problem you can send us a crash report.
|
||||
|
||||
We have generated a detailed report file at '{}'. Submit an issue or email with the subject of 'Zenyx Crash Report' and include the report as an attachment.
|
||||
|
||||
To submit the crash report:
|
||||
https://codeberg.org/Caznix/Zenyx/issues
|
||||
We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports.
|
||||
|
||||
Thank you kindly!"#,
|
||||
log_path.display()
|
||||
);
|
||||
|
||||
let final_msg = format!(
|
||||
r#"{}
|
||||
|
||||
For future reference, the error summary is as follows:
|
||||
{}
|
||||
|
||||
More details can be found in the crash report file."#,
|
||||
panic_msg, payload_str
|
||||
);
|
||||
|
||||
println!("{}", final_msg.red().bold());
|
||||
|
||||
if let Err(e) = MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
.set_title("A fatal error in Zenyx has occurred")
|
||||
.set_text(&final_msg)
|
||||
.show_confirm()
|
||||
{
|
||||
error!("Failed to show message dialog: {e}")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rust_version() -> Result<String, Box<dyn Error>> {
|
||||
let version = env!("CARGO_PKG_RUST_VERSION");
|
||||
Ok(version.to_string())
|
||||
}
|
||||
|
||||
fn capture_backtrace() -> String {
|
||||
let mut backtrace = String::new();
|
||||
let sysinfo = SystemMetadata::current();
|
||||
backtrace.push_str(&format!(
|
||||
"--- System Information ---\n{}\n",
|
||||
sysinfo.verbose_summary()
|
||||
));
|
||||
|
||||
let trace = std::backtrace::Backtrace::force_capture();
|
||||
let message = "\n--- Backtrace ---\n\n".to_string();
|
||||
backtrace.push_str(&message);
|
||||
backtrace.push_str(&format!("{trace:#}"));
|
||||
|
||||
backtrace
|
||||
}
|
||||
|
||||
trait Sanitize {
|
||||
fn sanitize_path(&self) -> String;
|
||||
}
|
||||
|
||||
impl Sanitize for str {
|
||||
fn sanitize_path(&self) -> String {
|
||||
let prefixes = ["/home/", "/Users/", "\\Users\\", "/opt/home/"];
|
||||
let mut result = String::from(self);
|
||||
|
||||
for prefix in prefixes {
|
||||
if let Some(start_index) = result.find(prefix) {
|
||||
let start_of_user = start_index + prefix.len();
|
||||
let mut end_of_user = result[start_of_user..]
|
||||
.find(|c| c == '/' || c == '\\')
|
||||
.map(|i| start_of_user + i)
|
||||
.unwrap_or(result.len());
|
||||
if end_of_user == start_of_user && start_of_user < result.len() {
|
||||
end_of_user = result.len();
|
||||
}
|
||||
result.replace_range(start_of_user..end_of_user, "<USER>");
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Memory {
|
||||
bytes: u64,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
pub const fn from_bytes(bytes: u64) -> Self {
|
||||
Self { bytes }
|
||||
}
|
||||
|
||||
pub const fn from_kb(kb: u64) -> Self {
|
||||
Self { bytes: kb * 1024 }
|
||||
}
|
||||
|
||||
pub const fn from_mb(mb: u64) -> Self {
|
||||
Self {
|
||||
bytes: mb * 1024 * 1024,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_gb(gb: u64) -> Self {
|
||||
Self {
|
||||
bytes: gb * 1024 * 1024 * 1024,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_bytes(&self) -> u64 {
|
||||
self.bytes
|
||||
}
|
||||
|
||||
pub const fn as_kb(&self) -> u64 {
|
||||
self.bytes / 1024
|
||||
}
|
||||
|
||||
pub const fn as_mb(&self) -> u64 {
|
||||
self.bytes / (1024 * 1024)
|
||||
}
|
||||
|
||||
pub const fn as_gb(&self) -> u64 {
|
||||
self.bytes / (1024 * 1024 * 1024)
|
||||
}
|
||||
|
||||
pub fn format_human(&self) -> String {
|
||||
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
||||
let mut size = self.bytes as f64;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
|
||||
size /= 1024.0;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
format!("{:.2} {}", size, UNITS[unit_index])
|
||||
}
|
||||
|
||||
pub fn verbose_info(&self) -> String {
|
||||
format!(
|
||||
"{} ({} bytes, {} KB, {} MB, {} GB)",
|
||||
self.format_human(),
|
||||
self.as_bytes(),
|
||||
self.as_kb(),
|
||||
self.as_mb(),
|
||||
self.as_gb()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Memory {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.format_human())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CPUBrand {
|
||||
Intel,
|
||||
AMD,
|
||||
SnapDragon,
|
||||
Apple,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<&str> for CPUBrand {
|
||||
fn from(s: &str) -> Self {
|
||||
let sl = s.to_lowercase();
|
||||
if sl.contains("intel") {
|
||||
Self::Intel
|
||||
} else if sl.contains("amd") {
|
||||
Self::AMD
|
||||
} else if sl.contains("snapdragon") {
|
||||
Self::SnapDragon
|
||||
} else if sl.contains("apple") {
|
||||
Self::Apple
|
||||
} else {
|
||||
Self::Other(s.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CPUBrand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Intel => write!(f, "Intel"),
|
||||
Self::AMD => write!(f, "AMD"),
|
||||
Self::SnapDragon => write!(f, "SnapDragon"),
|
||||
Self::Apple => write!(f, "Apple"),
|
||||
Self::Other(s) => write!(f, "{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CPUArch {
|
||||
X86,
|
||||
X86_64,
|
||||
AArch64,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl FromStr for CPUArch {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"x86" => Self::X86,
|
||||
"x86_64" => Self::X86_64,
|
||||
"arm" => Self::AArch64,
|
||||
"aarch64" => Self::AArch64,
|
||||
_ => Self::Other(s.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CPUArch {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::X86 => write!(f, "x86"),
|
||||
Self::X86_64 => write!(f, "x86_64"),
|
||||
Self::AArch64 => write!(f, "AArch64"),
|
||||
Self::Other(s) => write!(f, "{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ClockSpeed(pub u32);
|
||||
|
||||
impl ClockSpeed {
|
||||
pub fn as_mhz(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn as_ghz(&self) -> f32 {
|
||||
self.0 as f32 / 1000.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ClockSpeed {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:.2} GHz ({} MHz)", self.as_ghz(), self.as_mhz())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CPU {
|
||||
pub brand: CPUBrand,
|
||||
pub arch: CPUArch,
|
||||
pub name: String,
|
||||
pub vendor_id: String,
|
||||
pub physical_cores: Option<u8>,
|
||||
pub logical_cores: Option<u8>,
|
||||
pub max_clock_speed: ClockSpeed,
|
||||
pub current_clock_speed: ClockSpeed,
|
||||
pub l1_cache: Option<Memory>,
|
||||
pub l2_cache: Option<Memory>,
|
||||
pub l3_cache: Option<Memory>,
|
||||
}
|
||||
|
||||
impl CPU {
|
||||
pub fn current() -> Self {
|
||||
let mut sys = System::new_with_specifics(
|
||||
RefreshKind::default().with_cpu(CpuRefreshKind::everything()),
|
||||
);
|
||||
sys.refresh_cpu_all();
|
||||
|
||||
let cpu_opt = sys.cpus().first();
|
||||
|
||||
let brand = cpu_opt
|
||||
.map(|cpu| cpu.brand().into())
|
||||
.unwrap_or(CPUBrand::Other("unknown".into()));
|
||||
let name = cpu_opt
|
||||
.map(|cpu| cpu.name().to_string())
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
let vendor_id = cpu_opt
|
||||
.map(|cpu| cpu.vendor_id().to_string())
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
let max_clock_speed = cpu_opt
|
||||
.map(|cpu| ClockSpeed(cpu.frequency() as u32))
|
||||
.unwrap_or(ClockSpeed(0));
|
||||
let current_clock_speed = max_clock_speed;
|
||||
|
||||
let logical_cores = cpu_opt.map(|_| sys.cpus().len() as u8);
|
||||
let physical_cores = System::physical_core_count().map(|pc| pc as u8);
|
||||
|
||||
let mut l1_cache = None;
|
||||
let mut l2_cache = None;
|
||||
let mut l3_cache = None;
|
||||
|
||||
#[cfg(any(
|
||||
all(target_arch = "x86", not(target_env = "sgx"), target_feature = "sse"),
|
||||
all(target_arch = "x86_64", not(target_env = "sgx"))
|
||||
))]
|
||||
{
|
||||
let cpuid = CpuId::new();
|
||||
if let Some(iter) = cpuid.get_cache_parameters() {
|
||||
for cache in iter {
|
||||
match cache.level() {
|
||||
1 => {
|
||||
let size = cache.physical_line_partitions()
|
||||
* cache.coherency_line_size()
|
||||
* cache.associativity();
|
||||
if size > 0 {
|
||||
l1_cache = Some(Memory::from_bytes(size.try_into().unwrap()));
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
let size = cache.physical_line_partitions()
|
||||
* cache.coherency_line_size()
|
||||
* cache.associativity();
|
||||
if size > 0 {
|
||||
l2_cache = Some(Memory::from_bytes(size.try_into().unwrap()));
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
let size = (cache.physical_line_partitions() as u64)
|
||||
* (cache.coherency_line_size() as u64)
|
||||
* (cache.associativity() as u64);
|
||||
if size > 0 {
|
||||
l3_cache = Some(Memory::from_bytes(size));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
brand,
|
||||
arch: env::consts::ARCH
|
||||
.parse()
|
||||
.unwrap_or(CPUArch::Other("unknown".into())),
|
||||
name,
|
||||
vendor_id,
|
||||
physical_cores,
|
||||
logical_cores,
|
||||
max_clock_speed,
|
||||
current_clock_speed,
|
||||
l1_cache,
|
||||
l2_cache,
|
||||
l3_cache,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_intel(&self) -> bool {
|
||||
matches!(self.brand, CPUBrand::Intel)
|
||||
}
|
||||
|
||||
pub fn is_amd(&self) -> bool {
|
||||
matches!(self.brand, CPUBrand::AMD)
|
||||
}
|
||||
|
||||
pub fn is_arm(&self) -> bool {
|
||||
matches!(self.brand, CPUBrand::Intel)
|
||||
}
|
||||
|
||||
pub fn is_high_clock(&self) -> bool {
|
||||
self.max_clock_speed.0 > 3000
|
||||
}
|
||||
|
||||
pub fn verbose_info(&self) -> String {
|
||||
format!(
|
||||
"CPU Information:\n\
|
||||
- Brand: {}\n\
|
||||
- Architecture: {}\n\
|
||||
- Name: {}\n\
|
||||
- Vendor ID: {}\n\
|
||||
- Cores: {} physical, {} logical\n\
|
||||
- Clock Speed: {} (max), {} (current)\n\
|
||||
- Cache: L1 {}, L2 {}, L3 {}",
|
||||
self.brand,
|
||||
self.arch,
|
||||
self.name,
|
||||
self.vendor_id,
|
||||
self.physical_cores
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "unknown".into()),
|
||||
self.logical_cores
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "unknown".into()),
|
||||
self.max_clock_speed,
|
||||
self.current_clock_speed,
|
||||
self.l1_cache
|
||||
.map(|c| c.format_human())
|
||||
.unwrap_or_else(|| "unknown".into()),
|
||||
self.l2_cache
|
||||
.map(|c| c.format_human())
|
||||
.unwrap_or_else(|| "unknown".into()),
|
||||
self.l3_cache
|
||||
.map(|c| c.format_human())
|
||||
.unwrap_or_else(|| "unknown".into()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum GPUBrand {
|
||||
Nvidia,
|
||||
AMD,
|
||||
Intel,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<&str> for GPUBrand {
|
||||
fn from(s: &str) -> Self {
|
||||
let sl = s.to_lowercase();
|
||||
if sl.contains("nvidia") || sl.contains("geforce") {
|
||||
Self::Nvidia
|
||||
} else if sl.contains("amd") || sl.contains("radeon") {
|
||||
Self::AMD
|
||||
} else if sl.contains("intel") {
|
||||
Self::Intel
|
||||
} else {
|
||||
Self::Other(s.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for GPUBrand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Nvidia => write!(f, "NVIDIA"),
|
||||
Self::AMD => write!(f, "AMD"),
|
||||
Self::Intel => write!(f, "Intel"),
|
||||
Self::Other(s) => write!(f, "{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GPU {
|
||||
pub brand: GPUBrand,
|
||||
pub name: String,
|
||||
pub device_type: DeviceType,
|
||||
pub vram: Memory,
|
||||
pub driver_version: Option<String>,
|
||||
}
|
||||
|
||||
impl GPU {
|
||||
pub fn current() -> Vec<Self> {
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::all(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
instance
|
||||
.enumerate_adapters(wgpu::Backends::all())
|
||||
.iter()
|
||||
.map(|adapter| {
|
||||
let info = adapter.get_info();
|
||||
GPU {
|
||||
brand: info.name.as_str().into(),
|
||||
name: info.name.to_string(),
|
||||
device_type: info.device_type,
|
||||
vram: Memory::from_bytes(0),
|
||||
driver_version: Some(info.driver.to_string()),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn is_integrated(&self) -> bool {
|
||||
matches!(self.brand, GPUBrand::Intel)
|
||||
}
|
||||
|
||||
pub fn is_dedicated(&self) -> bool {
|
||||
!self.is_integrated()
|
||||
}
|
||||
|
||||
pub fn is_mobile(&self) -> bool {
|
||||
let lower_name = self.name.to_lowercase();
|
||||
lower_name.contains("adreno")
|
||||
|| lower_name.contains("mali")
|
||||
|| lower_name.contains("apple")
|
||||
|| lower_name.contains("mobile")
|
||||
|| lower_name.contains("snapdragon")
|
||||
}
|
||||
|
||||
pub fn verbose_info(&self) -> String {
|
||||
format!(
|
||||
"GPU Information:\n\
|
||||
- Brand: {}\n\
|
||||
- Name: {}\n\
|
||||
- VRAM: {}\n\
|
||||
- Driver: {}",
|
||||
self.brand,
|
||||
self.name,
|
||||
self.vram.format_human(),
|
||||
self.driver_version.as_deref().unwrap_or("Unknown")
|
||||
)
|
||||
}
|
||||
|
||||
fn unique_id(&self) -> String {
|
||||
format!("{}-{:?}", self.name, self.device_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SystemMemory {
|
||||
pub total: Memory,
|
||||
pub used: Memory,
|
||||
pub free: Memory,
|
||||
pub available: Memory,
|
||||
pub swap_total: Memory,
|
||||
pub swap_used: Memory,
|
||||
pub swap_free: Memory,
|
||||
}
|
||||
|
||||
impl SystemMemory {
|
||||
pub fn current() -> Self {
|
||||
let mut system = System::new();
|
||||
system.refresh_memory();
|
||||
|
||||
Self {
|
||||
total: Memory::from_bytes(system.total_memory()),
|
||||
used: Memory::from_bytes(system.used_memory()),
|
||||
free: Memory::from_bytes(system.free_memory()),
|
||||
available: Memory::from_bytes(system.available_memory()),
|
||||
swap_total: Memory::from_bytes(system.total_swap()),
|
||||
swap_used: Memory::from_bytes(system.used_swap()),
|
||||
swap_free: Memory::from_bytes(system.free_swap()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verbose_info(&self) -> String {
|
||||
format!(
|
||||
"Memory Information:\n\
|
||||
- RAM: {} total, {} used, {} free, {} available\n\
|
||||
- Swap: {} total, {} used, {} free",
|
||||
self.total.format_human(),
|
||||
self.used.format_human(),
|
||||
self.free.format_human(),
|
||||
self.available.format_human(),
|
||||
self.swap_total.format_human(),
|
||||
self.swap_used.format_human(),
|
||||
self.swap_free.format_human()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EngineInfo {
|
||||
pub timestamp: String,
|
||||
pub pkg_version: String,
|
||||
pub pkg_name: String,
|
||||
pub target_arch: String,
|
||||
pub target_os: String,
|
||||
pub target_env: String,
|
||||
pub rustc_version: String,
|
||||
pub wgpu_version: String,
|
||||
pub winit_version: String,
|
||||
pub commit_hash: String,
|
||||
}
|
||||
|
||||
impl EngineInfo {
|
||||
pub fn current() -> Self {
|
||||
Self {
|
||||
timestamp: build_info::BUILT_TIME_UTC.to_string(),
|
||||
pkg_version: build_info::PKG_VERSION.to_string(),
|
||||
pkg_name: build_info::PKG_NAME.to_string(),
|
||||
target_arch: build_info::CFG_TARGET_ARCH.to_string(),
|
||||
target_os: build_info::CFG_OS.to_string(),
|
||||
target_env: build_info::CFG_ENV.to_string(),
|
||||
rustc_version: build_info::RUSTC_VERSION.to_string(),
|
||||
wgpu_version: build_info::WGPU_VERSION.to_string(),
|
||||
winit_version: build_info::WGPU_VERSION.to_string(),
|
||||
commit_hash: build_info::GIT_COMMIT_HASH
|
||||
.unwrap_or(&format!("UNKNOWN-{:?}", std::time::SystemTime::now()))
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verbose_info(&self) -> String {
|
||||
format!(
|
||||
"Build Information:\n\
|
||||
- Timestamp: {}\n\
|
||||
- Package: {} v{}\n\
|
||||
- Target: {} {} {}\n\
|
||||
- Rustc version: {}\n\
|
||||
- Wgpu version: {}\n\
|
||||
- Winit version: {}\n\
|
||||
- commit_hash: {}\n\
|
||||
",
|
||||
self.timestamp,
|
||||
self.pkg_name,
|
||||
self.pkg_version,
|
||||
self.target_arch,
|
||||
self.target_os,
|
||||
self.target_env,
|
||||
self.rustc_version,
|
||||
self.wgpu_version,
|
||||
self.winit_version,
|
||||
self.commit_hash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SystemMetadata {
|
||||
pub cpu: CPU,
|
||||
pub memory: SystemMemory,
|
||||
pub gpus: Vec<GPU>,
|
||||
pub compile_info: EngineInfo,
|
||||
pub os_info: OSInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OSInfo {
|
||||
pub name: String,
|
||||
pub version: Option<String>,
|
||||
pub kernel_version: Option<String>,
|
||||
}
|
||||
|
||||
impl OSInfo {
|
||||
pub fn current() -> Self {
|
||||
let mut system = System::new();
|
||||
system.refresh_all();
|
||||
Self {
|
||||
name: System::name().unwrap_or_else(|| build_info::TARGET.to_string()),
|
||||
version: System::os_version(),
|
||||
kernel_version: System::kernel_version(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verbose_info(&self) -> String {
|
||||
format!(
|
||||
"Operating System Information:\n\
|
||||
- Name: {}\n\
|
||||
- Version: {}\n\
|
||||
- Kernel Version: {}",
|
||||
self.name,
|
||||
self.version.as_deref().unwrap_or("Unknown"),
|
||||
self.kernel_version.as_deref().unwrap_or("Unknown")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemMetadata {
|
||||
pub fn current() -> Self {
|
||||
Self {
|
||||
cpu: CPU::current(),
|
||||
memory: SystemMemory::current(),
|
||||
gpus: GPU::current(),
|
||||
compile_info: EngineInfo::current(),
|
||||
os_info: OSInfo::current(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_gpu(&self) -> Option<&GPU> {
|
||||
self.gpus
|
||||
.iter()
|
||||
.find(|g| g.is_dedicated())
|
||||
.or_else(|| self.gpus.iter().find(|g| g.is_integrated()))
|
||||
.or_else(|| self.gpus.first())
|
||||
}
|
||||
|
||||
pub fn verbose_summary(&self) -> String {
|
||||
let main_gpu = self.main_gpu();
|
||||
let main_gpu_info = main_gpu
|
||||
.map(|gpu| format!("Main GPU:\n{}", gpu.verbose_info()))
|
||||
.unwrap_or_else(|| "No main GPU detected".to_string());
|
||||
|
||||
let mut seen_gpu_ids = HashSet::new();
|
||||
let mut other_gpus_info = Vec::new();
|
||||
|
||||
if let Some(gpu) = main_gpu {
|
||||
seen_gpu_ids.insert(gpu.unique_id());
|
||||
}
|
||||
|
||||
for gpu in &self.gpus {
|
||||
let gpu_id = gpu.unique_id();
|
||||
if !seen_gpu_ids.contains(&gpu_id) {
|
||||
other_gpus_info.push(format!("[{:?}] {}", gpu.device_type, gpu.verbose_info()));
|
||||
seen_gpu_ids.insert(gpu_id);
|
||||
}
|
||||
}
|
||||
|
||||
let other_gpu_list = if other_gpus_info.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("Other GPUs:\n{}", other_gpus_info.join("\n\n"))
|
||||
};
|
||||
|
||||
format!(
|
||||
"System Information:\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}",
|
||||
self.os_info.verbose_info(),
|
||||
self.cpu.verbose_info(),
|
||||
main_gpu_info,
|
||||
other_gpu_list,
|
||||
self.memory.verbose_info(),
|
||||
self.compile_info.verbose_info()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_memory_conversions() {
|
||||
let mem = Memory::from_gb(8);
|
||||
assert_eq!(mem.as_bytes(), 8 * 1024 * 1024 * 1024);
|
||||
assert_eq!(mem.as_mb(), 8 * 1024);
|
||||
assert_eq!(mem.as_kb(), 8 * 1024 * 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_metadata() {
|
||||
let metadata = SystemMetadata::current();
|
||||
assert!(!metadata.cpu.name.is_empty());
|
||||
assert!(metadata.memory.total.as_bytes() > 0);
|
||||
assert!(!metadata.compile_info.pkg_version.is_empty());
|
||||
assert!(!metadata.os_info.name.is_empty());
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
exec ./test.zensh
|
|
@ -1 +0,0 @@
|
|||
exit "test"
|
|
@ -4,4 +4,4 @@ format_code_in_doc_comments = true
|
|||
trailing_comma = "Vertical"
|
||||
group_imports = "StdExternalCrate"
|
||||
reorder_impl_items = true
|
||||
unstable_features = true
|
||||
unstable_features = true
|
||||
|
|
6
src/main.rs
Normal file
6
src/main.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
|
||||
|
||||
fn main() {
|
||||
println!("Happy Halloween!")
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "zen_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.94"
|
||||
thiserror = "2.0.8"
|
||||
parking_lot.workspace = true
|
||||
|
||||
[profile.dev]
|
||||
debug-assertions = true
|
||||
|
||||
[profile.release]
|
||||
debug-assertions = false
|
|
@ -1,7 +0,0 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum ZError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
2
test.mtl
2
test.mtl
|
@ -1,2 +0,0 @@
|
|||
# Blender 4.2.3 LTS MTL File: 'None'
|
||||
# www.blender.org
|
|
@ -1,4 +0,0 @@
|
|||
[rendering]
|
||||
force_vsync = true
|
||||
|
||||
[settings]
|
Loading…
Add table
Add a link
Reference in a new issue