brewery
notashelf /
0df5588f02af0396fd7abfdd81aa839211ed77b8

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git

Commit 0df5588f02af

tarball

NotAShelf <raf@notashelf.dev> · 2026-06-26 07:21 UTC

unverified1 files changed+111-59
wayland: spawn the pty after the first configure

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0fb9dd38217943d4defd908f857d78766a6a6964
diff --git a/src/wayland.rs b/src/wayland.rsindex d308171..098a803 100644--- a/src/wayland.rs+++ b/src/wayland.rs@@ -10,7 +10,7 @@ use std::time::Duration;  use anyhow::Context; use calloop::generic::Generic;-use calloop::{EventLoop, Interest, Mode, PostAction};+use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction}; use calloop_wayland_source::WaylandSource;  use crate::font::Fonts;@@ -77,42 +77,6 @@ pub fn run() -> anyhow::Result<ExitCode> {      let fonts = Fonts::new(FONT_FAMILY, FONT_SIZE_PX).context("load font")?;     let renderer = Renderer::new(fonts);-    let (cols, rows) = grid_size(renderer.metrics(), DEFAULT_W, DEFAULT_H);--    let pty = Pty::spawn(cols, rows).context("spawn shell on pty")?;-    let term = Term::new(cols as usize, rows as usize);-    let mut parser = vte::Parser::new();--    // Read child output off a clone of the master; the original stays in `pty`-    // for writing input back.-    let read_fd = pty.master().try_clone().context("clone pty master")?;-    event_loop-        .handle()-        .insert_source(-            Generic::new(read_fd, Interest::READ, Mode::Level),-            move |_, fd, app: &mut App| {-                let mut buf = [0u8; 4096];-                match rustix::io::read(&*fd, &mut buf) {-                    Ok(0) => {-                        app.child_exited();-                        Ok(PostAction::Remove)-                    }-                    Ok(n) => {-                        parser.advance(&mut app.term, &buf[..n]);-                        app.after_feed();-                        Ok(PostAction::Continue)-                    }-                    Err(rustix::io::Errno::INTR | rustix::io::Errno::AGAIN) => {-                        Ok(PostAction::Continue)-                    }-                    Err(_) => {-                        app.child_exited();-                        Ok(PostAction::Remove)-                    }-                }-            },-        )-        .map_err(|e| anyhow::anyhow!("register pty in event loop: {e}"))?;      let mut app = App {         registry_state: RegistryState::new(&globals),@@ -121,9 +85,12 @@ pub fn run() -> anyhow::Result<ExitCode> {         shm,         pool,         window,-        pty,-        term,         renderer,+        loop_handle: event_loop.handle(),+        // The PTY is spawned on the first configure, once the real window size+        // is known, so the shell starts at the final size and is not hit by a+        // startup SIGWINCH storm that makes it reprint its prompt.+        session: None,         title: None,         width: DEFAULT_W,         height: DEFAULT_H,@@ -165,6 +132,14 @@ fn write_all(fd: &OwnedFd, mut buf: &[u8]) -> rustix::io::Result<()> {     Ok(()) } +/// The per-window terminal: the PTY and the parsed screen behind it. Created on+/// the first configure, once the real size is known.+#[derive(Debug)]+struct Session {+    pty: Pty,+    term: Term,+}+ /// Window + Wayland client state shared across all protocol handlers. #[derive(Debug)] struct App {@@ -174,9 +149,10 @@ struct App {     shm: Shm,     pool: SlotPool,     window: Window,-    pty: Pty,-    term: Term,     renderer: Renderer,+    loop_handle: LoopHandle<'static, App>,+    /// `None` until the first configure spawns the shell.+    session: Option<Session>,     /// Last title applied to the toplevel, to avoid redundant requests.     title: Option<String>,     width: u32,@@ -191,17 +167,79 @@ struct App { }  impl App {+    /// Spawn the shell at the current window size and start reading its output.+    fn spawn_session(&mut self) {+        let (cols, rows) = grid_size(self.renderer.metrics(), self.width, self.height);+        let pty = match Pty::spawn(cols, rows) {+            Ok(pty) => pty,+            Err(err) => {+                tracing::error!("spawn shell: {err:#}");+                self.exit = true;+                return;+            }+        };+        let read_fd = match pty.master().try_clone() {+            Ok(fd) => fd,+            Err(err) => {+                tracing::error!("clone pty master: {err}");+                self.exit = true;+                return;+            }+        };++        let mut parser = vte::Parser::new();+        let source = Generic::new(read_fd, Interest::READ, Mode::Level);+        let registered = self+            .loop_handle+            .insert_source(source, move |_, fd, app: &mut App| {+                let mut buf = [0u8; 4096];+                let n = match rustix::io::read(&*fd, &mut buf) {+                    Ok(0) => {+                        app.child_exited();+                        return Ok(PostAction::Remove);+                    }+                    Ok(n) => n,+                    Err(rustix::io::Errno::INTR | rustix::io::Errno::AGAIN) => {+                        return Ok(PostAction::Continue);+                    }+                    Err(_) => {+                        app.child_exited();+                        return Ok(PostAction::Remove);+                    }+                };+                if let Some(session) = app.session.as_mut() {+                    parser.advance(&mut session.term, &buf[..n]);+                }+                app.after_feed();+                Ok(PostAction::Continue)+            });+        if let Err(err) = registered {+            tracing::error!("register pty in event loop: {err}");+            self.exit = true;+            return;+        }++        self.session = Some(Session {+            pty,+            term: Term::new(cols as usize, rows as usize),+        });+    }+     /// After parsing child output: send any replies, sync the title, repaint.     fn after_feed(&mut self) {-        let reply = self.term.take_response();+        let Some(session) = self.session.as_mut() else {+            return;+        };+        let reply = session.term.take_response();         if !reply.is_empty()-            && let Err(err) = write_all(self.pty.master(), &reply)+            && let Err(err) = write_all(session.pty.master(), &reply)         {             tracing::warn!("write to pty: {err}");         } -        if self.term.title() != self.title.as_deref() {-            self.title = self.term.title().map(str::to_owned);+        let new_title = session.term.title().map(str::to_owned);+        if new_title.as_deref() != self.title.as_deref() {+            self.title = new_title;             self.window                 .set_title(self.title.clone().unwrap_or_default());         }@@ -212,11 +250,16 @@ impl App {     /// PTY about it if it changed.     fn resize_grid(&mut self) {         let (cols, rows) = grid_size(self.renderer.metrics(), self.width, self.height);-        if (cols as usize, rows as usize) == (self.term.grid().cols(), self.term.grid().rows()) {+        let Some(session) = self.session.as_mut() else {+            return;+        };+        if (cols as usize, rows as usize)+            == (session.term.grid().cols(), session.term.grid().rows())+        {             return;         }-        self.term.resize(cols as usize, rows as usize);-        if let Err(err) = self.pty.resize(cols, rows) {+        session.term.resize(cols as usize, rows as usize);+        if let Err(err) = session.pty.resize(cols, rows) {             tracing::warn!("resize pty: {err}");         }     }@@ -224,22 +267,27 @@ impl App {     /// The child shell has gone away; reap it, capture its code, and tear the     /// window down.     fn child_exited(&mut self) {-        match self.pty.wait() {-            Ok(status) => {-                tracing::info!("shell exited: {status}");-                // Mirror the shell's status: its code, or 128+signal if killed.-                let code = status-                    .code()-                    .unwrap_or_else(|| 128 + status.signal().unwrap_or(0));-                self.exit_code = ExitCode::from(code as u8);+        if let Some(session) = self.session.as_mut() {+            match session.pty.wait() {+                Ok(status) => {+                    tracing::info!("shell exited: {status}");+                    // Mirror the shell's status: its code, or 128+signal if killed.+                    let code = status+                        .code()+                        .unwrap_or_else(|| 128 + status.signal().unwrap_or(0));+                    self.exit_code = ExitCode::from(code as u8);+                }+                Err(err) => tracing::warn!("reap shell: {err}"),             }-            Err(err) => tracing::warn!("reap shell: {err}"),         }         self.exit = true;     }      /// Render the grid into a fresh buffer and present it.     fn draw(&mut self) {+        let Some(session) = self.session.as_ref() else {+            return;+        };         let (w, h) = (self.width, self.height);         let stride = w as i32 * 4; @@ -256,7 +304,7 @@ impl App {             };          self.renderer.render(-            self.term.grid(),+            session.term.grid(),             canvas,             w as usize,             h as usize,@@ -331,7 +379,11 @@ impl WindowHandler for App {             self.height = h.get();         }         self.focused = configure.is_activated();-        self.resize_grid();+        if self.session.is_none() {+            self.spawn_session();+        } else {+            self.resize_grid();+        }         self.draw();     } }