feat: basic GUI terminal when pressing F12
This commit is contained in:
parent
964a006c47
commit
08af93448c
15 changed files with 808 additions and 845 deletions
768
Cargo.lock
generated
768
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -2,39 +2,42 @@
|
|||
name = "zenyx"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/Zenyx-Engine/Zenyx"
|
||||
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]
|
||||
# TBR (if possible)
|
||||
backtrace = "0.3.74"
|
||||
# TBR (if possible)
|
||||
colored = "3.0.0"
|
||||
backtrace = { version = "0.3.74", default-features = false }
|
||||
colored = { version = "3.0.0", default-features = false }
|
||||
parking_lot.workspace = true
|
||||
# TBR (if possible)
|
||||
rustyline = { version = "15.0.0", features = ["derive", "rustyline-derive"] }
|
||||
thiserror = "2.0.11"
|
||||
# Tokio is heavy but so far its the best option, we should make better use of it or switch to another runtime.
|
||||
tokio = { version = "1.44.2", features = ["macros", "parking_lot", "rt-multi-thread"] }
|
||||
wgpu = "24.0.3"
|
||||
winit = "0.30.9"
|
||||
bytemuck = "1.21.0"
|
||||
# TBR (if possible)
|
||||
futures = "0.3.31"
|
||||
cgmath = "0.18.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
# TBR
|
||||
tobj = { version = "4.0.3", features = ["tokio"] }
|
||||
ahash = "0.8.11"
|
||||
wgpu_text = "0.9.2"
|
||||
toml = "0.8.20"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
native-dialog = "0.7.0"
|
||||
sysinfo = "0.34.2"
|
||||
raw-cpuid = "11.5.0"
|
||||
image = "0.25.6"
|
||||
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 }
|
||||
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", features = ["chrono"] }
|
||||
build-print = "0.1.1"
|
||||
cargo-lock = "10.1.0"
|
||||
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 }
|
||||
|
|
|
@ -82,13 +82,6 @@ fn main() {
|
|||
}
|
||||
};
|
||||
|
||||
writeln!(
|
||||
built_rs,
|
||||
"{}pub static GIT_COMMIT_HASH: &str = \"{}\";",
|
||||
ALLOW_DEAD_CODE, git_info
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
match Lockfile::load(lockfile_path) {
|
||||
Ok(lockfile) => {
|
||||
let dependencies_to_track = ["tokio", "winit", "wgpu"];
|
||||
|
|
27
engine/src/cli/mod.rs
Normal file
27
engine/src/cli/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
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,6 +1,22 @@
|
|||
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,5 +1,5 @@
|
|||
use std::str::FromStr;
|
||||
use std::{error::Error, path::PathBuf};
|
||||
use std::{env, error::Error, path::PathBuf, thread};
|
||||
|
||||
use native_dialog::{MessageDialog, MessageType};
|
||||
use parking_lot::Once;
|
||||
|
@ -15,7 +15,7 @@ pub fn set_panic_hook() {
|
|||
eprintln!("Error in panic hook: {}", e);
|
||||
default_hook(info);
|
||||
}
|
||||
std::process::exit(0);
|
||||
std::process::exit(1);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
@ -41,13 +41,53 @@ fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box<dyn Err
|
|||
"<non-string panic payload>"
|
||||
};
|
||||
|
||||
writeln!(file, "{}", payload_str)?;
|
||||
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 report file at '{}'. Submit an issue or email with the subject of 'Zenyx Crash Report' and include the report as an attachment.
|
||||
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
|
||||
|
@ -61,7 +101,9 @@ Thank you kindly!"#,
|
|||
r#"{}
|
||||
|
||||
For future reference, the error summary is as follows:
|
||||
{}"#,
|
||||
{}
|
||||
|
||||
More details can be found in the crash report file."#,
|
||||
panic_msg, payload_str
|
||||
);
|
||||
|
||||
|
@ -78,15 +120,24 @@ For future reference, the error summary is as follows:
|
|||
|
||||
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(&sysinfo.verbose_summary());
|
||||
backtrace.push_str(&format!(
|
||||
"--- System Information ---\n{}\n",
|
||||
sysinfo.verbose_summary()
|
||||
));
|
||||
|
||||
let trace = backtrace::Backtrace::new();
|
||||
let message = format!("\nBacktrace:\n\n");
|
||||
let trace = std::backtrace::Backtrace::force_capture();
|
||||
let message = format!("\n--- Backtrace ---\n\n");
|
||||
backtrace.push_str(&message);
|
||||
backtrace.push_str(&format!("{trace:?}"));
|
||||
backtrace.push_str(&format!("{trace:#}"));
|
||||
|
||||
backtrace
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ 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)]
|
||||
|
@ -148,7 +150,7 @@ impl Model {
|
|||
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Index Buffer"),
|
||||
contents: bytemuck::cast_slice(indices), // Use proper indices
|
||||
contents: bytemuck::cast_slice(indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
|
||||
|
@ -215,11 +217,12 @@ pub struct Renderer<'window> {
|
|||
|
||||
struct FontState {
|
||||
brush: TextBrush<FontRef<'static>>,
|
||||
section: OwnedSection,
|
||||
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 {
|
||||
|
@ -385,7 +388,7 @@ impl<'window> Renderer<'window> {
|
|||
let scale = base_scale * (surface_config.width as f32 / base_width as f32).clamp(0.5, 2.0);
|
||||
let color = wgpu::Color::WHITE;
|
||||
|
||||
let section = OwnedSection::default()
|
||||
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,
|
||||
|
@ -393,7 +396,25 @@ impl<'window> Renderer<'window> {
|
|||
color.a as f32,
|
||||
]))
|
||||
.with_screen_position((10.0, 10.0))
|
||||
.with_bounds((base_scale * 200.0, base_scale * 2.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)
|
||||
|
@ -424,7 +445,9 @@ impl<'window> Renderer<'window> {
|
|||
fps: 0f32,
|
||||
font_state: FontState {
|
||||
brush,
|
||||
section,
|
||||
fps_section,
|
||||
output_section,
|
||||
input_section,
|
||||
scale,
|
||||
color,
|
||||
},
|
||||
|
@ -458,35 +481,37 @@ impl<'window> Renderer<'window> {
|
|||
self.font_state
|
||||
.brush
|
||||
.resize_view(width as f32, height as f32, &self.queue);
|
||||
let base_width = 1280.0;
|
||||
let base_scale = 30.0;
|
||||
let scale = base_scale * (width as f32 / base_width as f32).clamp(0.5, 2.0);
|
||||
self.font_state.scale = scale;
|
||||
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) {
|
||||
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);
|
||||
|
||||
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_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
@ -508,8 +533,8 @@ impl<'window> Renderer<'window> {
|
|||
label: Some("Render Encoder"),
|
||||
});
|
||||
let fps_text = format!("FPS: {:.2}", self.fps);
|
||||
self.font_state.section.text.clear();
|
||||
self.font_state.section.text.push(
|
||||
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([
|
||||
|
@ -522,7 +547,11 @@ impl<'window> Renderer<'window> {
|
|||
if let Err(e) = self.font_state.brush.queue(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
&[self.font_state.section.clone()],
|
||||
&[
|
||||
self.font_state.fps_section.clone(),
|
||||
self.font_state.input_section.clone(),
|
||||
self.font_state.output_section.clone(),
|
||||
],
|
||||
) {
|
||||
error!("Failed to queue text: {}", e);
|
||||
}
|
||||
|
@ -594,7 +623,88 @@ impl<'window> Renderer<'window> {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ 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;
|
||||
|
@ -23,13 +24,17 @@ use winit::window::Icon;
|
|||
use winit::window::Window;
|
||||
use winit::window::WindowId;
|
||||
|
||||
use super::repl::input::evaluate_command;
|
||||
|
||||
pub mod ctx;
|
||||
|
||||
struct WindowContext<'window> {
|
||||
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;
|
||||
|
||||
|
@ -42,7 +47,18 @@ impl WindowContext<'_> {
|
|||
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>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct App<'window> {
|
||||
windows: ahash::AHashMap<WindowId, WindowContext<'window>>,
|
||||
|
@ -122,7 +138,6 @@ impl App<'_> {
|
|||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
error!("Failed to load Pumpkin.obj: {e}");
|
||||
// Fallback to CUBE_OBJ
|
||||
let fallback_obj = CUBE_OBJ.to_string();
|
||||
tobj::load_obj_buf(
|
||||
&mut fallback_obj.as_bytes(),
|
||||
|
@ -147,6 +162,7 @@ impl App<'_> {
|
|||
window,
|
||||
ctx: wgpu_ctx,
|
||||
main_window: true,
|
||||
terminal_state: None,
|
||||
},
|
||||
);
|
||||
info!("Main window created: {:?}", window_id);
|
||||
|
@ -158,6 +174,69 @@ impl App<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
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 self.windows.remove(&window_id).is_some() {
|
||||
debug!("Window {:?} closed", window_id);
|
||||
|
@ -175,6 +254,12 @@ impl App<'_> {
|
|||
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 => {
|
||||
|
@ -184,9 +269,10 @@ impl App<'_> {
|
|||
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),
|
||||
},
|
||||
_ => debug!("Received a keyboard event with no physical key"),
|
||||
_ => error!("Unhandled key event: {:?}", key_event),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,9 +354,9 @@ impl App<'_> {
|
|||
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());
|
||||
// .with_taskbar_icon(icon);
|
||||
|
||||
match main_ctx.window_handle() {
|
||||
Ok(handle) => {
|
||||
|
@ -321,6 +407,7 @@ impl App<'_> {
|
|||
window,
|
||||
ctx: wgpu_ctx,
|
||||
main_window: false,
|
||||
terminal_state: None,
|
||||
},
|
||||
);
|
||||
debug!("Spawned new child window: {:?}", window_id);
|
||||
|
@ -337,21 +424,39 @@ impl App<'_> {
|
|||
|
||||
fn handle_redraw_requested(&mut self, window_id: WindowId) {
|
||||
if let Some(window_context) = self.windows.get_mut(&window_id) {
|
||||
window_context.ctx.draw();
|
||||
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();
|
||||
trace!(
|
||||
"Redrew window {:?} with title: {}",
|
||||
window_id,
|
||||
window_context.window.title()
|
||||
);
|
||||
} else {
|
||||
warn!("Received redraw for unknown window {:?}", window_id);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -10,27 +10,28 @@ use crate::error::{ZenyxError, ZenyxErrorKind};
|
|||
pub struct HelpCommand;
|
||||
|
||||
impl Command for HelpCommand {
|
||||
fn execute(&self, _args: Option<Vec<String>>) -> Result<(), ZenyxError> {
|
||||
fn execute(&self, _args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
let manager = COMMAND_MANAGER.read();
|
||||
println!("Available commands:\n");
|
||||
let mut output = String::new();
|
||||
output.push_str("Available commands:\n\n");
|
||||
|
||||
for (_, command) in manager.get_commands() {
|
||||
println!(
|
||||
"Command: {}\n\tDescription: {}\n\tParameters: {}\n\tHelp: {}\n",
|
||||
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() {
|
||||
println!("Aliases:");
|
||||
output.push_str("Aliases:\n");
|
||||
for (alias, command) in &manager.aliases {
|
||||
println!("\t{} -> {}", alias, command);
|
||||
output.push_str(&format!("\t{} -> {}\n", alias, command));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn undo(&self) {}
|
||||
|
@ -58,8 +59,7 @@ impl Command for HelpCommand {
|
|||
pub struct ClearCommand;
|
||||
|
||||
impl Command for ClearCommand {
|
||||
fn execute(&self, _args: Option<Vec<String>>) -> Result<(), ZenyxError> {
|
||||
println!("Clearing screen..., running command");
|
||||
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"])
|
||||
|
@ -67,7 +67,7 @@ impl Command for ClearCommand {
|
|||
} else {
|
||||
std::process::Command::new("clear").spawn()
|
||||
};
|
||||
Ok(())
|
||||
Ok(String::from("Screen cleared."))
|
||||
}
|
||||
|
||||
fn undo(&self) {}
|
||||
|
@ -95,7 +95,7 @@ impl Command for ClearCommand {
|
|||
pub struct ExitCommand;
|
||||
|
||||
impl Command for ExitCommand {
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<(), ZenyxError> {
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
match args {
|
||||
Some(args) => {
|
||||
let exit_code = args[0].parse().map_err(|e| {
|
||||
|
@ -141,7 +141,7 @@ impl Command for ExitCommand {
|
|||
pub struct ExecFile;
|
||||
|
||||
impl Command for ExecFile {
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<(), ZenyxError> {
|
||||
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| {
|
||||
|
@ -161,23 +161,30 @@ impl Command for ExecFile {
|
|||
.with_source(e)
|
||||
.build()
|
||||
})?;
|
||||
if let Ok(command) = eval(zscript) {
|
||||
println!("{:#?}", command);
|
||||
for (cmd_name, cmd_args) in command {
|
||||
let mut script_output = String::new();
|
||||
if let Ok(commands_to_execute) = eval(zscript) {
|
||||
for (cmd_name, cmd_args) in commands_to_execute {
|
||||
match COMMAND_MANAGER.read().execute(&cmd_name, cmd_args) {
|
||||
Ok(_) => (),
|
||||
Ok(output) => script_output.push_str(&output),
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error executing command returned an error: {}. Aborting script",
|
||||
e
|
||||
);
|
||||
break;
|
||||
return Err(ZenyxError::builder(
|
||||
ZenyxErrorKind::CommandExecution,
|
||||
)
|
||||
.with_message(format!(
|
||||
"Error executing command '{}' in script: {}",
|
||||
cmd_name, e
|
||||
))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(script_output);
|
||||
} else {
|
||||
return Err(ZenyxError::builder(ZenyxErrorKind::CommandExecution)
|
||||
.with_message("Failed to evaluate script")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => Err(ZenyxError::builder(ZenyxErrorKind::CommandParsing)
|
||||
.with_message("Not enough arguments")
|
||||
|
@ -212,12 +219,13 @@ pub struct CounterCommand {
|
|||
}
|
||||
|
||||
impl Command for CounterCommand {
|
||||
fn execute(&self, _args: Option<Vec<String>>) -> Result<(), ZenyxError> {
|
||||
// Increment the counter
|
||||
fn execute(&self, _args: Option<Vec<String>>) -> Result<String, ZenyxError> {
|
||||
let mut count = self.counter.write();
|
||||
*count += 1;
|
||||
println!("CounterCommand executed. Current count: {}", *count);
|
||||
Ok(())
|
||||
Ok(format!(
|
||||
"CounterCommand executed. Current count: {}",
|
||||
*count
|
||||
))
|
||||
}
|
||||
|
||||
fn undo(&self) {
|
||||
|
@ -248,14 +256,15 @@ impl Command for CounterCommand {
|
|||
#[derive(Default)]
|
||||
pub struct PanicCommmand;
|
||||
impl Command for PanicCommmand {
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<(), ZenyxError> {
|
||||
if args.is_some() {
|
||||
let panic_msg = &args.unwrap()[0];
|
||||
panic!("{}", panic_msg)
|
||||
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");
|
||||
}
|
||||
let option: Option<i32> = None;
|
||||
println!("Unwrapping None: {}", option.unwrap());
|
||||
panic!("Panic command was called")
|
||||
}
|
||||
|
||||
fn undo(&self) {}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use ahash::AHashMap;
|
||||
use colored::Colorize;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
|
@ -93,15 +93,15 @@ fn check_similarity(target: &str) -> Option<String> {
|
|||
}
|
||||
|
||||
pub struct CommandManager {
|
||||
pub commands: HashMap<String, Box<dyn Command>>,
|
||||
pub aliases: HashMap<String, String>,
|
||||
pub commands: AHashMap<String, Box<dyn Command>>,
|
||||
pub aliases: AHashMap<String, String>,
|
||||
}
|
||||
|
||||
impl CommandManager {
|
||||
pub fn init() -> CommandManager {
|
||||
CommandManager {
|
||||
commands: HashMap::new(),
|
||||
aliases: HashMap::new(),
|
||||
commands: AHashMap::new(),
|
||||
aliases: AHashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,10 +113,10 @@ impl CommandManager {
|
|||
&self,
|
||||
command: &str,
|
||||
args: Option<Vec<String>>,
|
||||
) -> Result<(), ZenyxError> {
|
||||
) -> Result<String, ZenyxError> {
|
||||
if let Some(command) = self.commands.get(command) {
|
||||
command.execute(args)?;
|
||||
Ok(())
|
||||
let output = command.execute(args)?;
|
||||
Ok(output)
|
||||
} else {
|
||||
let corrected_cmd = check_similarity(command);
|
||||
if let Some(corrected_cmd) = corrected_cmd {
|
||||
|
@ -132,12 +132,12 @@ impl CommandManager {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn execute(&self, command: &str, args: Option<Vec<String>>) -> Result<(), ZenyxError> {
|
||||
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 => {
|
||||
self.execute_command(command, args)?;
|
||||
Ok(())
|
||||
let output = self.execute_command(command, args)?;
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ impl CommandManager {
|
|||
}
|
||||
|
||||
pub trait Command: Send + Sync {
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<(), ZenyxError>;
|
||||
fn execute(&self, args: Option<Vec<String>>) -> Result<String, ZenyxError>;
|
||||
fn undo(&self);
|
||||
fn redo(&self);
|
||||
fn get_description(&self) -> String;
|
||||
|
|
|
@ -16,13 +16,8 @@ use tracing::{debug, error, info, warn};
|
|||
|
||||
use super::handler::COMMAND_MANAGER;
|
||||
use crate::error::{Result, ZenyxError, ZenyxErrorKind};
|
||||
|
||||
#[derive(Default)]
|
||||
struct CommandCompleter;
|
||||
impl CommandCompleter {
|
||||
fn new() -> Self {
|
||||
CommandCompleter {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for CommandCompleter {
|
||||
type Candidate = String;
|
||||
|
@ -138,15 +133,19 @@ pub fn parse_command(input: &str) -> Result<Vec<String>> {
|
|||
Ok(commands)
|
||||
}
|
||||
|
||||
pub fn evaluate_command(input: &str) -> Result<()> {
|
||||
pub fn evaluate_command(input: &str) -> std::result::Result<String, ZenyxError> {
|
||||
if input.trim().is_empty() {
|
||||
return Ok(());
|
||||
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();
|
||||
|
@ -166,20 +165,16 @@ pub fn evaluate_command(input: &str) -> Result<()> {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
COMMAND_MANAGER
|
||||
.read()
|
||||
.execute(cmd_name, args)
|
||||
.map_err(|e| {
|
||||
ZenyxError::builder(ZenyxErrorKind::CommandExecution)
|
||||
.with_message(format!("Failed to execute command: {cmd_name}"))
|
||||
.with_context(format!("{e}"))
|
||||
.build()
|
||||
})?;
|
||||
match COMMAND_MANAGER.read().execute(cmd_name, args) {
|
||||
Ok(command_output) => output.push_str(&command_output),
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn format_time() -> String {
|
||||
pub fn format_time() -> String {
|
||||
let now = SystemTime::now();
|
||||
let duration = now.duration_since(UNIX_EPOCH).unwrap();
|
||||
let total_seconds = duration.as_secs();
|
||||
|
@ -201,7 +196,7 @@ pub async fn handle_repl() -> Result<()> {
|
|||
let mut rl = Editor::<MyHelper, DefaultHistory>::new()?;
|
||||
rl.set_helper(Some(MyHelper {
|
||||
hinter: HistoryHinter::new(),
|
||||
completer: CommandCompleter::new(),
|
||||
completer: CommandCompleter::default(),
|
||||
}));
|
||||
|
||||
rl.bind_sequence(
|
||||
|
@ -218,6 +213,7 @@ pub async fn handle_repl() -> Result<()> {
|
|||
loop {
|
||||
let time = format_time();
|
||||
let prompt = format!("[{}/{}] {}", time, "SHELL", ">>\t");
|
||||
|
||||
let sig = rl.readline(&prompt.bright_white());
|
||||
|
||||
match sig {
|
||||
|
|
|
@ -115,7 +115,7 @@ impl std::fmt::Display for ZenyxError {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Error: {}{}{}{}",
|
||||
"{}{}{}{}",
|
||||
self.kind,
|
||||
self.message
|
||||
.as_ref()
|
||||
|
|
|
@ -1,35 +1,65 @@
|
|||
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::{level_filters::LevelFilter, subscriber::set_global_default};
|
||||
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 subscriber = tracing_subscriber::fmt()
|
||||
.with_max_level(LevelFilter::DEBUG)
|
||||
let stdout_layer = fmt::layer()
|
||||
.with_level(true)
|
||||
.compact()
|
||||
.pretty()
|
||||
.log_internal_errors(false)
|
||||
.without_time()
|
||||
.with_thread_names(true)
|
||||
.finish();
|
||||
.with_thread_names(true);
|
||||
|
||||
set_global_default(subscriber).expect("Failed to set default subscriber");
|
||||
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(flavor = "current_thread")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
init_logger();
|
||||
cli::parse();
|
||||
let sysinfo = crate::metadata::SystemMetadata::current();
|
||||
|
||||
set_panic_hook();
|
||||
// set_panic_hook();
|
||||
setup();
|
||||
|
||||
splash::print_splash();
|
||||
|
@ -51,8 +81,6 @@ async fn main() {
|
|||
};
|
||||
rt.block_on(core::repl::input::handle_repl())
|
||||
});
|
||||
splash::print_splash();
|
||||
info!("Type 'help' for a list of commands.");
|
||||
|
||||
match EventLoop::new() {
|
||||
Ok(event_loop) => {
|
||||
|
|
|
@ -1,15 +1,183 @@
|
|||
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: parking_lot::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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Memory {
|
||||
bytes: u64,
|
||||
|
@ -489,7 +657,9 @@ impl EngineInfo {
|
|||
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.to_string(),
|
||||
commit_hash: build_info::GIT_COMMIT_HASH
|
||||
.unwrap_or(&format!("UNKNOWN-{:?}", std::time::SystemTime::now()))
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -524,6 +694,38 @@ pub struct SystemMetadata {
|
|||
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: sysinfo::System::name().unwrap_or_else(|| build_info::TARGET.to_string()),
|
||||
version: sysinfo::System::os_version(),
|
||||
kernel_version: sysinfo::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 {
|
||||
|
@ -533,6 +735,7 @@ impl SystemMetadata {
|
|||
memory: SystemMemory::current(),
|
||||
gpus: GPU::current(),
|
||||
compile_info: EngineInfo::current(),
|
||||
os_info: OSInfo::current(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -572,7 +775,8 @@ impl SystemMetadata {
|
|||
};
|
||||
|
||||
format!(
|
||||
"System Information:\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}",
|
||||
"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,
|
||||
|
@ -600,5 +804,6 @@ mod tests {
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
0
zenyx.toml
Normal file
0
zenyx.toml
Normal file
Loading…
Add table
Add a link
Reference in a new issue