Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17794aba0b | |||
| aed22a3dbc | |||
| 5ba5a52c14 | |||
| e5ce246760 | |||
| a3fa7eb9d2 | |||
| fdc923621e | |||
| ebd3d4008b | |||
| ae56f78075 | |||
| b0ecc5623c | |||
| 3715ecf841 | |||
| 1ec12fa728 |
23
.gitignore
vendored
23
.gitignore
vendored
@@ -1,22 +1 @@
|
|||||||
# ---> Rust
|
/target
|
||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
|
||||||
*.pdb
|
|
||||||
|
|
||||||
# RustRover
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|||||||
17
AGENTS.md
Normal file
17
AGENTS.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
## Agent Instructions
|
||||||
|
|
||||||
|
### Build, Lint, and Test
|
||||||
|
|
||||||
|
- Build: `cargo build`
|
||||||
|
- Lint: `cargo clippy`
|
||||||
|
- Test: `cargo test`
|
||||||
|
- Run a single test: `cargo test --test <test_name>`
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- **Formatting**: Use `cargo fmt` to format the code.
|
||||||
|
- **Imports**: Group imports by standard library, external crates, and project modules.
|
||||||
|
- **Types**: Use explicit types.
|
||||||
|
- **Naming Conventions**: Follow Rust's naming conventions (e.g., `snake_case` for variables and functions, `PascalCase` for types).
|
||||||
|
- **Error Handling**: Use `Result` and `?` for error propagation. Use `panic!` only for unrecoverable errors.
|
||||||
|
- **Logging**: Use the `log` crate for logging.
|
||||||
2999
Cargo.lock
generated
Normal file
2999
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "simplicitty"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.44", features = ["full"] }
|
||||||
|
|
||||||
|
winit = { version = "0.30", features = ["wayland", "x11"] }
|
||||||
|
wgpu = "25"
|
||||||
|
glyphon = "0.9"
|
||||||
|
portable-pty = "0.9"
|
||||||
|
vte = "0.15"
|
||||||
|
|
||||||
|
# For reading from the PTY in a non-blocking way
|
||||||
|
crossbeam-channel = "0.5"
|
||||||
|
|
||||||
|
# For logging and running the event loop
|
||||||
|
env_logger = "0.10"
|
||||||
|
log = "0.4"
|
||||||
|
pollster = "0.3"
|
||||||
12
README.md
12
README.md
@@ -1,3 +1,11 @@
|
|||||||
# simplicitty
|
# Simplicitty
|
||||||
|
|
||||||
A simple terminal emulator
|
## Roadmap
|
||||||
|
|
||||||
|
1. [ ] Complete keyboard input handling (arrow keys, backspace, etc.)
|
||||||
|
2. [ ] Implement a PTY interface to spawn and communicate with processes
|
||||||
|
3. [ ] Add support for terminal protocols (ANSI escape sequences)
|
||||||
|
4. [ ] Implement scrollback buffer and viewport scrolling
|
||||||
|
5. [ ] Add color and text styling support
|
||||||
|
6. [ ] Implement clipboard integration
|
||||||
|
7. [ ] Add configuration options
|
||||||
|
|||||||
84
project.md
Normal file
84
project.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Module Breakdown and Responsibilities
|
||||||
|
|
||||||
|
## 1. src/pty.rs (The Shell Interface)
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- Use portable-pty to create a new PTY.
|
||||||
|
- Spawn a default shell process (e.g., bash) connected to the PTY.
|
||||||
|
- Provide a way to write user input (from winit) to the PTY.
|
||||||
|
- Provide a way to read shell output from the PTY.
|
||||||
|
|
||||||
|
Key Implementation Detail: Reading from the PTY is a blocking operation. You
|
||||||
|
should do this on a separate thread. This thread will read data and send it back
|
||||||
|
to the main UI thread using a channel (like crossbeam-channel). This prevents
|
||||||
|
the UI from freezing while waiting for the shell to output something.
|
||||||
|
|
||||||
|
## 2. src/terminal.rs (The Brains / State Model)
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- Hold the state of the terminal grid: a 2D array of cells, where each cell
|
||||||
|
contains a character, foreground color, background color, and style flags
|
||||||
|
(bold, italic, etc.).
|
||||||
|
|
||||||
|
- Contain a vte::Parser.
|
||||||
|
|
||||||
|
- Implement the vte::Perform trait. This is the core of this module. The vte
|
||||||
|
parser will call methods on your implementation as it processes the byte
|
||||||
|
stream from the PTY. For example:
|
||||||
|
- print(char): You update the character at the current cursor position in your
|
||||||
|
grid and advance the cursor.
|
||||||
|
- execute(byte): You handle control characters like newline (\n) or backspace
|
||||||
|
(\b).
|
||||||
|
- csi_dispatch(...): You handle complex ANSI escape sequences for changing
|
||||||
|
colors, moving the cursor, clearing the screen, etc.
|
||||||
|
|
||||||
|
- Keep track of the cursor's position and the current color/style settings.
|
||||||
|
|
||||||
|
## 3. src/renderer.rs (The Painter)
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- Initialize wgpu: get an adapter and device, configure a surface on the winit
|
||||||
|
window.
|
||||||
|
- Initialize glyphon: create a FontSystem, Cache, Atlas, and TextRenderer.
|
||||||
|
- Contain the main render() function. This function will be called on every
|
||||||
|
frame.
|
||||||
|
- Inside render():
|
||||||
|
- Get the current terminal state.
|
||||||
|
- Iterate through the terminal grid.
|
||||||
|
- For each line of text, create a glyphon::Buffer. Set its text content,
|
||||||
|
colors, and styles based on the data in your grid.
|
||||||
|
- Use the glyphon::TextRenderer to prepare and draw all the buffers to the
|
||||||
|
wgpu surface.
|
||||||
|
- Draw the cursor as a solid block or underline.
|
||||||
|
|
||||||
|
## 4. src/main.rs (The Conductor)
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- The main function.
|
||||||
|
- Initialize logging (env_logger).
|
||||||
|
- Create the winit event loop and window.
|
||||||
|
- Initialize your Pty, Terminal, and Renderer modules.
|
||||||
|
- Start the PTY reader thread.
|
||||||
|
- Run the main event loop.
|
||||||
|
|
||||||
|
### The Main Event Loop (winit::event_loop::run)
|
||||||
|
|
||||||
|
This is where everything comes together. You'll have a match statement to handle
|
||||||
|
different events:
|
||||||
|
|
||||||
|
- Event::WindowEvent::Resized: Tell the renderer about the new size. You'll also
|
||||||
|
need to inform the PTY of the new dimensions so that applications running in
|
||||||
|
the shell (like vim) can reflow correctly.
|
||||||
|
- Event::WindowEvent::KeyboardInput: Translate the keyboard event into
|
||||||
|
characters or escape sequences and write them to the pty module.
|
||||||
|
- Event::UserEvent: This is how your PTY thread will communicate with the main
|
||||||
|
thread. When the PTY thread reads new data, it sends it through the channel,
|
||||||
|
and you receive it here. You then feed this data into the terminal.vte_parser.
|
||||||
|
- Event::WindowEvent::RedrawRequested: This is the signal to render a new frame.
|
||||||
|
You call your renderer.render() function here. After processing PTY data, you
|
||||||
|
should always request a redraw.
|
||||||
|
- Event::WindowEvent::CloseRequested: Terminate the application.
|
||||||
111
src/app/mod.rs
Normal file
111
src/app/mod.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use crossbeam_channel::Receiver;
|
||||||
|
use log::{debug, error, info, trace, warn};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
use vte::Parser;
|
||||||
|
use winit::dpi::LogicalSize;
|
||||||
|
use winit::{
|
||||||
|
event::WindowEvent,
|
||||||
|
event_loop::{ActiveEventLoop, EventLoopProxy},
|
||||||
|
window::{Window, WindowId},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod pty;
|
||||||
|
mod renderer;
|
||||||
|
mod terminal;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum UserEvent {
|
||||||
|
PtyOutput(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TermState {
|
||||||
|
pty: pty::Pty,
|
||||||
|
terminal: terminal::Terminal,
|
||||||
|
renderer: renderer::Renderer,
|
||||||
|
vte_parser: vte::Parser,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Application {
|
||||||
|
runtime: Runtime,
|
||||||
|
event_loop_proxy: EventLoopProxy<UserEvent>,
|
||||||
|
term_state: Option<TermState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application {
|
||||||
|
pub fn new(event_loop_proxy: EventLoopProxy<UserEvent>) -> Self {
|
||||||
|
trace!("Creating new Application");
|
||||||
|
Self {
|
||||||
|
runtime: Runtime::new().expect("Failed to create tokio runtime"),
|
||||||
|
event_loop_proxy,
|
||||||
|
term_state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_pty_output_recv_thread(&self, recv: Receiver<String>) {
|
||||||
|
let proxy = self.event_loop_proxy.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
while let Ok(output) = recv.recv() {
|
||||||
|
trace!("Read from the PTY output: {}", output);
|
||||||
|
proxy.send_event(UserEvent::PtyOutput(output)).ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl winit::application::ApplicationHandler<UserEvent> for Application {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.term_state.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up window
|
||||||
|
let (width, height) = (800, 600);
|
||||||
|
let window_attributes = Window::default_attributes()
|
||||||
|
.with_inner_size(LogicalSize::new(width as f64, height as f64))
|
||||||
|
.with_title("Terminal emulator");
|
||||||
|
debug!("Creating a 800x600 window");
|
||||||
|
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
|
||||||
|
|
||||||
|
trace!("Creating crossbeam channel for PTY output rendering");
|
||||||
|
let (send, recv) = crossbeam_channel::unbounded();
|
||||||
|
|
||||||
|
self.term_state = Some(TermState {
|
||||||
|
pty: pty::Pty::new(send),
|
||||||
|
terminal: terminal::Terminal::new(),
|
||||||
|
renderer: self.runtime.block_on(renderer::Renderer::new(window)),
|
||||||
|
vte_parser: vte::Parser::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
self.start_pty_output_recv_thread(recv);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
|
||||||
|
if let Some(state) = &mut self.term_state {
|
||||||
|
match event {
|
||||||
|
UserEvent::PtyOutput(output) => {
|
||||||
|
for byte in output.bytes() {
|
||||||
|
state.vte_parser.advance(&mut state.terminal, &[byte]);
|
||||||
|
}
|
||||||
|
state.renderer.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
_event_loop: &ActiveEventLoop,
|
||||||
|
_window_id: WindowId,
|
||||||
|
event: WindowEvent,
|
||||||
|
) {
|
||||||
|
if let Some(state) = &mut self.term_state {
|
||||||
|
match event {
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
state.renderer.render(&state.terminal);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/app/pty/mod.rs
Normal file
90
src/app/pty/mod.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
use crossbeam_channel::Sender;
|
||||||
|
use portable_pty::{Child, CommandBuilder, PtySize, native_pty_system};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
pub struct Pty {
|
||||||
|
writer: Box<dyn Write + Send + 'static>,
|
||||||
|
child: Box<dyn Child>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PtyError {
|
||||||
|
Io(std::io::Error),
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// You need this `From` implementation!
|
||||||
|
impl From<std::io::Error> for PtyError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
PtyError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pty {
|
||||||
|
pub fn new(output_sender: crossbeam_channel::Sender<String>) -> Self {
|
||||||
|
let pty_system = native_pty_system();
|
||||||
|
|
||||||
|
// Create a new pty
|
||||||
|
let pair = pty_system
|
||||||
|
.openpty(PtySize {
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
// Not all systems support pixel_width, pixel_height,
|
||||||
|
// but it is good practice to set it to something
|
||||||
|
// that matches the size of the selected font. That
|
||||||
|
// is more complex than can be shown here in this
|
||||||
|
// brief example though!
|
||||||
|
pixel_width: 0,
|
||||||
|
pixel_height: 0,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Spawn a shell into the pty
|
||||||
|
let cmd = CommandBuilder::new_default_prog();
|
||||||
|
|
||||||
|
// The `slave` end of the PTY is given to the shell process.
|
||||||
|
// The shell thinks it's talking to a real terminal.
|
||||||
|
let mut child = pair.slave.spawn_command(cmd).unwrap();
|
||||||
|
// We can now drop the slave, as it's been consumed by the child process.
|
||||||
|
drop(pair.slave);
|
||||||
|
|
||||||
|
// Our application will use the `master` end to communicate with the shell.
|
||||||
|
// We are cloning the master so we can use it in a separate thread for reading.
|
||||||
|
let writer = pair.master.take_writer().unwrap();
|
||||||
|
let reader = pair.master.try_clone_reader().unwrap();
|
||||||
|
|
||||||
|
start_reader_thread(output_sender.clone(), reader);
|
||||||
|
|
||||||
|
Self { writer, child }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_command(&mut self, cmd: String) -> Result<(), PtyError> {
|
||||||
|
self.writer.write_all(cmd.as_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_reader_thread(sender: Sender<String>, mut reader: Box<dyn Read + Send + 'static>) {
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut buffer = [0u8; 1024];
|
||||||
|
loop {
|
||||||
|
match reader.read(&mut buffer) {
|
||||||
|
Ok(0) => {
|
||||||
|
// EOF, the child process has exited.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
// Append the read data to our output string.
|
||||||
|
// We use from_utf8_lossy to handle potential invalid UTF-8 sequences.
|
||||||
|
sender
|
||||||
|
.send(String::from_utf8_lossy(&buffer[..n]).to_string())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// An error occurred.
|
||||||
|
eprintln!("Error reading from PTY: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
181
src/app/renderer.rs
Normal file
181
src/app/renderer.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
use log::{debug, trace};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use wgpu::{
|
||||||
|
CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Instance,
|
||||||
|
InstanceDescriptor, Queue, RenderPassColorAttachment, RenderPassDescriptor,
|
||||||
|
RequestAdapterOptions, Surface, SurfaceConfiguration, SurfaceError, TextureFormat,
|
||||||
|
TextureViewDescriptor,
|
||||||
|
};
|
||||||
|
use winit::dpi::PhysicalSize;
|
||||||
|
use winit::window::Window;
|
||||||
|
|
||||||
|
pub struct Renderer {
|
||||||
|
surface: Surface<'static>,
|
||||||
|
device: Device,
|
||||||
|
queue: Queue,
|
||||||
|
config: SurfaceConfiguration,
|
||||||
|
|
||||||
|
size: PhysicalSize<u32>,
|
||||||
|
|
||||||
|
/// text: terminal_text::TerminalText,
|
||||||
|
window: Arc<Window>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderer {
|
||||||
|
pub async fn new(window: Arc<Window>) -> Self {
|
||||||
|
let size = window.inner_size();
|
||||||
|
|
||||||
|
// The instance is needed to create an `Adapter` and a `Surface`
|
||||||
|
// Backends::all => Vulkan + Metal + DX12 + Browser WebGPU
|
||||||
|
let instance = Instance::new(&InstanceDescriptor {
|
||||||
|
backends: wgpu::Backends::PRIMARY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is the part of the window where we draw
|
||||||
|
// The window needs to implement the `HasRawWindowHandle` trait
|
||||||
|
// We also need the surface to request the `Adapter`
|
||||||
|
let surface = instance
|
||||||
|
.create_surface(window.clone())
|
||||||
|
.expect("Failed to create surface");
|
||||||
|
|
||||||
|
// The adapter is the handle to our graphics card. We can get information about the device
|
||||||
|
// with this. We will use it to create the `Device` and `Queue` later
|
||||||
|
// We could enumerate the adapters and find the one that suits us:
|
||||||
|
//
|
||||||
|
// let adapter = instance
|
||||||
|
// .enumerate_adapters(wgpu::Backends::all())
|
||||||
|
// .filter(|adapter| {
|
||||||
|
// // Check if this adapter supports our surface
|
||||||
|
// adapter.is_surface_supported(&surface)
|
||||||
|
// })
|
||||||
|
// .next()
|
||||||
|
// .unwrap()
|
||||||
|
//
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::default(),
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&DeviceDescriptor {
|
||||||
|
// The features field allows to specify the extra features we want
|
||||||
|
// The graphics card you have limits the features you can use. If you want to use certain
|
||||||
|
// features, you may need to limit what devices you support or provide workarounds.
|
||||||
|
// You can get a list of features supported by your device using `adapter.features()` or
|
||||||
|
// `device.features()`.
|
||||||
|
// See: https://docs.rs/wgpu/latest/wgpu/struct.Features.html
|
||||||
|
required_features: wgpu::Features::empty(),
|
||||||
|
// The limits field describes the limits of certain types of resources we can
|
||||||
|
// create. See: https://docs.rs/wgpu/latest/wgpu/struct.Limits.html
|
||||||
|
required_limits: wgpu::Limits::default(),
|
||||||
|
label: None,
|
||||||
|
// Provides the adapter with the preferred memory allocation strategy.
|
||||||
|
// See: https://wgpu.rs/doc/wgpu/enum.MemoryHints.html
|
||||||
|
memory_hints: Default::default(),
|
||||||
|
trace: wgpu::Trace::Off,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// let surface_caps = surface.get_capabilities(&adapter);
|
||||||
|
// // This will define how the surface creates its underlying `SurfaceTexture`
|
||||||
|
// // This assumes an sRGB surface texture. Using a different one will result in all the colors
|
||||||
|
// // coming out darker. If we want to support non sRGB surfaces, we'll need to account for that
|
||||||
|
// // when drawing to the frame.
|
||||||
|
// let surface_format = surface_caps
|
||||||
|
// .formats
|
||||||
|
// .iter()
|
||||||
|
// .find(|f| f.is_srgb())
|
||||||
|
// .copied()
|
||||||
|
// .unwrap_or(surface_caps.formats[0]); // We get the first sRGB format
|
||||||
|
|
||||||
|
let surface_format = TextureFormat::Bgra8UnormSrgb;
|
||||||
|
let config = SurfaceConfiguration {
|
||||||
|
// This field describes how SurfaceTextures will be used. RENDER_ATTACHMENT
|
||||||
|
// specifies that the textures will be used to write to the screen.
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
// The format defines how SurfaceTextures will be stored on the GPU.
|
||||||
|
// We can get a supported format from the SurfaceCapabilities
|
||||||
|
format: surface_format,
|
||||||
|
width: size.width, // Must be > 0
|
||||||
|
height: size.height, // Must be > 0
|
||||||
|
// Takes a `wgpu::PresentMode` option, which determines how to sync the surface with
|
||||||
|
// the display. `PresentMode::Fifo` is VSync.
|
||||||
|
// A list of available modes can be obtained with `&surface_caps.present_modes`
|
||||||
|
// See: https://docs.rs/wgpu/latest/wgpu/enum.PresentMode.html
|
||||||
|
present_mode: wgpu::PresentMode::Fifo,
|
||||||
|
// ???
|
||||||
|
alpha_mode: CompositeAlphaMode::Opaque,
|
||||||
|
//
|
||||||
|
view_formats: vec![],
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
};
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
size,
|
||||||
|
window,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_redraw(&self) {
|
||||||
|
self.window.request_redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The renderer takes the terminal state as an argument to its render method.
|
||||||
|
// It does not store a copy of the grid itself.
|
||||||
|
pub fn render(&mut self, terminal: &super::terminal::Terminal) -> Result<(), SurfaceError> {
|
||||||
|
let grid = terminal.get_grid();
|
||||||
|
|
||||||
|
// 1. Iterate through the `grid`.
|
||||||
|
// 2. For each row, create a glyphon::Buffer.
|
||||||
|
// 3. Set the text, colors, and styles for that buffer from the row's `Cell` data.
|
||||||
|
// 4. Draw all the buffers to the screen.
|
||||||
|
// 5. Draw the cursor at terminal.get_cursor_pos().
|
||||||
|
trace!("Rendering requested");
|
||||||
|
// Prepare with current configuration (which includes the current window size)
|
||||||
|
|
||||||
|
let output = self.surface.get_current_texture()?;
|
||||||
|
let view = output
|
||||||
|
.texture
|
||||||
|
.create_view(&TextureViewDescriptor::default());
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&CommandEncoderDescriptor {
|
||||||
|
label: Some("Render Encoder"),
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut _render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||||
|
label: Some("Render Pass"),
|
||||||
|
color_attachments: &[Some(RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
trace!("Rendering...");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queue.submit(Some(encoder.finish()));
|
||||||
|
output.present();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/app/terminal/mod.rs
Normal file
78
src/app/terminal/mod.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use vte::{Parser, Perform};
|
||||||
|
|
||||||
|
// A struct to represent a single cell on the screen
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Cell {
|
||||||
|
pub char: char,
|
||||||
|
// pub fg_color: Color,
|
||||||
|
// pub bg_color: Color,
|
||||||
|
// pub flags: StyleFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CellGrid(Vec<Vec<Cell>>);
|
||||||
|
|
||||||
|
impl CellGrid {
|
||||||
|
pub fn new(width: usize, height: usize) -> Self {
|
||||||
|
CellGrid(vec![vec![Cell::default(); width]; height])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Terminal {
|
||||||
|
grid: CellGrid,
|
||||||
|
cursor_x: usize,
|
||||||
|
cursor_y: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let width = 80;
|
||||||
|
let height = 24;
|
||||||
|
Terminal {
|
||||||
|
grid: CellGrid::new(width, height),
|
||||||
|
cursor_x: 0,
|
||||||
|
cursor_y: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_grid(&self) -> &CellGrid {
|
||||||
|
&self.grid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Perform for Terminal {
|
||||||
|
// this method will update the CellGrid and the cursor position
|
||||||
|
fn print(&mut self, c: char) {
|
||||||
|
// todo!()
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This method is called when an "execute" control character is seen.
|
||||||
|
/// These are single-byte control codes like newline, backspace, etc.
|
||||||
|
fn execute(&mut self, byte: u8) {
|
||||||
|
match byte {
|
||||||
|
b'\n' => {
|
||||||
|
// Newline
|
||||||
|
println!("Execute: Newline");
|
||||||
|
self.cursor_y += 1;
|
||||||
|
self.cursor_x = 0; // Carriage return is implicit with newline
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("Execute: Unhandled control character: 0x{:02x}", byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This method is called for "Control Sequence Introducer" (CSI) sequences.
|
||||||
|
/// These are the complex `ESC [ ...` sequences for moving the cursor,
|
||||||
|
/// changing colors, clearing the screen, etc.
|
||||||
|
fn csi_dispatch(
|
||||||
|
&mut self,
|
||||||
|
params: &vte::Params,
|
||||||
|
_intermediates: &[u8],
|
||||||
|
_ignore: bool,
|
||||||
|
action: char,
|
||||||
|
) {
|
||||||
|
//todo!()
|
||||||
|
()
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main.rs
Normal file
26
src/main.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use log::{debug, info};
|
||||||
|
use winit::event_loop::EventLoop;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Initialize logger
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
// Log platform information
|
||||||
|
info!("Starting simplicitty terminal emulator");
|
||||||
|
|
||||||
|
debug!("Creating winit event loop");
|
||||||
|
let event_loop = EventLoop::<app::UserEvent>::with_user_event()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Create event loop proxy to send pty output events to the window
|
||||||
|
let event_loop_proxy = event_loop.create_proxy();
|
||||||
|
|
||||||
|
debug!("Starting winit event loop");
|
||||||
|
// Run the application
|
||||||
|
event_loop
|
||||||
|
.run_app(&mut app::Application::new(event_loop_proxy))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user