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

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