Skip to content

Commit 29fe5ef

Browse files
fix(tui): start pty with matching TUI size (#9101)
### Description Fixes #9060 [From the issue](#9060 (comment)) > I think I see the issue, it appears that rspack adds enough trailing spaces in the progress bar output the fill the current line: > > ``` > ● ESC[1mESC[0m ESC[32mESC[37mESC[2m━━━━━━━━━━━━━━━━━━━━━━━━━ESC[0mESC[0m (0%) ESC[2msetup compilation ESC[0m^MESC[2K > ``` > > The `ESC[2K` operation at the erases the current line, but if the trailing spaces have caused the line to wrap, it won't erase the intended line instead just the line overflow. This probably indicates we have a mismatch in the PTY dimensions and the virtual terminal dimensions so `rspack` is outputting too many spaces for the terminal. > > I'll look at making sure the underlying PTY has the exact same dimensions as the virtual terminal. There is future work to make sure that we update underlying PTY instances if the pane changes sizes, but that requires a pretty heavy refactor to our process management code to communicate with the PTY "controller" as we only hold onto the "receiver". ### Testing Instructions Before: <img width="899" alt="Screenshot 2024-09-03 at 9 34 21 AM" src="https://github.com/user-attachments/assets/b773457e-33ef-4075-aae4-9b0b9932a47a"> After: <img width="483" alt="Screenshot 2024-09-03 at 9 30 20 AM" src="https://github.com/user-attachments/assets/34997ea3-1032-4cf5-96db-b3a30e1c752e">
1 parent 8461559 commit 29fe5ef

File tree

6 files changed

+110
-35
lines changed

6 files changed

+110
-35
lines changed

crates/turborepo-lib/src/process/child.rs

+39-32
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use tokio::{
3333
};
3434
use tracing::{debug, trace};
3535

36-
use super::Command;
36+
use super::{Command, PtySize};
3737

3838
#[derive(Debug)]
3939
pub enum ChildState {
@@ -135,22 +135,17 @@ impl ChildHandle {
135135
}
136136

137137
#[tracing::instrument(skip(command))]
138-
pub fn spawn_pty(command: Command) -> io::Result<SpawnResult> {
139-
use portable_pty::PtySize;
140-
138+
pub fn spawn_pty(command: Command, size: PtySize) -> io::Result<SpawnResult> {
141139
let keep_stdin_open = command.will_open_stdin();
142140

143141
let command = portable_pty::CommandBuilder::from(command);
144142
let pty_system = native_pty_system();
145-
let size =
146-
console::Term::stdout()
147-
.size_checked()
148-
.map_or_else(PtySize::default, |(rows, cols)| PtySize {
149-
rows,
150-
cols,
151-
pixel_width: 0,
152-
pixel_height: 0,
153-
});
143+
let size = portable_pty::PtySize {
144+
rows: size.rows,
145+
cols: size.cols,
146+
pixel_width: 0,
147+
pixel_height: 0,
148+
};
154149
let pair = pty_system
155150
.openpty(size)
156151
.map_err(|err| match err.downcast() {
@@ -438,15 +433,15 @@ impl Child {
438433
pub fn spawn(
439434
command: Command,
440435
shutdown_style: ShutdownStyle,
441-
use_pty: bool,
436+
pty_size: Option<PtySize>,
442437
) -> io::Result<Self> {
443438
let label = command.label();
444439
let SpawnResult {
445440
handle: mut child,
446441
io: ChildIO { stdin, output },
447442
controller,
448-
} = if use_pty {
449-
ChildHandle::spawn_pty(command)
443+
} = if let Some(size) = pty_size {
444+
ChildHandle::spawn_pty(command, size)
450445
} else {
451446
ChildHandle::spawn_normal(command)
452447
}?;
@@ -832,7 +827,10 @@ mod test {
832827
use turbopath::AbsoluteSystemPathBuf;
833828

834829
use super::{Child, ChildInput, ChildOutput, ChildState, Command};
835-
use crate::process::child::{ChildExit, ShutdownStyle};
830+
use crate::process::{
831+
child::{ChildExit, ShutdownStyle},
832+
PtySize,
833+
};
836834

837835
const STARTUP_DELAY: Duration = Duration::from_millis(500);
838836
// We skip testing PTY usage on Windows
@@ -855,7 +853,8 @@ mod test {
855853
let script = find_script_dir().join_component("hello_world.js");
856854
let mut cmd = Command::new("node");
857855
cmd.args([script.as_std_path()]);
858-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
856+
let mut child =
857+
Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
859858

860859
assert_matches!(child.pid(), Some(_));
861860
child.stop().await;
@@ -872,7 +871,8 @@ mod test {
872871
let script = find_script_dir().join_component("hello_world.js");
873872
let mut cmd = Command::new("node");
874873
cmd.args([script.as_std_path()]);
875-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
874+
let mut child =
875+
Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
876876

877877
let exit1 = child.wait().await;
878878
let exit2 = child.wait().await;
@@ -892,7 +892,8 @@ mod test {
892892
cmd
893893
};
894894

895-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
895+
let mut child =
896+
Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
896897

897898
{
898899
let state = child.state.read().await;
@@ -917,7 +918,8 @@ mod test {
917918
let mut cmd = Command::new("node");
918919
cmd.args([script.as_std_path()]);
919920
cmd.open_stdin();
920-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
921+
let mut child =
922+
Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
921923

922924
tokio::time::sleep(STARTUP_DELAY).await;
923925

@@ -959,7 +961,8 @@ mod test {
959961
let mut cmd = Command::new("node");
960962
cmd.args([script.as_std_path()]);
961963
cmd.open_stdin();
962-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
964+
let mut child =
965+
Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
963966

964967
tokio::time::sleep(STARTUP_DELAY).await;
965968

@@ -1006,7 +1009,7 @@ mod test {
10061009
let mut child = Child::spawn(
10071010
cmd,
10081011
ShutdownStyle::Graceful(Duration::from_millis(500)),
1009-
use_pty,
1012+
use_pty.then(PtySize::default),
10101013
)
10111014
.unwrap();
10121015

@@ -1043,7 +1046,7 @@ mod test {
10431046
let mut child = Child::spawn(
10441047
cmd,
10451048
ShutdownStyle::Graceful(Duration::from_millis(500)),
1046-
use_pty,
1049+
use_pty.then(PtySize::default),
10471050
)
10481051
.unwrap();
10491052

@@ -1073,7 +1076,7 @@ mod test {
10731076
let mut child = Child::spawn(
10741077
cmd,
10751078
ShutdownStyle::Graceful(Duration::from_millis(500)),
1076-
use_pty,
1079+
use_pty.then(PtySize::default),
10771080
)
10781081
.unwrap();
10791082

@@ -1122,7 +1125,8 @@ mod test {
11221125
let mut cmd = Command::new("node");
11231126
cmd.args([script.as_std_path()]);
11241127
cmd.open_stdin();
1125-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
1128+
let mut child =
1129+
Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
11261130

11271131
let mut out = Vec::new();
11281132

@@ -1144,7 +1148,8 @@ mod test {
11441148
let mut cmd = Command::new("node");
11451149
cmd.args([script.as_std_path()]);
11461150
cmd.open_stdin();
1147-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
1151+
let mut child =
1152+
Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
11481153

11491154
let mut buffer = Vec::new();
11501155

@@ -1168,7 +1173,8 @@ mod test {
11681173
let mut cmd = Command::new("node");
11691174
cmd.args([script.as_std_path()]);
11701175
cmd.open_stdin();
1171-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
1176+
let mut child =
1177+
Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
11721178

11731179
let mut out = Vec::new();
11741180

@@ -1189,7 +1195,8 @@ mod test {
11891195
let mut cmd = Command::new("node");
11901196
cmd.args([script.as_std_path()]);
11911197
cmd.open_stdin();
1192-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
1198+
let mut child =
1199+
Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
11931200

11941201
let mut out = Vec::new();
11951202

@@ -1217,7 +1224,7 @@ mod test {
12171224
let mut child = Child::spawn(
12181225
cmd,
12191226
ShutdownStyle::Graceful(Duration::from_millis(100)),
1220-
use_pty,
1227+
use_pty.then(PtySize::default),
12211228
)
12221229
.unwrap();
12231230

@@ -1233,7 +1240,7 @@ mod test {
12331240
async fn test_orphan_process() {
12341241
let mut cmd = Command::new("sh");
12351242
cmd.args(["-c", "echo hello; sleep 120; echo done"]);
1236-
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, false).unwrap();
1243+
let mut child = Child::spawn(cmd, ShutdownStyle::Kill, None).unwrap();
12371244

12381245
tokio::time::sleep(STARTUP_DELAY).await;
12391246

@@ -1267,7 +1274,7 @@ mod test {
12671274
let script = find_script_dir().join_component("hello_world.js");
12681275
let mut cmd = Command::new("node");
12691276
cmd.args([script.as_std_path()]);
1270-
let child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty).unwrap();
1277+
let child = Child::spawn(cmd, ShutdownStyle::Kill, use_pty.then(PtySize::default)).unwrap();
12711278

12721279
let mut stops = FuturesUnordered::new();
12731280
for _ in 1..10 {

crates/turborepo-lib/src/process/mod.rs

+37-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ pub struct ProcessManager {
3939
struct ProcessManagerInner {
4040
is_closing: bool,
4141
children: Vec<child::Child>,
42+
size: Option<PtySize>,
43+
}
44+
45+
#[derive(Debug, Clone, Copy)]
46+
pub struct PtySize {
47+
rows: u16,
48+
cols: u16,
4249
}
4350

4451
impl ProcessManager {
@@ -48,6 +55,7 @@ impl ProcessManager {
4855
state: Arc::new(Mutex::new(ProcessManagerInner {
4956
is_closing: false,
5057
children: Vec::new(),
58+
size: None,
5159
})),
5260
use_pty,
5361
}
@@ -99,10 +107,11 @@ impl ProcessManager {
99107
debug!("process manager closing");
100108
return None;
101109
}
110+
let pty_size = self.use_pty.then(|| lock.pty_size()).flatten();
102111
let child = child::Child::spawn(
103112
command,
104113
child::ShutdownStyle::Graceful(stop_timeout),
105-
self.use_pty,
114+
pty_size,
106115
);
107116
if let Ok(child) = &child {
108117
lock.children.push(child.clone());
@@ -161,6 +170,33 @@ impl ProcessManager {
161170
lock.children = vec![];
162171
}
163172
}
173+
174+
pub fn set_pty_size(&self, rows: u16, cols: u16) {
175+
self.state.lock().expect("not poisoned").size = Some(PtySize { rows, cols });
176+
}
177+
}
178+
179+
impl ProcessManagerInner {
180+
fn pty_size(&mut self) -> Option<PtySize> {
181+
if self.size.is_none() {
182+
self.size = PtySize::from_tty();
183+
}
184+
self.size
185+
}
186+
}
187+
188+
impl PtySize {
189+
fn from_tty() -> Option<Self> {
190+
console::Term::stdout()
191+
.size_checked()
192+
.map(|(rows, cols)| Self { rows, cols })
193+
}
194+
}
195+
196+
impl Default for PtySize {
197+
fn default() -> Self {
198+
Self { rows: 24, cols: 80 }
199+
}
164200
}
165201

166202
#[cfg(test)]

crates/turborepo-lib/src/task_graph/visitor.rs

+7
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ impl<'a> Visitor<'a> {
129129

130130
let sink = Self::sink(run_opts);
131131
let color_cache = ColorSelector::default();
132+
// Set up correct size for underlying pty
133+
if let Some(pane_size) = experimental_ui_sender
134+
.as_ref()
135+
.and_then(|sender| sender.pane_size())
136+
{
137+
manager.set_pty_size(pane_size.rows, pane_size.cols);
138+
}
132139

133140
Self {
134141
color_cache,

crates/turborepo-ui/src/tui/app.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const FRAMERATE: Duration = Duration::from_millis(3);
1818
const RESIZE_DEBOUNCE_DELAY: Duration = Duration::from_millis(10);
1919

2020
use super::{
21-
event::{CacheResult, Direction, OutputLogs, TaskResult},
21+
event::{CacheResult, Direction, OutputLogs, PaneSize, TaskResult},
2222
input,
2323
search::SearchResults,
2424
AppReceiver, Debouncer, Error, Event, InputOptions, SizeInfo, TaskTable, TerminalPane,
@@ -771,6 +771,15 @@ fn update(
771771
Event::SearchBackspace => {
772772
app.search_remove_char()?;
773773
}
774+
Event::PaneSizeQuery(callback) => {
775+
// If caller has already hung up do nothing
776+
callback
777+
.send(PaneSize {
778+
rows: app.size.pane_rows(),
779+
cols: app.size.pane_cols(),
780+
})
781+
.ok();
782+
}
774783
}
775784
Ok(None)
776785
}

crates/turborepo-ui/src/tui/event.rs

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub enum Event {
1616
status: String,
1717
result: CacheResult,
1818
},
19+
PaneSizeQuery(std::sync::mpsc::SyncSender<PaneSize>),
1920
Stop(std::sync::mpsc::SyncSender<()>),
2021
// Stop initiated by the TUI itself
2122
InternalStop,
@@ -88,6 +89,12 @@ pub enum OutputLogs {
8889
ErrorsOnly,
8990
}
9091

92+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93+
pub struct PaneSize {
94+
pub rows: u16,
95+
pub cols: u16,
96+
}
97+
9198
#[cfg(test)]
9299
mod test {
93100
use super::*;

crates/turborepo-ui/src/tui/handle.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
};
55

66
use super::{
7-
event::{CacheResult, OutputLogs},
7+
event::{CacheResult, OutputLogs, PaneSize},
88
Event, TaskResult,
99
};
1010

@@ -72,6 +72,15 @@ impl AppSender {
7272
pub fn restart_tasks(&self, tasks: Vec<String>) -> Result<(), mpsc::SendError<Event>> {
7373
self.primary.send(Event::RestartTasks { tasks })
7474
}
75+
76+
/// Fetches the size of the terminal pane
77+
pub fn pane_size(&self) -> Option<PaneSize> {
78+
let (callback_tx, callback_rx) = mpsc::sync_channel(1);
79+
// Send query, if no receiver to handle the request return None
80+
self.primary.send(Event::PaneSizeQuery(callback_tx)).ok()?;
81+
// Wait for callback to be sent
82+
callback_rx.recv().ok()
83+
}
7584
}
7685

7786
impl AppReceiver {

0 commit comments

Comments
 (0)