Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Determine the terminal size in a subshell (unix) #276

Closed
3 tasks
Canop opened this issue Oct 18, 2019 · 7 comments
Closed
3 tasks

Determine the terminal size in a subshell (unix) #276

Canop opened this issue Oct 18, 2019 · 7 comments
Milestone

Comments

@Canop
Copy link
Contributor

Canop commented Oct 18, 2019

Like all other libraries I've checked, crossterm fails to read the terminal size when the crossterm based program is ran in a subshell.

I've found a solution which works in my tests: call tput cols and tput lines and parse the result:

fn tput_value(arg: &str) -> Option<u16> {
    match process::Command::new("tput").arg(arg).output() {
        Ok(process::Output{stdout, ..}) => {
                let value = stdout.iter()
                    .map(|&b| b as u16)
                    .take_while(|&b| b>=48 && b<=58)
                    .fold(0, |v, b| v*10 + (b-48));
                if value > 0 { Some(value) } else { None }
        }
        _ => None,
    }
}

pub fn terminal_size() -> (u16, u16) {
    let mut size = Terminal::new().size();
    #[cfg(unix)]
    {
        if size.is_err() {
            if let (Some(w), Some(h)) = (tput_value("cols"), tput_value("lines")) {
                return (w, h);
            }
        }
    }
    ...

I'm willing to post a PR.

Check-list before the PR:

  • wait for the various crossterm-rs subcrate to be merged
  • try to find if there's a similar problem (and solution) on Windows
  • decide a name and return type for this function (which should probably be consciously called as it starts new processes)

(In the meantime I've included the function in termimad which will let me run more tests)

@zrzka
Copy link
Contributor

zrzka commented Oct 18, 2019

I know that the tput is with us for a long time. As a safety measure, are we aware of any UNIX like platform (#[cfg(unix)]), which doesn't have tput?

This approach is pragmatic and I'm fine with it. Although, do we know how the tput works? I mean, is there're an easy way how to achieve the same thing without process::Command::new("tput")? If not, feel free to use tput, just asking as I do not want you to spend lot of time on reimplementing tput.

@joshtriplett
Copy link

Invoking a separate program has many problems, not least of which that it won't work if the program runs in a container or chroot.

tput typically invokes the TIOCGWINSZ ioctl, which returns the terminal size.

@Canop
Copy link
Contributor Author

Canop commented Oct 23, 2019

@joshtriplett I agree that invoking a separate program is problematic (should we use it in a separate function?) but you can easily check in a subshell that this solution works while the TIOCGWINSZ ioctl currently doesn't (and doesn't work either in a container).

I'd be happy to use a better solution, using tput is a workaround. Can you investigate and determine why tput works in subshells while the current solution in use in crossterm and similar libs doesn't ?

@zrzka
Copy link
Contributor

zrzka commented Oct 29, 2019

Just a thought here ... This is what we do for cursor ...

crossterm/src/input.rs

Lines 67 to 71 in 3ab5b17

/// Internal cursor position event. Don't use it, it will be removed in the
/// `crossterm` 1.0.
#[doc(hidden)]
#[cfg(unix)]
CursorPosition(u16, u16), // TODO 1.0: Remove

fn read_position_raw() -> Result<(u16, u16)> {
// Use `ESC [ 6 n` to and retrieve the cursor position.
let mut stdout = io::stdout();
stdout.write_all(b"\x1B[6n")?;
stdout.flush()?;
let mut reader = TerminalInput::new().read_sync();
loop {
if let Some(InputEvent::CursorPosition(x, y)) = reader.next() {
return Ok((x, y));
}
}
}

fn parse_csi_cursor_position(buffer: &[u8]) -> Result<Option<InternalEvent>> {
// ESC [ Cy ; Cx R
// Cy - cursor row number (starting from 1)
// Cx - cursor column number (starting from 1)
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
assert!(buffer.ends_with(&[b'R']));
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
.map_err(|_| could_not_parse_event_error())?;
let mut split = s.split(';');
let y = next_parsed::<u16>(&mut split)? - 1;
let x = next_parsed::<u16>(&mut split)? - 1;
Ok(Some(InternalEvent::CursorPosition(x, y)))
}

There's CSI P s ; P s ; P & P s = 1 8 which reports the size of the text area in characters as CSI 8 ; height ; width t. Then we can enhance InputEvent with TextAreaSize(u16, u16) and use it in the same way? Didn't test it, but it might work. And later we can move it to InternalEvent along with the CursorPosition variant.

@TimonPost TimonPost added this to the 0.13 milestone Oct 29, 2019
@TimonPost
Copy link
Member

This issue is fixed with #283, if not, re-open.

@joshtriplett
Copy link

you can easily check in a subshell that this solution works while the TIOCGWINSZ ioctl currently doesn't

What do you mean by "in a subshell" here? I have checked that running tput in a subshell still correctly figures out the terminal size. tput just tries stderr, stdout, and stdin, to see if they're hooked up to the terminal, and if not then it opens /dev/tty and uses that. What is tput doing that crossterm couldn't do itself, and under what circumstances does TIOCGWINSZ not work in a subshell?

@Canop
Copy link
Contributor Author

Canop commented Dec 10, 2019

@joshtriplett Write a sub_program program needing to know the size of the terminal. Let's imagine it writes that size on stdout.

Then call it as

echo "my program says $(sub_program)"

Then sub_program runs in a subshell and the previous way of obtaining the terminal dimension didn't work.

What tput does could probably be reproduced, and I'm all for doing it, but in the meantime calling it is a working workaround.

december1981 pushed a commit to december1981/crossterm that referenced this issue Oct 26, 2023
This commit also adds support for borrowed deserialization (crossterm-rs#231, RUST-688) and fixes RUST-880 and RUST-884.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants