Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
422fe23cf2 | ||
|
|
065f68e9a8 | ||
|
|
b6b0a8c22b | ||
|
|
f1658619ad | ||
|
|
cf1e8a7c9e | ||
|
|
05081a7662 | ||
|
|
68a91c4af1 | ||
|
|
a2a5884a2a | ||
|
|
2d18aeaba2 | ||
|
|
8be866656a | ||
|
|
b58746fee4 |
Generated
+1
-1
@@ -1832,7 +1832,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lemur"
|
||||
version = "0.5.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atoi",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ description = "An emulator for the Virtual Boy."
|
||||
repository = "https://git.virtual-boy.com/PVB/lemur"
|
||||
publish = false
|
||||
license = "MIT"
|
||||
version = "0.5.0"
|
||||
version = "0.7.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+7
-1
@@ -24,7 +24,8 @@ use crate::{
|
||||
persistence::Persistence,
|
||||
window::{
|
||||
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow,
|
||||
GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, RegisterWindow, WorldWindow,
|
||||
GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, RegisterWindow, TerminalWindow,
|
||||
WorldWindow,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -243,6 +244,10 @@ impl ApplicationHandler<UserEvent> for Application {
|
||||
let registers = RegisterWindow::new(sim_id, &self.memory);
|
||||
self.open(event_loop, Box::new(registers));
|
||||
}
|
||||
UserEvent::OpenTerminal(sim_id) => {
|
||||
let terminal = TerminalWindow::new(sim_id, &self.client);
|
||||
self.open(event_loop, Box::new(terminal));
|
||||
}
|
||||
UserEvent::OpenDebugger(sim_id) => {
|
||||
let debugger =
|
||||
GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
|
||||
@@ -513,6 +518,7 @@ pub enum UserEvent {
|
||||
OpenWorlds(SimId),
|
||||
OpenFrameBuffers(SimId),
|
||||
OpenRegisters(SimId),
|
||||
OpenTerminal(SimId),
|
||||
OpenDebugger(SimId),
|
||||
OpenInput,
|
||||
OpenHotkeys,
|
||||
|
||||
+51
-5
@@ -43,6 +43,13 @@ impl SimId {
|
||||
Self::Player2 => 1,
|
||||
}
|
||||
}
|
||||
pub const fn from_index(index: usize) -> Option<Self> {
|
||||
match index {
|
||||
0 => Some(Self::Player1),
|
||||
1 => Some(Self::Player2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Display for SimId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -169,8 +176,9 @@ pub struct Emulator {
|
||||
renderers: HashMap<SimId, TextureSink>,
|
||||
messages: HashMap<SimId, mpsc::Sender<Toast>>,
|
||||
debuggers: HashMap<SimId, DebugInfo>,
|
||||
stdouts: HashMap<SimId, mpsc::Sender<String>>,
|
||||
watched_regions: HashMap<MemoryRange, Weak<MemoryRegion>>,
|
||||
eye_contents: Vec<u8>,
|
||||
eye_contents: [Vec<u8>; 2],
|
||||
audio_samples: Vec<f32>,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
@@ -195,8 +203,9 @@ impl Emulator {
|
||||
renderers: HashMap::new(),
|
||||
messages: HashMap::new(),
|
||||
debuggers: HashMap::new(),
|
||||
stdouts: HashMap::new(),
|
||||
watched_regions: HashMap::new(),
|
||||
eye_contents: vec![0u8; 384 * 224 * 2],
|
||||
eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]],
|
||||
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
|
||||
buffer: vec![],
|
||||
})
|
||||
@@ -228,8 +237,15 @@ impl Emulator {
|
||||
|
||||
let index = sim_id.to_index();
|
||||
while self.sims.len() <= index {
|
||||
let new_index = self.sims.len();
|
||||
self.sims.push(Sim::new());
|
||||
self.sim_state[index].store(SimState::NoGame, Ordering::Release);
|
||||
if self
|
||||
.stdouts
|
||||
.contains_key(&SimId::from_index(new_index).unwrap())
|
||||
{
|
||||
self.sims[new_index].watch_stdout(true);
|
||||
}
|
||||
self.sim_state[new_index].store(SimState::NoGame, Ordering::Release);
|
||||
}
|
||||
let sim = &mut self.sims[index];
|
||||
sim.reset();
|
||||
@@ -471,6 +487,20 @@ impl Emulator {
|
||||
self.state.store(EmulatorState::Paused, Ordering::Release);
|
||||
}
|
||||
|
||||
// stdout
|
||||
self.stdouts.retain(|sim_id, stdout| {
|
||||
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
|
||||
return true;
|
||||
};
|
||||
if let Some(text) = sim.take_stdout() {
|
||||
if stdout.send(text).is_err() {
|
||||
sim.watch_stdout(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
// Debug state
|
||||
if state == EmulatorState::Debugging {
|
||||
for sim_id in SimId::values() {
|
||||
@@ -498,9 +528,12 @@ impl Emulator {
|
||||
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
|
||||
continue;
|
||||
};
|
||||
if sim.read_pixels(&mut self.eye_contents) {
|
||||
if sim.read_pixels(&mut self.eye_contents[sim_id.to_index()]) {
|
||||
idle = false;
|
||||
if renderer.queue_render(&self.eye_contents).is_err() {
|
||||
if renderer
|
||||
.queue_render(&self.eye_contents[sim_id.to_index()])
|
||||
.is_err()
|
||||
{
|
||||
self.renderers.remove(&sim_id);
|
||||
}
|
||||
}
|
||||
@@ -645,6 +678,13 @@ impl Emulator {
|
||||
};
|
||||
sim.remove_watchpoint(address, length, watch);
|
||||
}
|
||||
EmulatorCommand::WatchStdout(sim_id, stdout_sink) => {
|
||||
self.stdouts.insert(sim_id, stdout_sink);
|
||||
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
|
||||
return;
|
||||
};
|
||||
sim.watch_stdout(true);
|
||||
}
|
||||
EmulatorCommand::SetAudioEnabled(p1, p2) => {
|
||||
self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
|
||||
self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
|
||||
@@ -665,6 +705,10 @@ impl Emulator {
|
||||
sim.set_keys(keys);
|
||||
}
|
||||
}
|
||||
EmulatorCommand::Screenshot(sim_id, sender) => {
|
||||
let contents = self.eye_contents[sim_id.to_index()].clone();
|
||||
let _ = sender.send(contents);
|
||||
}
|
||||
EmulatorCommand::Exit(done) => {
|
||||
for sim_id in SimId::values() {
|
||||
if let Err(error) = self.save_sram(sim_id) {
|
||||
@@ -718,11 +762,13 @@ pub enum EmulatorCommand {
|
||||
RemoveBreakpoint(SimId, u32),
|
||||
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
|
||||
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
|
||||
WatchStdout(SimId, mpsc::Sender<String>),
|
||||
SetAudioEnabled(bool, bool),
|
||||
Link,
|
||||
Unlink,
|
||||
Reset(SimId),
|
||||
SetKeys(SimId, VBKey),
|
||||
Screenshot(SimId, oneshot::Sender<Vec<u8>>),
|
||||
Exit(oneshot::Sender<()>),
|
||||
}
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ extern "C" fn on_write(
|
||||
sim: *mut VB,
|
||||
address: u32,
|
||||
_type: VBDataType,
|
||||
_value: *mut i32,
|
||||
value: *mut i32,
|
||||
_cycles: *mut u32,
|
||||
_cancel: *mut c_int,
|
||||
) -> c_int {
|
||||
@@ -238,6 +238,14 @@ extern "C" fn on_write(
|
||||
// There is no way for the userdata to be null or otherwise invalid.
|
||||
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
|
||||
|
||||
// If we're monitoring stdout, track this write
|
||||
if let Some(stdout) = data.stdout.as_mut() {
|
||||
let normalized_hw_address = address & 0x0700003f;
|
||||
if normalized_hw_address == 0x02000030 {
|
||||
stdout.push(unsafe { *value } as u8);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(start) = data.write_watchpoints.start_of_range_containing(address) {
|
||||
let watch = if data.read_watchpoints.contains(address) {
|
||||
VBWatchpointType::Access
|
||||
@@ -263,6 +271,7 @@ struct VBState {
|
||||
breakpoints: Vec<u32>,
|
||||
read_watchpoints: AddressSet,
|
||||
write_watchpoints: AddressSet,
|
||||
stdout: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl VBState {
|
||||
@@ -306,6 +315,7 @@ impl Sim {
|
||||
breakpoints: vec![],
|
||||
read_watchpoints: AddressSet::new(),
|
||||
write_watchpoints: AddressSet::new(),
|
||||
stdout: None,
|
||||
};
|
||||
unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
|
||||
unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
|
||||
@@ -564,7 +574,9 @@ impl Sim {
|
||||
state.write_watchpoints.remove(address, length);
|
||||
let needs_execute = state.needs_execute_callback();
|
||||
if state.write_watchpoints.is_empty() {
|
||||
if state.stdout.is_none() {
|
||||
unsafe { vb_set_write_callback(self.sim, None) };
|
||||
}
|
||||
if !needs_execute {
|
||||
unsafe { vb_set_execute_callback(self.sim, None) };
|
||||
}
|
||||
@@ -586,11 +598,40 @@ impl Sim {
|
||||
data.breakpoints.clear();
|
||||
data.read_watchpoints.clear();
|
||||
data.write_watchpoints.clear();
|
||||
let needs_write = data.stdout.is_some();
|
||||
unsafe { vb_set_read_callback(self.sim, None) };
|
||||
if !needs_write {
|
||||
unsafe { vb_set_write_callback(self.sim, None) };
|
||||
}
|
||||
unsafe { vb_set_execute_callback(self.sim, None) };
|
||||
}
|
||||
|
||||
pub fn watch_stdout(&mut self, watch: bool) {
|
||||
let data = self.get_state();
|
||||
if watch {
|
||||
if data.stdout.is_none() {
|
||||
data.stdout = Some(vec![]);
|
||||
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
|
||||
}
|
||||
} else {
|
||||
data.stdout.take();
|
||||
if data.write_watchpoints.is_empty() {
|
||||
unsafe { vb_set_write_callback(self.sim, None) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_stdout(&mut self) -> Option<String> {
|
||||
let data = self.get_state();
|
||||
let stdout = data.stdout.take()?;
|
||||
let string = match String::from_utf8(stdout) {
|
||||
Ok(str) => str,
|
||||
Err(err) => String::from_utf8_lossy(err.as_bytes()).into_owned(),
|
||||
};
|
||||
data.stdout = Some(vec![]);
|
||||
Some(string)
|
||||
}
|
||||
|
||||
pub fn stop_reason(&mut self) -> Option<StopReason> {
|
||||
let data = self.get_state();
|
||||
let reason = data.stop_reason.take();
|
||||
|
||||
@@ -12,7 +12,7 @@ impl RegisterInfo {
|
||||
pub fn to_description(&self) -> String {
|
||||
let mut string = format!("name:{}", self.name);
|
||||
if let Some(alt) = self.alt_name {
|
||||
string.push_str(&format!(";alt-name:{}", alt));
|
||||
string.push_str(&format!(";alt-name:{alt}"));
|
||||
}
|
||||
string.push_str(&format!(
|
||||
";bitsize:32;offset:{};encoding:uint;format:hex;set:{};dwarf:{}",
|
||||
@@ -21,7 +21,7 @@ impl RegisterInfo {
|
||||
self.dwarf
|
||||
));
|
||||
if let Some(generic) = self.generic {
|
||||
string.push_str(&format!(";generic:{}", generic));
|
||||
string.push_str(&format!(";generic:{generic}"));
|
||||
}
|
||||
string
|
||||
}
|
||||
|
||||
+9
-2
@@ -227,7 +227,7 @@ impl Mappings for InputMapping {
|
||||
for (keyboard_key, keys) in &self.keys {
|
||||
let name = match keyboard_key {
|
||||
PhysicalKey::Code(code) => format!("{code:?}"),
|
||||
k => format!("{:?}", k),
|
||||
k => format!("{k:?}"),
|
||||
};
|
||||
for key in keys.iter() {
|
||||
results.entry(key).or_default().push(name.clone());
|
||||
@@ -471,11 +471,12 @@ pub enum Command {
|
||||
FastForward(u32),
|
||||
Reset,
|
||||
PauseResume,
|
||||
Screenshot,
|
||||
// if you update this, update Command::all and add a default
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn all() -> [Self; 6] {
|
||||
pub fn all() -> [Self; 7] {
|
||||
[
|
||||
Self::OpenRom,
|
||||
Self::Quit,
|
||||
@@ -483,6 +484,7 @@ impl Command {
|
||||
Self::Reset,
|
||||
Self::FrameAdvance,
|
||||
Self::FastForward(0),
|
||||
Self::Screenshot,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -494,6 +496,7 @@ impl Command {
|
||||
Self::Reset => "Reset",
|
||||
Self::FrameAdvance => "Frame Advance",
|
||||
Self::FastForward(_) => "Fast Forward",
|
||||
Self::Screenshot => "Screenshot",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,6 +542,10 @@ impl Default for Shortcuts {
|
||||
Command::FastForward(0),
|
||||
KeyboardShortcut::new(Modifiers::NONE, Key::Space),
|
||||
);
|
||||
shortcuts.set(
|
||||
Command::Screenshot,
|
||||
KeyboardShortcut::new(Modifiers::NONE, Key::F12),
|
||||
);
|
||||
shortcuts
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -44,9 +44,9 @@ fn set_panic_handler() {
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let mut message = String::new();
|
||||
if let Some(msg) = info.payload().downcast_ref::<&str>() {
|
||||
message += &format!("{}\n", msg);
|
||||
message += &format!("{msg}\n");
|
||||
} else if let Some(msg) = info.payload().downcast_ref::<String>() {
|
||||
message += &format!("{}\n", msg);
|
||||
message += &format!("{msg}\n");
|
||||
}
|
||||
if let Some(location) = info.location() {
|
||||
message += &format!(
|
||||
@@ -56,9 +56,9 @@ fn set_panic_handler() {
|
||||
);
|
||||
}
|
||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||
message += &format!("stack trace:\n{:#}\n", backtrace);
|
||||
message += &format!("stack trace:\n{backtrace:#}\n");
|
||||
|
||||
eprint!("{}", message);
|
||||
eprint!("{message}");
|
||||
|
||||
let Some(project_dirs) = directories::ProjectDirs::from("com", "virtual-boy", "Lemur")
|
||||
else {
|
||||
@@ -72,7 +72,7 @@ fn set_panic_handler() {
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
let logfile_name = format!("crash-{}.txt", timestamp);
|
||||
let logfile_name = format!("crash-{timestamp}.txt");
|
||||
let _ = std::fs::write(data_dir.join(logfile_name), message);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ pub use game::GameWindow;
|
||||
pub use gdb::GdbServerWindow;
|
||||
pub use hotkeys::HotkeysWindow;
|
||||
pub use input::InputWindow;
|
||||
pub use terminal::TerminalWindow;
|
||||
pub use vip::{
|
||||
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
|
||||
};
|
||||
@@ -17,6 +18,7 @@ mod game_screen;
|
||||
mod gdb;
|
||||
mod hotkeys;
|
||||
mod input;
|
||||
mod terminal;
|
||||
mod utils;
|
||||
mod vip;
|
||||
|
||||
|
||||
+81
-3
@@ -6,11 +6,12 @@ use crate::{
|
||||
input::{Command, ShortcutProvider},
|
||||
persistence::Persistence,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use egui::{
|
||||
Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, Vec2,
|
||||
ViewportBuilder, ViewportCommand, ViewportId, Window, menu,
|
||||
};
|
||||
use egui_toast::{Toast, Toasts};
|
||||
use egui_toast::{Toast, ToastKind, ToastOptions, Toasts};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use winit::event_loop::EventLoopProxy;
|
||||
|
||||
@@ -69,7 +70,7 @@ impl GameWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn show_menu(&mut self, ctx: &Context, ui: &mut Ui, toasts: &mut Toasts) {
|
||||
let state = self.client.emulator_state();
|
||||
let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
|
||||
let can_pause = is_ready && state == EmulatorState::Running;
|
||||
@@ -113,6 +114,16 @@ impl GameWindow {
|
||||
self.client
|
||||
.send_command(EmulatorCommand::SetSpeed(speed as f64));
|
||||
}
|
||||
Command::Screenshot => {
|
||||
let autopause = state == EmulatorState::Running && can_pause;
|
||||
if autopause {
|
||||
self.client.send_command(EmulatorCommand::Pause);
|
||||
}
|
||||
pollster::block_on(self.take_screenshot(toasts));
|
||||
if autopause {
|
||||
self.client.send_command(EmulatorCommand::Resume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +189,17 @@ impl GameWindow {
|
||||
self.client.send_command(EmulatorCommand::FrameAdvance);
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
is_ready,
|
||||
self.button_for(ui.ctx(), "Screenshot", Command::Screenshot),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
pollster::block_on(self.take_screenshot(toasts));
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui));
|
||||
ui.menu_button("Multiplayer", |ui| {
|
||||
@@ -204,6 +226,12 @@ impl GameWindow {
|
||||
}
|
||||
});
|
||||
ui.menu_button("Tools", |ui| {
|
||||
if ui.button("Terminal").clicked() {
|
||||
self.proxy
|
||||
.send_event(UserEvent::OpenTerminal(self.sim_id))
|
||||
.unwrap();
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("GDB Server").clicked() {
|
||||
self.proxy
|
||||
.send_event(UserEvent::OpenDebugger(self.sim_id))
|
||||
@@ -256,6 +284,56 @@ impl GameWindow {
|
||||
});
|
||||
}
|
||||
|
||||
async fn take_screenshot(&self, toasts: &mut Toasts) {
|
||||
match self.try_take_screenshot().await {
|
||||
Ok(Some(path)) => {
|
||||
toasts.add(
|
||||
Toast::new()
|
||||
.kind(ToastKind::Info)
|
||||
.options(ToastOptions::default().duration_in_seconds(5.0))
|
||||
.text(format!("Saved to {path}")),
|
||||
);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
toasts.add(
|
||||
Toast::new()
|
||||
.kind(ToastKind::Error)
|
||||
.options(ToastOptions::default().duration_in_seconds(5.0))
|
||||
.text(format!("{error:#}")),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_take_screenshot(&self) -> anyhow::Result<Option<String>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.client
|
||||
.send_command(EmulatorCommand::Screenshot(self.sim_id, tx));
|
||||
let bytes = rx.await.context("Could not take screenshot")?;
|
||||
let file = rfd::AsyncFileDialog::new()
|
||||
.add_filter("PNG images", &["png"])
|
||||
.set_file_name("screenshot.png")
|
||||
.save_file()
|
||||
.await;
|
||||
let Some(file) = file else {
|
||||
return Ok(None);
|
||||
};
|
||||
if bytes.len() != 384 * 224 * 2 {
|
||||
anyhow::bail!("Unexpected screenshot size");
|
||||
}
|
||||
let mut screencap = image::GrayImage::new(384 * 2, 224);
|
||||
for (index, pixel) in bytes.into_iter().enumerate() {
|
||||
let x = (index / 2) % 384 + ((index % 2) * 384);
|
||||
let y = (index / 2) / 384;
|
||||
screencap.put_pixel(x as u32, y as u32, image::Luma([pixel]));
|
||||
}
|
||||
screencap
|
||||
.save(&file.path())
|
||||
.context("Could not save screenshot")?;
|
||||
Ok(Some(file.path().display().to_string()))
|
||||
}
|
||||
|
||||
fn show_options_menu(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
ui.menu_button("Video", |ui| {
|
||||
ui.menu_button("Screen Size", |ui| {
|
||||
@@ -472,7 +550,7 @@ impl AppWindow for GameWindow {
|
||||
.exact_height(22.0)
|
||||
.show(ctx, |ui| {
|
||||
menu::bar(ui, |ui| {
|
||||
self.show_menu(ctx, ui);
|
||||
self.show_menu(ctx, ui, &mut toasts);
|
||||
});
|
||||
});
|
||||
if self.color_picker.is_some() {
|
||||
|
||||
@@ -71,7 +71,7 @@ impl GameScreen {
|
||||
module: &shader,
|
||||
entry_point: Some(entry_point),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||
format: render_state.target_format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
use std::{collections::VecDeque, sync::mpsc};
|
||||
|
||||
use egui::{
|
||||
Align, CentralPanel, Context, FontFamily, Label, RichText, ScrollArea, ViewportBuilder,
|
||||
ViewportId,
|
||||
};
|
||||
|
||||
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
|
||||
|
||||
use super::AppWindow;
|
||||
|
||||
const SCROLLBACK: usize = 1000;
|
||||
|
||||
pub struct TerminalWindow {
|
||||
sim_id: SimId,
|
||||
receiver: mpsc::Receiver<String>,
|
||||
lines: VecDeque<String>,
|
||||
}
|
||||
|
||||
impl TerminalWindow {
|
||||
pub fn new(sim_id: SimId, client: &EmulatorClient) -> Self {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
client.send_command(EmulatorCommand::WatchStdout(sim_id, sender));
|
||||
let mut lines = VecDeque::new();
|
||||
lines.push_back(String::new());
|
||||
Self {
|
||||
sim_id,
|
||||
receiver,
|
||||
lines,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppWindow for TerminalWindow {
|
||||
fn viewport_id(&self) -> ViewportId {
|
||||
ViewportId::from_hash_of(format!("terminal-{}", self.sim_id))
|
||||
}
|
||||
|
||||
fn sim_id(&self) -> SimId {
|
||||
self.sim_id
|
||||
}
|
||||
|
||||
fn initial_viewport(&self) -> ViewportBuilder {
|
||||
ViewportBuilder::default()
|
||||
.with_title(format!("Terminal ({})", self.sim_id))
|
||||
.with_inner_size((640.0, 480.0))
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context) {
|
||||
if let Ok(text) = self.receiver.try_recv() {
|
||||
let mut rest = text.as_str();
|
||||
while let Some(index) = rest.find('\n') {
|
||||
let (line, lines) = rest.split_at(index);
|
||||
let current = self.lines.back_mut().unwrap();
|
||||
current.push_str(line);
|
||||
self.lines.push_back(String::new());
|
||||
if self.lines.len() > SCROLLBACK {
|
||||
self.lines.pop_front();
|
||||
}
|
||||
rest = &lines[1..];
|
||||
}
|
||||
self.lines.back_mut().unwrap().push_str(rest);
|
||||
}
|
||||
CentralPanel::default().show(ctx, |ui| {
|
||||
ScrollArea::vertical()
|
||||
.stick_to_bottom(true)
|
||||
.auto_shrink([false, false])
|
||||
.animated(false)
|
||||
.show(ui, |ui| {
|
||||
for line in &self.lines {
|
||||
let label = Label::new(RichText::new(line).family(FontFamily::Monospace))
|
||||
.halign(Align::LEFT)
|
||||
.wrap();
|
||||
ui.add(label);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user