300 lines
8.4 KiB
Rust

//! Terminal driver implementation
use core::{
pin::Pin,
sync::atomic::{AtomicBool, Ordering},
task::{Context, Poll},
};
use abi::{
error::Error,
io::{TerminalInputOptions, TerminalLineOptions, TerminalOptions, TerminalOutputOptions},
process::{ProcessGroupId, Signal},
};
use device_api::serial::SerialDevice;
use futures_util::Future;
use libk::task::process::ProcessImpl;
use libk_util::{ring::RingBuffer, sync::IrqSafeSpinlock, waker::QueueWaker};
use crate::{proc::io::ProcessIoImpl, task::process::ProcessManagerImpl};
struct TerminalRing {
buffer: IrqSafeSpinlock<RingBuffer<u8>>,
eof: AtomicBool,
notify: QueueWaker,
}
impl TerminalRing {
const CAPACITY: usize = 128;
const fn new() -> Self {
Self {
buffer: IrqSafeSpinlock::new(RingBuffer::with_capacity(TerminalRing::CAPACITY)),
eof: AtomicBool::new(false),
notify: QueueWaker::new(),
}
}
fn write(&self, ch: u8, signal: bool) {
self.buffer.lock().write(ch);
if signal {
self.notify.wake_one();
}
}
fn signal(&self) {
self.notify.wake_all();
}
fn read_blocking(&self) -> impl Future<Output = Option<u8>> + '_ {
struct F<'f> {
ring: &'f TerminalRing,
}
impl<'f> Future for F<'f> {
type Output = Option<u8>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.ring.notify.register(cx.waker());
if self
.ring
.eof
.compare_exchange(true, false, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
self.ring.notify.remove(cx.waker());
return Poll::Ready(None);
}
let mut lock = self.ring.buffer.lock();
if lock.is_readable() {
self.ring.notify.remove(cx.waker());
Poll::Ready(Some(unsafe { lock.read_single_unchecked() }))
} else {
Poll::Pending
}
}
}
F { ring: self }
}
}
struct TtyContextInner {
config: TerminalOptions,
process_group: Option<ProcessGroupId>,
}
/// Represents the context of a terminal device
pub struct TtyContext {
ring: TerminalRing, // AsyncRing<u8, 128>,
inner: IrqSafeSpinlock<TtyContextInner>,
}
// TODO merge this code with PTY
/// Terminal device interface
pub trait TtyDevice: SerialDevice {
/// Returns the ring buffer associated with the device
fn context(&self) -> &TtyContext;
/// Sets the process group to which signals from this terminal should be delivered
fn set_signal_group(&self, id: ProcessGroupId) {
self.context().inner.lock().process_group.replace(id);
}
/// Sends a single byte to the terminal
fn line_send(&self, byte: u8) -> Result<(), Error> {
let cx = self.context();
let inner = cx.inner.lock();
if byte == b'\n'
&& inner
.config
.output
.contains(TerminalOutputOptions::NL_TO_CRNL)
{
self.send(b'\r').ok();
}
drop(inner);
self.send(byte)
}
/// Receives a single byte from the terminal
fn recv_byte(&self, mut byte: u8) {
let cx = self.context();
let inner = cx.inner.lock();
if byte == b'\r' && inner.config.input.contains(TerminalInputOptions::CR_TO_NL) {
byte = b'\n';
}
if byte == b'\n' {
// TODO implement proper echo here
let _echo = inner.config.line.contains(TerminalLineOptions::ECHO)
|| inner
.config
.line
.contains(TerminalLineOptions::CANONICAL | TerminalLineOptions::ECHO_NL);
if inner
.config
.output
.contains(TerminalOutputOptions::NL_TO_CRNL)
{
self.send(b'\r').ok();
}
self.send(byte).ok();
} else if inner.config.line.contains(TerminalLineOptions::ECHO) {
if byte.is_ascii_control() {
if byte != inner.config.chars.erase && byte != inner.config.chars.werase {
self.send(b'^').ok();
self.send(byte + 0x40).ok();
}
} else {
self.send(byte).ok();
}
}
// byte == config.chars.interrupt
if byte == inner.config.chars.interrupt
&& inner.config.line.contains(TerminalLineOptions::SIGNAL)
{
let pgrp = inner.process_group;
cx.signal();
if let Some(pgrp) = pgrp {
drop(inner);
ProcessImpl::<ProcessManagerImpl, ProcessIoImpl>::signal_group(
pgrp,
Signal::Interrupted,
);
return;
} else {
debugln!("Terminal has no process group attached");
}
}
let canonical = inner.config.line.contains(TerminalLineOptions::CANONICAL);
drop(inner);
cx.putc(byte, !canonical || byte == b'\n' || byte == b'\r');
}
/// Reads and processes data from the terminal
async fn line_read(&self, data: &mut [u8]) -> Result<usize, Error> {
let cx = self.context();
let mut inner = cx.inner.lock();
if data.is_empty() {
return Ok(0);
}
if !inner.config.is_canonical() {
drop(inner);
let Some(byte) = cx.getc().await else {
return Ok(0);
};
data[0] = byte;
Ok(1)
} else {
let mut rem = data.len();
let mut off = 0;
// Run until either end of buffer or return condition is reached
while rem != 0 {
drop(inner);
let Some(byte) = cx.getc().await else {
break;
};
inner = cx.inner.lock();
if inner.config.is_canonical() {
if byte == inner.config.chars.eof {
break;
} else if byte == inner.config.chars.erase {
// Erase
if off != 0 {
self.raw_write(b"\x1b[D \x1b[D")?;
off -= 1;
rem += 1;
}
continue;
} else if byte == inner.config.chars.werase {
todo!()
}
}
data[off] = byte;
off += 1;
rem -= 1;
if byte == b'\n' || byte == b'\r' {
break;
}
}
Ok(off)
}
}
/// Processes and writes the data to the terminal
fn line_write(&self, data: &[u8]) -> Result<usize, Error> {
for &byte in data {
self.line_send(byte)?;
}
Ok(data.len())
}
/// Writes raw data to the terminal bypassing the processing functions
fn raw_write(&self, data: &[u8]) -> Result<usize, Error> {
for &byte in data {
self.send(byte)?;
}
Ok(data.len())
}
}
impl TtyContext {
/// Constructs a new [TtyContext]
pub fn new() -> Self {
Self {
ring: TerminalRing::new(), // AsyncRing::new(0),
inner: IrqSafeSpinlock::new(TtyContextInner {
config: TerminalOptions::const_default(),
process_group: None,
}),
}
}
/// Signals an event on the terminal
pub fn signal(&self) {
self.ring.signal()
}
/// Writes a single character to the terminal
pub fn putc(&self, ch: u8, signal: bool) {
self.ring.write(ch, signal);
}
/// Performs a blocking read of a single character from the terminal
pub async fn getc(&self) -> Option<u8> {
self.ring.read_blocking().await
}
/// Changes the configuration of the terminal
pub fn set_config(&self, config: &TerminalOptions) -> Result<(), Error> {
self.inner.lock().config = *config;
Ok(())
}
/// Returns the configuration of the terminal
pub fn config(&self) -> TerminalOptions {
self.inner.lock().config
}
}