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 44ff41ea45
commit 73a6d5e9a8
Signed by: bitsyndicate
GPG key ID: 443E4198D6BBA6DE
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"
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 }

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) {
Ok(lockfile) => {
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 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 {}

View file

@ -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
}

View file

@ -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;
}

View file

@ -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);
@ -165,6 +181,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);
@ -182,6 +261,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 => {
@ -191,9 +276,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),
}
}
@ -275,9 +361,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) => {
@ -328,6 +414,7 @@ impl App<'_> {
window,
ctx: wgpu_ctx,
main_window: false,
terminal_state: None,
},
);
debug!("Spawned new child window: {:?}", window_id);
@ -344,21 +431,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;

View file

@ -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) {}

View file

@ -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;

View file

@ -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 {

View file

@ -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()

View file

@ -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) => {

View file

@ -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
View file