Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
2025-06-22 03:54:44 +01:00
16 changed files with 539 additions and 440 deletions
+12 -2
View File
@@ -7,6 +7,7 @@ default-run = "emulator"
[lib]
name = "dsa_rs"
path = "src/lib.rs"
crate-type = ["cdylib", "rlib"]
[[bin]]
name = "emulator"
@@ -16,15 +17,24 @@ required-features = ["config"]
common = { path = "../common" }
assembler = { path = "../assembler" }
dsa_editor = { path = "../dsa_editor" }
eframe = "0.31.1"
eframe = { version = "0.31.1" }
egui = "0.31.1"
rfd = "0.15.3"
dirs = "6.0.0"
discord-presence = { version = "1.6.0", optional = true }
toml = { version = "0.8.23", optional = true }
serde = { version = "1.0.219", features = ["derive"], optional = true }
egui_file = "0.22.1"
[features]
default = ["config"]
discord-rpc = ["dep:discord-presence"]
config = ["dep:toml", "dep:serde"]
# Add support for Android for the fun of it.
[target.'cfg(target_os = "android")'.dependencies]
winit = { version = "0.30.11", features = ["android-native-activity"] }
# jni = "0.21.1"
[target.'cfg(target_os = "android")'.dependencies.eframe]
version = "0.31.1"
features = ["android-native-activity"]
-1
View File
@@ -1,2 +1 @@
#[cfg(feature = "discord-rpc")]
pub mod rpc;
+33 -9
View File
@@ -7,7 +7,7 @@
//!
//! # Configuration
//!
//! This may be disabled like so in your `.dsarc.toml` file:
//! This may be disabled like so in your `.dsa.emulator.toml` file:
//!
//! ```toml
//! [misc]
@@ -16,24 +16,23 @@
//!
//! Alternatively, you can hide this in your Discord settings.
use std::{
path::PathBuf,
sync::{
Arc,
mpsc::{Receiver, Sender},
},
time::Duration,
};
#[cfg(feature = "discord-rpc")]
use std::{path::PathBuf, sync::Arc, time::Duration};
use std::sync::mpsc::{Receiver, Sender};
#[cfg(feature = "discord-rpc")]
use discord_presence::{Client, DiscordError, models::ActivityTimestamps};
use crate::emulator::config::Config;
#[derive(Debug)]
#[cfg(feature = "discord-rpc")]
pub enum RpcClientError {
Client(DiscordError),
}
#[cfg(feature = "discord-rpc")]
impl std::fmt::Display for RpcClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@@ -42,8 +41,10 @@ impl std::fmt::Display for RpcClientError {
}
}
#[cfg(feature = "discord-rpc")]
impl std::error::Error for RpcClientError {}
#[cfg(feature = "discord-rpc")]
impl From<DiscordError> for RpcClientError {
fn from(err: DiscordError) -> Self {
Self::Client(err)
@@ -52,6 +53,7 @@ impl From<DiscordError> for RpcClientError {
/// The type of activity the user is currently doing.
#[derive(Debug, Clone)]
#[cfg(feature = "discord-rpc")]
pub enum Activity {
Idle,
EditingFile(PathBuf),
@@ -59,6 +61,7 @@ pub enum Activity {
/// Messages to send over the wire.
#[derive(Debug)]
#[cfg(feature = "discord-rpc")]
pub enum Message {
/// Sent when we want to update the [`Context`].
Update(Activity),
@@ -66,9 +69,11 @@ pub enum Message {
Stop,
}
#[cfg(feature = "discord-rpc")]
unsafe impl Send for Message {}
#[derive(Debug, Clone)]
#[cfg(feature = "discord-rpc")]
pub struct RpcClient {
/// Sends updates to [`Context`] (our state).
sender: Sender<Message>,
@@ -76,6 +81,7 @@ pub struct RpcClient {
thread_handle: Option<Arc<std::thread::JoinHandle<()>>>,
}
#[cfg(feature = "discord-rpc")]
impl RpcClient {
#[expect(clippy::unreadable_literal)]
/// Sets up the [`RpcClient`].
@@ -169,6 +175,7 @@ impl RpcClient {
}
// Possibly unneeded but good practice.
#[cfg(feature = "discord-rpc")]
impl Drop for RpcClient {
fn drop(&mut self) {
self.stop();
@@ -181,14 +188,31 @@ impl Drop for RpcClient {
}
}
/// Stub for when the feature is disabled.
#[cfg(not(feature = "discord-rpc"))]
pub struct RpcClient {}
/// Stub for when the feature is disabled.
#[cfg(not(feature = "discord-rpc"))]
pub enum Message {}
/// Stub for when the feature is disabled.
#[cfg(not(feature = "discord-rpc"))]
pub enum Activity {}
/// Gets the discord [`RpcClient`] or returns None if this has been disabled in the config
/// options.
#[cfg(feature = "config")]
#[allow(clippy::needless_pass_by_value, unused_variables)]
pub fn get_rpc_client_or_none(
config: &Config,
rpc_sender: Sender<Message>,
rpc_reciever: Receiver<Message>,
) -> Result<Option<RpcClient>, Box<dyn std::error::Error + 'static>> {
#[cfg(not(feature = "discord-rpc"))]
return Ok(None);
#[cfg(feature = "discord-rpc")]
if config.misc.use_discord_rpc {
Ok(Some(RpcClient::new(rpc_sender, rpc_reciever)?))
} else {
+3 -3
View File
@@ -1,4 +1,3 @@
#[cfg(feature = "discord-rpc")]
use std::sync::Arc;
use std::{
sync::mpsc::{self, Receiver, Sender},
@@ -6,7 +5,7 @@ use std::{
time::Duration,
};
#[cfg(feature = "discord-rpc")]
#[allow(unused_imports)]
use crate::emulator::misc::rpc::{Activity, RpcClient};
use crate::emulator::system::{
@@ -17,11 +16,12 @@ use crate::emulator::system::{
use common::prelude::*;
#[expect(clippy::too_many_lines)]
#[allow(unused_variables)]
pub fn run_emulator(
cmd_rx: &Receiver<Command>,
state_tx: &Sender<State>,
mut processor: Processor,
#[cfg(feature = "discord-rpc")] rpc_client: Option<&Arc<RpcClient>>,
rpc_client: Option<&Arc<RpcClient>>,
) {
println!("INFO: Starting emulator.");
+148 -62
View File
@@ -1,10 +1,14 @@
use std::{ffi::OsStr, path::PathBuf, sync::mpsc::Sender};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
sync::mpsc::Sender,
};
use common::prelude::Instruction;
use egui::{Align, Context, Key, Layout, Ui};
use rfd::FileDialog;
use dsa_editor::{CodeEditor, ColorTheme, Syntax};
use egui_file::FileDialog;
use crate::emulator::{
system::model::{Command, State},
@@ -29,6 +33,10 @@ pub struct Editor {
cursor_col: usize,
cursor_line: usize,
// file dialogs
open_file_dialog: Option<FileDialog>,
save_file_dialog: Option<FileDialog>,
// other
visible: bool,
sender: Sender<Command>,
@@ -98,6 +106,8 @@ impl Editor {
load_offset: 0,
offset_str: String::new(),
error: None,
open_file_dialog: None,
save_file_dialog: None,
}
}
@@ -130,36 +140,31 @@ impl Editor {
}
fn save(&mut self) {
let work_dir = std::env::current_dir().unwrap_or_else(|_| {
dirs::home_dir().expect(
"Couldn't get your current working directory or your home directory.",
)
});
if let Some(path) = &self.path {
if let Err(why) = std::fs::write(path, &self.text) {
self.error = Some(format!("Failed to save file: {why}"));
return;
}
self.buffer = self.text.clone();
self.unsaved = false;
return;
if self.open_file_dialog.is_some() {
// TODO: Flash an error stating you can only have one menu open at once.
self.open_file_dialog = None;
}
if let Some(path) = FileDialog::new()
.add_filter("Assembly Files or Binaries", &["dsa", "dsb"])
.add_filter("all", &["*"])
.set_directory(&work_dir)
.save_file()
{
if let Err(why) = std::fs::write(&path, &self.text) {
if let Some(path) = &self.path {
// Save to existing path
if let Err(why) = std::fs::write(path, &self.text) {
self.error = Some(format!("Failed to save file: {why}"));
} else {
self.path = Some(path);
self.buffer = self.text.clone();
self.unsaved = false;
}
} else {
// Open the save dialog.
let work_dir = std::env::current_dir().unwrap_or_else(|_| {
dirs::home_dir().expect(
"Couldn't get your current working directory or your home directory.",
)
});
if self.save_file_dialog.is_none() {
let mut dialog = FileDialog::save_file(Some(work_dir));
dialog.open();
self.save_file_dialog = Some(dialog);
}
}
}
@@ -170,46 +175,125 @@ impl Editor {
)
});
if let Some(path) = FileDialog::new()
.add_filter("Assembly Files or Binaries", &["dsa", "dsb"])
.add_filter("all", &["*"])
.set_directory(&work_dir)
.pick_file()
{
match path.extension().and_then(|ext| ext.to_str()) {
Some("dsb") => {
let contents = match std::fs::read(&path) {
Ok(contents) => contents,
Err(why) => {
self.error = Some(format!("Failed to read file: {why}"));
return;
}
};
// if let Some(path) = FileDialog::new()
// .add_filter("Assembly Files or Binaries", &["dsa", "dsb"])
// .add_filter("all", &["*"])
// .set_directory(&work_dir)
// .pick_file()
// {
// match path.extension().and_then(|ext| ext.to_str()) {
// Some("dsb") => {
// let contents = match std::fs::read(&path) {
// Ok(contents) => contents,
// Err(why) => {
// self.error = Some(format!("Failed to read file: {why}"));
// return;
// }
// };
self.path = Some(path.clone());
self.output = contents;
self.unsaved = false;
self.text = String::from("Loaded Binary File!");
self.buffer = self.text.clone();
self.unsaved = false;
}
_ => {
if let Ok(contents) = std::fs::read_to_string(&path) {
self.path = Some(path.clone());
self.text.clone_from(&contents);
self.buffer = contents;
self.unsaved = false;
}
}
// self.path = Some(path.clone());
// self.output = contents;
// self.unsaved = false;
// self.text = String::from("Loaded Binary File!");
// self.buffer = self.text.clone();
// self.unsaved = false;
// }
// _ => {
// if let Ok(contents) = std::fs::read_to_string(&path) {
// self.path = Some(path.clone());
// self.text.clone_from(&contents);
// self.buffer = contents;
// self.unsaved = false;
// }
// }
// }
if self.save_file_dialog.is_some() {
// TODO: Flash an error stating you can only have one menu open at once.
self.save_file_dialog = None;
}
if self.open_file_dialog.is_none() {
if let Some(p) = &self.path {
let path = p.parent().map(Path::to_path_buf);
let mut dialog = FileDialog::open_file(path);
dialog.open();
self.open_file_dialog = Some(dialog);
} else {
let mut dialog = FileDialog::open_file(Some(work_dir));
dialog.open();
self.open_file_dialog = Some(dialog);
}
std::env::set_current_dir(
path.parent().expect("A file should be in a directory!"),
)
.expect("ERROR: Failed to set current working directory.");
}
}
fn handle_file_dialogs(&mut self, ctx: &egui::Context) {
// Handle open dialog
if let Some(dialog) = &mut self.open_file_dialog {
if dialog.show(ctx).selected() {
if let Some(file) = dialog.path() {
match std::fs::read_to_string(file) {
Ok(content) => {
self.text = content;
self.path = Some(file.to_path_buf());
self.unsaved = false;
self.error = None;
}
Err(e) => {
self.error = Some(format!("Failed to read file: {e}"));
}
}
}
self.open_file_dialog = None;
}
}
// Handle save dialog
if let Some(dialog) = &mut self.save_file_dialog {
if dialog.show(ctx).selected() {
if let Some(file) = dialog.path() {
match std::fs::write(file, &self.text) {
Ok(()) => {
self.path = Some(file.to_path_buf());
self.unsaved = false;
self.error = None;
}
Err(e) => {
self.error = Some(format!("Failed to save file: {e}"));
}
}
}
self.save_file_dialog = None;
}
}
}
// fn open(&mut self) {
// let work_dir = std::env::current_dir().unwrap_or_else(|_| {
// dirs::home_dir().expect(
// "Couldn't get your current working directory or your home directory.",
// )
// });
// if let Some(path) = FileDialog::new()
// .add_filter("Assembly Files or Binaries", &["dsa", "dsb"])
// .add_filter("all", &["*"])
// .set_directory(&work_dir)
// .pick_file()
// {
// if let Ok(contents) = std::fs::read_to_string(&path) {
// self.path = Some(path.clone());
// self.text.clone_from(&contents);
// self.buffer = contents;
// self.unsaved = false;
// }
// std::env::set_current_dir(
// path.parent().expect("A file should be in a directory!"),
// )
// .expect("ERROR: Failed to set current working directory.");
// }
// }
fn render_output(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
// Output area with synchronized scrolling
egui::ScrollArea::vertical()
@@ -336,7 +420,9 @@ impl Editor {
});
}
fn render_toolbar(&mut self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
fn render_toolbar(&mut self, _state: &mut State, ui: &mut Ui, ctx: &Context) {
self.handle_file_dialogs(ctx);
ui.horizontal(|ui| {
ui.label(format!("File type: {}", self.extension()));
ui.label(format!("Filename: {}", self.filename()));
+114
View File
@@ -13,3 +13,117 @@
)]
pub mod emulator;
use std::{
sync::{
Arc,
mpsc::{Receiver, Sender},
},
thread,
};
#[cfg(target_os = "android")]
use winit::platform::android::{EventLoopBuilderExtAndroid, activity::AndroidApp};
use crate::emulator::{
misc::rpc::RpcClient,
system::{
emulator::run_emulator,
memory::MainStore,
model::{Command, State},
processor::Processor,
},
ui::{
control_unit::ControlPanel, display::Display, editor::Editor,
interface::EmulatorUI, memory_inspector::MemoryInspector,
stack_inspector::StackInspector,
},
};
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
pub fn android_main(app: AndroidApp) -> Result<(), Box<dyn std::error::Error>> {
use crate::emulator::{config::Config, misc::rpc::get_rpc_client_or_none};
use std::path::Path;
// Initialize channels and read in configuration.
let (cmd_sender, cmd_receiver) = std::sync::mpsc::channel();
let (state_sender, state_reciever) = std::sync::mpsc::channel();
let config = Config::load(Path::new(".dsa.emulator.toml"))?;
// Setup RPC if enabled.
let (rpc_sender, rpc_reciever) = std::sync::mpsc::channel();
let rpc_client =
get_rpc_client_or_none(&config, rpc_sender, rpc_reciever)?.map(Arc::new);
setup_emulator(cmd_receiver, state_sender, rpc_client);
let ui = setup_ui(cmd_sender, state_reciever);
// Run UI.
#[allow(unused_variables)]
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
event_loop_builder: Some(Box::new(move |builder| {
#[cfg(target_os = "android")]
builder.with_android_app(app);
})),
..Default::default()
};
eframe::run_native(
"DSA Simulator (Damn Simple Architecture 🔥)",
options,
Box::new(move |cc| {
cc.egui_ctx.set_visuals(egui::Visuals::default());
Ok(Box::new(ui))
}),
)?;
Ok(())
}
pub fn setup_emulator(
cmd_receiver: Receiver<Command>,
state_sender: Sender<State>,
rpc_client: Option<Arc<RpcClient>>,
) {
let main_store = MainStore::new();
let processor = Processor::new(Box::new(main_store), vec![]);
thread::spawn(move || {
run_emulator(&cmd_receiver, &state_sender, processor, rpc_client.as_ref());
});
}
/// Creates the [`EmulatorUI`].
#[must_use]
pub fn setup_ui(
cmd_sender: Sender<Command>,
state_reciever: Receiver<State>,
) -> EmulatorUI {
let mut ui = EmulatorUI::new(cmd_sender.clone(), state_reciever);
// Create UI modules.
let control_unit = ControlPanel::new(cmd_sender.clone());
ui.add_component(Box::new(control_unit));
let mem_inspector = MemoryInspector::new(cmd_sender.clone());
ui.add_component(Box::new(mem_inspector));
let stack_inspector = StackInspector::new();
ui.add_component(Box::new(stack_inspector));
let editor = Editor::new(cmd_sender);
ui.add_component(Box::new(editor));
let display = Display::new();
ui.add_component(Box::new(display));
let history = emulator::ui::history::History::new();
ui.add_component(Box::new(history));
ui
}
+5 -74
View File
@@ -1,28 +1,7 @@
#[cfg(feature = "discord-rpc")]
use std::path::Path;
use std::sync::Arc;
use std::{
path::Path,
sync::mpsc::{Receiver, Sender},
thread,
};
#[cfg(feature = "discord-rpc")]
use dsa_rs::emulator::misc::rpc::{RpcClient, get_rpc_client_or_none};
use dsa_rs::emulator::{
config::Config,
system::{
emulator::run_emulator,
memory::MainStore,
model::{Command, State},
processor::Processor,
},
ui::{
control_unit::ControlPanel, display::Display, editor::Editor,
interface::EmulatorUI, memory_inspector::MemoryInspector,
stack_inspector::StackInspector,
},
};
use dsa_rs::emulator::{config::Config, misc::rpc::get_rpc_client_or_none};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize channels and read in configuration.
@@ -31,21 +10,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load(Path::new(".dsa.emulator.toml"))?;
// Setup RPC if enabled.
#[cfg(feature = "discord-rpc")]
let (rpc_sender, rpc_reciever) = std::sync::mpsc::channel();
#[cfg(feature = "discord-rpc")]
let rpc_client =
get_rpc_client_or_none(&config, rpc_sender, rpc_reciever)?.map(Arc::new);
#[cfg(feature = "discord-rpc")]
setup_emulator(cmd_receiver, state_sender, rpc_client);
#[cfg(not(feature = "discord-rpc"))]
setup_emulator(cmd_receiver, state_sender);
dsa_rs::setup_emulator(cmd_receiver, state_sender, rpc_client);
let ui = setup_ui(cmd_sender, state_reciever);
let ui = dsa_rs::setup_ui(cmd_sender, state_reciever);
// Run UI.
#[allow(unused_variables)]
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
..Default::default()
@@ -62,47 +37,3 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn setup_emulator(
cmd_receiver: Receiver<Command>,
state_sender: Sender<State>,
#[cfg(feature = "discord-rpc")] rpc_client: Option<Arc<RpcClient>>,
) {
let main_store = MainStore::new();
let processor = Processor::new(Box::new(main_store), vec![]);
thread::spawn(move || {
#[cfg(feature = "discord-rpc")]
run_emulator(&cmd_receiver, &state_sender, processor, rpc_client.as_ref());
#[cfg(not(feature = "discord-rpc"))]
run_emulator(&cmd_receiver, &state_sender, processor);
});
}
/// Creates the [`EmulatorUI`].
fn setup_ui(cmd_sender: Sender<Command>, state_reciever: Receiver<State>) -> EmulatorUI {
let mut ui = EmulatorUI::new(cmd_sender.clone(), state_reciever);
// Create UI modules.
let control_unit = ControlPanel::new(cmd_sender.clone());
ui.add_component(Box::new(control_unit));
let mem_inspector = MemoryInspector::new(cmd_sender.clone());
ui.add_component(Box::new(mem_inspector));
let stack_inspector = StackInspector::new();
ui.add_component(Box::new(stack_inspector));
let editor = Editor::new(cmd_sender.clone());
ui.add_component(Box::new(editor));
let display = Display::new();
ui.add_component(Box::new(display));
let history = dsa_rs::emulator::ui::history::History::new();
ui.add_component(Box::new(history));
ui
}