feat: basic GUI terminal when pressing F12

This commit is contained in:
Chance 2025-04-10 14:26:52 -04:00 committed by BitSyndicate
parent 07871b77f3
commit 00ec1350b7
15 changed files with 808 additions and 845 deletions

768
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,39 +2,42 @@
name = "zenyx" name = "zenyx"
version = "0.1.0" version = "0.1.0"
edition = "2024" 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] [dependencies]
# TBR (if possible) backtrace = { version = "0.3.74", default-features = false }
backtrace = "0.3.74" colored = { version = "3.0.0", default-features = false }
# TBR (if possible)
colored = "3.0.0"
parking_lot.workspace = true parking_lot.workspace = true
# TBR (if possible) rustyline = { version = "15.0.0", default-features = false, features = ["custom-bindings", "derive","with-file-history"] }
rustyline = { version = "15.0.0", features = ["derive", "rustyline-derive"] } thiserror = { version = "2.0.11", default-features = false }
thiserror = "2.0.11" tokio = { version = "1.44.2", default-features = false, features = ["macros", "rt", "rt-multi-thread"] }
# Tokio is heavy but so far its the best option, we should make better use of it or switch to another runtime. # Will be updated to 25.x.x when other dependencies are updated to be supported
tokio = { version = "1.44.2", features = ["macros", "parking_lot", "rt-multi-thread"] } wgpu = { version = "24.0.3", default-features = false }
wgpu = "24.0.3" winit = { version = "0.30.9", default-features = false, features = ["rwh_06", "wayland"] }
winit = "0.30.9" bytemuck = { version = "1.21.0", default-features = false }
bytemuck = "1.21.0" futures = { version = "0.3.31", default-features = false, features = ["executor"] }
# TBR (if possible) cgmath = { version = "0.18.0", default-features = false }
futures = "0.3.31" tracing = { version = "0.1.41", default-features = false }
cgmath = "0.18.0" tracing-subscriber = { version = "0.3.19", default-features = false, features = ["ansi", "fmt"] }
tracing = "0.1.41" tobj = { version = "4.0.3", default-features = false }
tracing-subscriber = "0.3.19" ahash = { version = "0.8.11", default-features = false }
# TBR wgpu_text = { version = "0.9.2", default-features = false }
tobj = { version = "4.0.3", features = ["tokio"] } toml = { version = "0.8.20", default-features = false }
ahash = "0.8.11" serde = { version = "1.0.219", default-features = false, features = ["derive"] }
wgpu_text = "0.9.2" native-dialog = { version = "0.7.0", default-features = false }
toml = "0.8.20" sysinfo = { version = "0.34.2", default-features = false, features = ["system"] }
serde = { version = "1.0.219", features = ["derive"] } raw-cpuid = { version = "11.5.0", default-features = false }
native-dialog = "0.7.0" image = { version = "0.25.6", default-features = false, features = ["png"] }
sysinfo = "0.34.2" clap = { version = "4.5.35", default-features = false, features = ["std"] }
raw-cpuid = "11.5.0"
image = "0.25.6"
[build-dependencies] [build-dependencies]
built = { version = "0.7.7", features = ["chrono"] } built = { version = "0.7.7", default-features = false, features = ["cargo-lock", "chrono", "git2"] }
build-print = "0.1.1" build-print = { version = "0.1.1", default-features = false }
cargo-lock = "10.1.0" cargo-lock = { version = "10.1.0", default-features = false }

View file

@ -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) { match Lockfile::load(lockfile_path) {
Ok(lockfile) => { Ok(lockfile) => {
let dependencies_to_track = ["tokio", "winit", "wgpu"]; let dependencies_to_track = ["tokio", "winit", "wgpu"];

27
engine/src/cli/mod.rs Normal file
View 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();
}

View file

@ -1,6 +1,22 @@
use serde::{Deserialize, Serialize};
pub mod ecs; pub mod ecs;
pub mod panic; pub mod panic;
pub mod repl; pub mod repl;
pub mod splash; pub mod splash;
pub mod render; 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 {}

View file

@ -1,5 +1,5 @@
use std::str::FromStr; use std::str::FromStr;
use std::{error::Error, path::PathBuf}; use std::{env, error::Error, path::PathBuf, thread};
use native_dialog::{MessageDialog, MessageType}; use native_dialog::{MessageDialog, MessageType};
use parking_lot::Once; use parking_lot::Once;
@ -15,7 +15,7 @@ pub fn set_panic_hook() {
eprintln!("Error in panic hook: {}", e); eprintln!("Error in panic hook: {}", e);
default_hook(info); 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>" "<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())?; 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!( let panic_msg = format!(
r#"Zenyx had a problem and crashed. To help us diagnose the problem you can send us a crash report. 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: To submit the crash report:
https://codeberg.org/Caznix/Zenyx/issues https://codeberg.org/Caznix/Zenyx/issues
@ -61,7 +101,9 @@ Thank you kindly!"#,
r#"{} r#"{}
For future reference, the error summary is as follows: For future reference, the error summary is as follows:
{}"#, {}
More details can be found in the crash report file."#,
panic_msg, payload_str panic_msg, payload_str
); );
@ -78,15 +120,24 @@ For future reference, the error summary is as follows:
Ok(()) Ok(())
} }
fn rust_version() -> Result<String, Box<dyn Error>> {
let version = env!("CARGO_PKG_RUST_VERSION");
Ok(version.to_string())
}
fn capture_backtrace() -> String { fn capture_backtrace() -> String {
let mut backtrace = String::new(); let mut backtrace = String::new();
let sysinfo = crate::metadata::SystemMetadata::current(); 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 trace = std::backtrace::Backtrace::force_capture();
let message = format!("\nBacktrace:\n\n"); let message = format!("\n--- Backtrace ---\n\n");
backtrace.push_str(&message); backtrace.push_str(&message);
backtrace.push_str(&format!("{trace:?}")); backtrace.push_str(&format!("{trace:#}"));
backtrace backtrace
} }

View file

@ -16,6 +16,8 @@ use winit::window::Window;
use crate::error::Result; use crate::error::Result;
use crate::error::{ZenyxError, ZenyxErrorKind}; use crate::error::{ZenyxError, ZenyxErrorKind};
use super::TerminalState;
const SHADER_SRC: &str = include_str!("shader.wgsl"); const SHADER_SRC: &str = include_str!("shader.wgsl");
#[repr(C)] #[repr(C)]
@ -148,7 +150,7 @@ impl Model {
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"), label: Some("Index Buffer"),
contents: bytemuck::cast_slice(indices), // Use proper indices contents: bytemuck::cast_slice(indices),
usage: wgpu::BufferUsages::INDEX, usage: wgpu::BufferUsages::INDEX,
}); });
@ -215,11 +217,12 @@ pub struct Renderer<'window> {
struct FontState { struct FontState {
brush: TextBrush<FontRef<'static>>, brush: TextBrush<FontRef<'static>>,
section: OwnedSection, output_section: OwnedSection,
input_section: OwnedSection,
fps_section: OwnedSection,
scale: f32, scale: f32,
color: wgpu::Color, color: wgpu::Color,
} }
impl<'window> Renderer<'window> { impl<'window> Renderer<'window> {
pub async fn new(window: Arc<Window>) -> Result<Self> { pub async fn new(window: Arc<Window>) -> Result<Self> {
let instance = wgpu::Instance::new(&InstanceDescriptor { 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 scale = base_scale * (surface_config.width as f32 / base_width as f32).clamp(0.5, 2.0);
let color = wgpu::Color::WHITE; 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([ .add_text(OwnedText::new("FPS: 0.00").with_scale(scale).with_color([
color.r as f32, color.r as f32,
color.g as f32, color.g as f32,
@ -393,7 +396,25 @@ impl<'window> Renderer<'window> {
color.a as f32, color.a as f32,
])) ]))
.with_screen_position((10.0, 10.0)) .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( .with_layout(
Layout::default() Layout::default()
.h_align(HorizontalAlign::Left) .h_align(HorizontalAlign::Left)
@ -424,7 +445,9 @@ impl<'window> Renderer<'window> {
fps: 0f32, fps: 0f32,
font_state: FontState { font_state: FontState {
brush, brush,
section, fps_section,
output_section,
input_section,
scale, scale,
color, color,
}, },
@ -458,16 +481,17 @@ impl<'window> Renderer<'window> {
self.font_state self.font_state
.brush .brush
.resize_view(width as f32, height as f32, &self.queue); .resize_view(width as f32, height as f32, &self.queue);
let base_width = 1280.0; self.font_state.output_section.bounds = (width as f32 - 20.0, height as f32 - 60.0);
let base_scale = 30.0; self.font_state.input_section.screen_position = (10.0, height as f32 - 50.0);
let scale = base_scale * (width as f32 / base_width as f32).clamp(0.5, 2.0);
self.font_state.scale = scale;
self.camera.resize(width, height); 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(); 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() { for (i, model) in self.models.iter_mut().enumerate() {
@ -475,7 +499,8 @@ impl<'window> Renderer<'window> {
if i % 2 == 0 { if i % 2 == 0 {
model.set_transform(Matrix4::from_angle_y(angle)); model.set_transform(Matrix4::from_angle_y(angle));
} else { } else {
model.set_transform(Matrix4::from_angle_x(angle) * Matrix4::from_angle_y(angle)); model
.set_transform(Matrix4::from_angle_x(angle) * Matrix4::from_angle_y(angle));
} }
} }
for (i, model) in self.models.iter().enumerate() { for (i, model) in self.models.iter().enumerate() {
@ -486,7 +511,7 @@ impl<'window> Renderer<'window> {
self.model_versions[i] = model.version; self.model_versions[i] = model.version;
} }
} }
}
let surface_texture = self let surface_texture = self
.surface .surface
.get_current_texture() .get_current_texture()
@ -508,8 +533,8 @@ impl<'window> Renderer<'window> {
label: Some("Render Encoder"), label: Some("Render Encoder"),
}); });
let fps_text = format!("FPS: {:.2}", self.fps); let fps_text = format!("FPS: {:.2}", self.fps);
self.font_state.section.text.clear(); self.font_state.fps_section.text.clear();
self.font_state.section.text.push( self.font_state.fps_section.text.push(
OwnedText::new(fps_text) OwnedText::new(fps_text)
.with_scale(self.font_state.scale) .with_scale(self.font_state.scale)
.with_color([ .with_color([
@ -522,7 +547,11 @@ impl<'window> Renderer<'window> {
if let Err(e) = self.font_state.brush.queue( if let Err(e) = self.font_state.brush.queue(
&self.device, &self.device,
&self.queue, &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); error!("Failed to queue text: {}", e);
} }
@ -594,7 +623,88 @@ impl<'window> Renderer<'window> {
self.last_frame_instant = Instant::now(); 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) { pub fn set_bg_color(&mut self, color: wgpu::Color) {
self.bg_color = color; self.bg_color = color;
} }

View file

@ -16,6 +16,7 @@ use winit::dpi::LogicalSize;
use winit::dpi::Size; use winit::dpi::Size;
use winit::event::{KeyEvent, WindowEvent}; use winit::event::{KeyEvent, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::NamedKey;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use winit::platform::windows::WindowAttributesExtWindows; use winit::platform::windows::WindowAttributesExtWindows;
use winit::window::Fullscreen; use winit::window::Fullscreen;
@ -23,13 +24,17 @@ use winit::window::Icon;
use winit::window::Window; use winit::window::Window;
use winit::window::WindowId; use winit::window::WindowId;
use super::repl::input::evaluate_command;
pub mod ctx; pub mod ctx;
struct WindowContext<'window> { pub struct WindowContext<'window> {
window: Arc<Window>, window: Arc<Window>,
ctx: Renderer<'window>, ctx: Renderer<'window>,
main_window: bool, main_window: bool,
terminal_state: Option<TerminalState>,
} }
impl Deref for WindowContext<'_> { impl Deref for WindowContext<'_> {
type Target = winit::window::Window; type Target = winit::window::Window;
@ -42,7 +47,18 @@ impl WindowContext<'_> {
self.main_window 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)] #[derive(Default)]
pub struct App<'window> { pub struct App<'window> {
windows: ahash::AHashMap<WindowId, WindowContext<'window>>, windows: ahash::AHashMap<WindowId, WindowContext<'window>>,
@ -122,7 +138,6 @@ impl App<'_> {
Ok(obj) => obj, Ok(obj) => obj,
Err(e) => { Err(e) => {
error!("Failed to load Pumpkin.obj: {e}"); error!("Failed to load Pumpkin.obj: {e}");
// Fallback to CUBE_OBJ
let fallback_obj = CUBE_OBJ.to_string(); let fallback_obj = CUBE_OBJ.to_string();
tobj::load_obj_buf( tobj::load_obj_buf(
&mut fallback_obj.as_bytes(), &mut fallback_obj.as_bytes(),
@ -147,6 +162,7 @@ impl App<'_> {
window, window,
ctx: wgpu_ctx, ctx: wgpu_ctx,
main_window: true, main_window: true,
terminal_state: None,
}, },
); );
info!("Main window created: {:?}", window_id); 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) { fn handle_close_requested(&mut self, window_id: WindowId) {
if self.windows.remove(&window_id).is_some() { if self.windows.remove(&window_id).is_some() {
debug!("Window {:?} closed", window_id); debug!("Window {:?} closed", window_id);
@ -175,6 +254,12 @@ impl App<'_> {
if !key_event.state.is_pressed() || key_event.repeat { if !key_event.state.is_pressed() || key_event.repeat {
return; 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 { match key_event.physical_key {
winit::keyboard::PhysicalKey::Code(code) => match code { winit::keyboard::PhysicalKey::Code(code) => match code {
winit::keyboard::KeyCode::Space => { winit::keyboard::KeyCode::Space => {
@ -184,9 +269,10 @@ impl App<'_> {
self.spawn_child_window(event_loop); self.spawn_child_window(event_loop);
} }
winit::keyboard::KeyCode::F11 => self.toggle_fullscreen(window_id), winit::keyboard::KeyCode::F11 => self.toggle_fullscreen(window_id),
winit::keyboard::KeyCode::F12 => self.create_terminal_window(event_loop),
other => error!("Unimplemented keycode: {:?}", other), 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 win_attr = unsafe {
let base = Window::default_attributes() let base = Window::default_attributes()
.with_title(title) .with_title(title)
// .with_taskbar_icon(icon)
.with_min_inner_size(Size::Logical(LogicalSize::new(100.0, 100.0))) .with_min_inner_size(Size::Logical(LogicalSize::new(100.0, 100.0)))
.with_window_icon(icon.clone()); .with_window_icon(icon.clone());
// .with_taskbar_icon(icon);
match main_ctx.window_handle() { match main_ctx.window_handle() {
Ok(handle) => { Ok(handle) => {
@ -321,6 +407,7 @@ impl App<'_> {
window, window,
ctx: wgpu_ctx, ctx: wgpu_ctx,
main_window: false, main_window: false,
terminal_state: None,
}, },
); );
debug!("Spawned new child window: {:?}", window_id); debug!("Spawned new child window: {:?}", window_id);
@ -337,21 +424,39 @@ impl App<'_> {
fn handle_redraw_requested(&mut self, window_id: WindowId) { fn handle_redraw_requested(&mut self, window_id: WindowId) {
if let Some(window_context) = self.windows.get_mut(&window_id) { 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(); 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>) { 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 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 we dont ignore size 0 this WILL cause a crash. DO NOT REMOVE
if new_size.height == 0 || new_size.width == 0 { if new_size.height == 0 || new_size.width == 0 {
error!("Attempted to resize a window to 0x0!"); error!("Attempted to resize a window to 0x0!");
return; return;

View file

@ -10,27 +10,28 @@ use crate::error::{ZenyxError, ZenyxErrorKind};
pub struct HelpCommand; pub struct HelpCommand;
impl Command for 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(); 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() { for (_, command) in manager.get_commands() {
println!( output.push_str(&format!(
"Command: {}\n\tDescription: {}\n\tParameters: {}\n\tHelp: {}\n", "Command: {}\n\tDescription: {}\n\tParameters: {}\n\tHelp: {}\n\n",
command.get_name().to_lowercase(), command.get_name().to_lowercase(),
command.get_description(), command.get_description(),
command.get_params(), command.get_params(),
command.get_help() command.get_help()
); ));
} }
if !manager.aliases.is_empty() { if !manager.aliases.is_empty() {
println!("Aliases:"); output.push_str("Aliases:\n");
for (alias, command) in &manager.aliases { for (alias, command) in &manager.aliases {
println!("\t{} -> {}", alias, command); output.push_str(&format!("\t{} -> {}\n", alias, command));
} }
} }
Ok(()) Ok(output)
} }
fn undo(&self) {} fn undo(&self) {}
@ -58,8 +59,7 @@ impl Command for HelpCommand {
pub struct ClearCommand; pub struct ClearCommand;
impl Command for ClearCommand { impl Command for ClearCommand {
fn execute(&self, _args: Option<Vec<String>>) -> Result<(), ZenyxError> { fn execute(&self, _args: Option<Vec<String>>) -> Result<String, ZenyxError> {
println!("Clearing screen..., running command");
let _result = if cfg!(target_os = "windows") { let _result = if cfg!(target_os = "windows") {
std::process::Command::new("cmd") std::process::Command::new("cmd")
.args(["/c", "cls"]) .args(["/c", "cls"])
@ -67,7 +67,7 @@ impl Command for ClearCommand {
} else { } else {
std::process::Command::new("clear").spawn() std::process::Command::new("clear").spawn()
}; };
Ok(()) Ok(String::from("Screen cleared."))
} }
fn undo(&self) {} fn undo(&self) {}
@ -95,7 +95,7 @@ impl Command for ClearCommand {
pub struct ExitCommand; pub struct ExitCommand;
impl Command for 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 { match args {
Some(args) => { Some(args) => {
let exit_code = args[0].parse().map_err(|e| { let exit_code = args[0].parse().map_err(|e| {
@ -141,7 +141,7 @@ impl Command for ExitCommand {
pub struct ExecFile; pub struct ExecFile;
impl Command for 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 { match args {
Some(args) => { Some(args) => {
let file_path = PathBuf::from_str(&args[0]).map_err(|e| { let file_path = PathBuf::from_str(&args[0]).map_err(|e| {
@ -161,23 +161,30 @@ impl Command for ExecFile {
.with_source(e) .with_source(e)
.build() .build()
})?; })?;
if let Ok(command) = eval(zscript) { let mut script_output = String::new();
println!("{:#?}", command); if let Ok(commands_to_execute) = eval(zscript) {
for (cmd_name, cmd_args) in command { for (cmd_name, cmd_args) in commands_to_execute {
match COMMAND_MANAGER.read().execute(&cmd_name, cmd_args) { match COMMAND_MANAGER.read().execute(&cmd_name, cmd_args) {
Ok(_) => (), Ok(output) => script_output.push_str(&output),
Err(e) => { Err(e) => {
println!( return Err(ZenyxError::builder(
"Error executing command returned an error: {}. Aborting script", ZenyxErrorKind::CommandExecution,
e )
); .with_message(format!(
break; "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) None => Err(ZenyxError::builder(ZenyxErrorKind::CommandParsing)
.with_message("Not enough arguments") .with_message("Not enough arguments")
@ -212,12 +219,13 @@ pub struct CounterCommand {
} }
impl Command for CounterCommand { impl Command for CounterCommand {
fn execute(&self, _args: Option<Vec<String>>) -> Result<(), ZenyxError> { fn execute(&self, _args: Option<Vec<String>>) -> Result<String, ZenyxError> {
// Increment the counter
let mut count = self.counter.write(); let mut count = self.counter.write();
*count += 1; *count += 1;
println!("CounterCommand executed. Current count: {}", *count); Ok(format!(
Ok(()) "CounterCommand executed. Current count: {}",
*count
))
} }
fn undo(&self) { fn undo(&self) {
@ -248,14 +256,15 @@ impl Command for CounterCommand {
#[derive(Default)] #[derive(Default)]
pub struct PanicCommmand; pub struct PanicCommmand;
impl Command for PanicCommmand { impl Command for PanicCommmand {
fn execute(&self, args: Option<Vec<String>>) -> Result<(), ZenyxError> { fn execute(&self, args: Option<Vec<String>>) -> Result<String, ZenyxError> {
if args.is_some() { if let Some(args) = args {
let panic_msg = &args.unwrap()[0]; let panic_msg = &args[0];
panic!("{}", panic_msg) panic!("{}", panic_msg);
} } else {
let option: Option<i32> = None; let option: Option<i32> = None;
println!("Unwrapping None: {}", option.unwrap()); println!("Unwrapping None: {}", option.unwrap());
panic!("Panic command was called") panic!("Panic command was called");
}
} }
fn undo(&self) {} fn undo(&self) {}

View file

@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::sync::LazyLock; use std::sync::LazyLock;
use ahash::AHashMap;
use colored::Colorize; use colored::Colorize;
use parking_lot::RwLock; use parking_lot::RwLock;
@ -93,15 +93,15 @@ fn check_similarity(target: &str) -> Option<String> {
} }
pub struct CommandManager { pub struct CommandManager {
pub commands: HashMap<String, Box<dyn Command>>, pub commands: AHashMap<String, Box<dyn Command>>,
pub aliases: HashMap<String, String>, pub aliases: AHashMap<String, String>,
} }
impl CommandManager { impl CommandManager {
pub fn init() -> CommandManager { pub fn init() -> CommandManager {
CommandManager { CommandManager {
commands: HashMap::new(), commands: AHashMap::new(),
aliases: HashMap::new(), aliases: AHashMap::new(),
} }
} }
@ -113,10 +113,10 @@ impl CommandManager {
&self, &self,
command: &str, command: &str,
args: Option<Vec<String>>, args: Option<Vec<String>>,
) -> Result<(), ZenyxError> { ) -> Result<String, ZenyxError> {
if let Some(command) = self.commands.get(command) { if let Some(command) = self.commands.get(command) {
command.execute(args)?; let output = command.execute(args)?;
Ok(()) Ok(output)
} else { } else {
let corrected_cmd = check_similarity(command); let corrected_cmd = check_similarity(command);
if let Some(corrected_cmd) = corrected_cmd { 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) { match self.aliases.get(command) {
Some(command) => self.execute(command, args), Some(command) => self.execute(command, args),
None => { None => {
self.execute_command(command, args)?; let output = self.execute_command(command, args)?;
Ok(()) Ok(output)
} }
} }
} }
@ -156,7 +156,7 @@ impl CommandManager {
} }
pub trait Command: Send + Sync { 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 undo(&self);
fn redo(&self); fn redo(&self);
fn get_description(&self) -> String; fn get_description(&self) -> String;

View file

@ -16,13 +16,8 @@ use tracing::{debug, error, info, warn};
use super::handler::COMMAND_MANAGER; use super::handler::COMMAND_MANAGER;
use crate::error::{Result, ZenyxError, ZenyxErrorKind}; use crate::error::{Result, ZenyxError, ZenyxErrorKind};
#[derive(Default)]
struct CommandCompleter; struct CommandCompleter;
impl CommandCompleter {
fn new() -> Self {
CommandCompleter {}
}
}
impl Completer for CommandCompleter { impl Completer for CommandCompleter {
type Candidate = String; type Candidate = String;
@ -138,15 +133,19 @@ pub fn parse_command(input: &str) -> Result<Vec<String>> {
Ok(commands) 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() { 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 let commands = input
.split(|c| c == ';' || c == '\n') .split(|c| c == ';' || c == '\n')
.map(|slice| slice.to_string()) .map(|slice| slice.to_string())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let mut output = String::new();
for command in commands { for command in commands {
let command = command.trim(); let command = command.trim();
@ -166,20 +165,16 @@ pub fn evaluate_command(input: &str) -> Result<()> {
} else { } else {
None None
}; };
COMMAND_MANAGER match COMMAND_MANAGER.read().execute(cmd_name, args) {
.read() Ok(command_output) => output.push_str(&command_output),
.execute(cmd_name, args) Err(e) => {
.map_err(|e| { return Err(e);
ZenyxError::builder(ZenyxErrorKind::CommandExecution)
.with_message(format!("Failed to execute command: {cmd_name}"))
.with_context(format!("{e}"))
.build()
})?;
} }
Ok(())
} }
}
fn format_time() -> String { Ok(output)
}
pub fn format_time() -> String {
let now = SystemTime::now(); let now = SystemTime::now();
let duration = now.duration_since(UNIX_EPOCH).unwrap(); let duration = now.duration_since(UNIX_EPOCH).unwrap();
let total_seconds = duration.as_secs(); let total_seconds = duration.as_secs();
@ -201,7 +196,7 @@ pub async fn handle_repl() -> Result<()> {
let mut rl = Editor::<MyHelper, DefaultHistory>::new()?; let mut rl = Editor::<MyHelper, DefaultHistory>::new()?;
rl.set_helper(Some(MyHelper { rl.set_helper(Some(MyHelper {
hinter: HistoryHinter::new(), hinter: HistoryHinter::new(),
completer: CommandCompleter::new(), completer: CommandCompleter::default(),
})); }));
rl.bind_sequence( rl.bind_sequence(
@ -218,6 +213,7 @@ pub async fn handle_repl() -> Result<()> {
loop { loop {
let time = format_time(); let time = format_time();
let prompt = format!("[{}/{}] {}", time, "SHELL", ">>\t"); let prompt = format!("[{}/{}] {}", time, "SHELL", ">>\t");
let sig = rl.readline(&prompt.bright_white()); let sig = rl.readline(&prompt.bright_white());
match sig { match sig {

View file

@ -115,7 +115,7 @@ impl std::fmt::Display for ZenyxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
"Error: {}{}{}{}", "{}{}{}{}",
self.kind, self.kind,
self.message self.message
.as_ref() .as_ref()

View file

@ -1,35 +1,65 @@
use core::{panic::set_panic_hook, repl::setup, splash}; use core::{panic::set_panic_hook, repl::setup, splash};
use std::{fs::OpenOptions, io::BufWriter};
use colored::Colorize; use colored::Colorize;
use tokio::runtime; use tokio::runtime;
use tracing::level_filters::LevelFilter;
#[allow(unused_imports)] #[allow(unused_imports)]
use tracing::{debug, error, info, warn}; 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; use winit::event_loop::EventLoop;
pub mod cli;
pub mod core; pub mod core;
pub mod error; pub mod error;
pub mod metadata; pub mod metadata;
fn init_logger() { fn init_logger() {
let subscriber = tracing_subscriber::fmt() let stdout_layer = fmt::layer()
.with_max_level(LevelFilter::DEBUG)
.with_level(true) .with_level(true)
.compact() .compact()
.pretty() .pretty()
.log_internal_errors(false) .log_internal_errors(false)
.without_time() .without_time()
.with_thread_names(true) .with_thread_names(true);
.finish();
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() { async fn main() {
init_logger(); init_logger();
cli::parse();
let sysinfo = crate::metadata::SystemMetadata::current(); let sysinfo = crate::metadata::SystemMetadata::current();
set_panic_hook(); // set_panic_hook();
setup(); setup();
splash::print_splash(); splash::print_splash();
@ -51,8 +81,6 @@ async fn main() {
}; };
rt.block_on(core::repl::input::handle_repl()) rt.block_on(core::repl::input::handle_repl())
}); });
splash::print_splash();
info!("Type 'help' for a list of commands.");
match EventLoop::new() { match EventLoop::new() {
Ok(event_loop) => { Ok(event_loop) => {

View file

@ -1,15 +1,183 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt; use std::fmt;
use std::str::FromStr; 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 raw_cpuid::CpuId;
use sysinfo::{CpuRefreshKind, RefreshKind, System}; use sysinfo::{CpuRefreshKind, RefreshKind, System};
use tracing::error;
use wgpu::DeviceType; use wgpu::DeviceType;
mod build_info { mod build_info {
include!(concat!(env!("OUT_DIR"), "/built.rs")); 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Memory { pub struct Memory {
bytes: u64, bytes: u64,
@ -489,7 +657,9 @@ impl EngineInfo {
rustc_version: build_info::RUSTC_VERSION.to_string(), rustc_version: build_info::RUSTC_VERSION.to_string(),
wgpu_version: build_info::WGPU_VERSION.to_string(), wgpu_version: build_info::WGPU_VERSION.to_string(),
winit_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 memory: SystemMemory,
pub gpus: Vec<GPU>, pub gpus: Vec<GPU>,
pub compile_info: EngineInfo, 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 { impl SystemMetadata {
@ -533,6 +735,7 @@ impl SystemMetadata {
memory: SystemMemory::current(), memory: SystemMemory::current(),
gpus: GPU::current(), gpus: GPU::current(),
compile_info: EngineInfo::current(), compile_info: EngineInfo::current(),
os_info: OSInfo::current(),
} }
} }
@ -572,7 +775,8 @@ impl SystemMetadata {
}; };
format!( 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(), self.cpu.verbose_info(),
main_gpu_info, main_gpu_info,
other_gpu_list, other_gpu_list,
@ -600,5 +804,6 @@ mod tests {
assert!(!metadata.cpu.name.is_empty()); assert!(!metadata.cpu.name.is_empty());
assert!(metadata.memory.total.as_bytes() > 0); assert!(metadata.memory.total.as_bytes() > 0);
assert!(!metadata.compile_info.pkg_version.is_empty()); assert!(!metadata.compile_info.pkg_version.is_empty());
assert!(!metadata.os_info.name.is_empty());
} }
} }

0
zenyx.toml Normal file
View file