Skip to content

Multiline support #27

@cengels

Description

@cengels

Is your feature request related to a problem? Please describe.
I'd like to implement multiline strings as the spinner text. All of the lines should be erased and rewritten when the spinner gets updated. Currently, this crate only clears and rewrites the last line.

I tried every other spinner crate out there I could find (including spinner, spinners, and zenity) and spinoff was the only one that I was able to successfully implement a workaround for. It's a bit hacky, but I'd like to present this workaround to you in the hopes that it can help you implement proper multiline support for spinoff. (That said, my workaround is perfectly serviceable for now so this is definitely very much a "nice to have" and there's no pressure at all.)

Describe the solution you'd like
When a string containing newlines is passed to the spinner, the spinner should count the number of newlines and use corresponding ANSI escape codes to move the terminal cursor up the correct number of lines before clearing all those lines and rewriting them.

Describe alternatives you've considered
Here's the workaround I implemented:

fn set_text(&mut self, mut string: String) {
  let old_line_breaks = self.line_breaks;
  self.line_breaks = string.lines().count() - 1;
  
  if old_line_breaks > self.line_breaks {
      // There would be outdated lines left over at the bottom of the output without this.
      std::io::stdout().execute(crossterm::terminal::Clear(crossterm::terminal::ClearType::FromCursorDown)).unwrap();
  }
  
  if self.line_breaks > 0 {
      // \x1B[#F -> moves the cursor up # lines, then moves cursor to the beginning of that line
      // \x1B[#C -> moves the cursor # columns to the right, to ensure the cursor isn't on top of the spinner
      let first_line_length = string.lines().nth(0).unwrap().len() + 2;
      write!(string, "\x1B[{}F\x1B[{}C", self.line_breaks, first_line_length).unwrap();
  }

  spinner.update_text(string);
}

The important thing here is to append the ANSI escape codes to the string, rather than the other way around, otherwise the spinner would break (since it would be update the spinner, then move the cursor up and erase the spinner straight away).

It's not pretty, mainly due to the fact that you can't move the cursor before rendering the spinner. This problem, I imagine, would be solved if this were implemented in the crate itself - the crate would likely still need to count the number of lines to move the cursor to the right position before rerendering the spinner, but you wouldn't need to move the cursor after rendering the text anymore, so it would be a bit prettier to look at. Strange things also start happening if the output is automatically line-wrapped; the String::lines() function doesn't cover that usecase.

Anyway, I just wanted to leave this here; even if you're not going to add this to the crate itself, it could still help anyone who's looking for a way to make multi-line strings work via this crate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions