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

Provide available terminal space #5

Closed
vadimdemedes opened this issue Jul 9, 2017 · 21 comments
Closed

Provide available terminal space #5

vadimdemedes opened this issue Jul 9, 2017 · 21 comments

Comments

@vadimdemedes
Copy link
Owner

See #3 (comment).

The problem is demonstrated perfectly when rendering a progress bar:

<div>
  <Label/>
  <ProgressBar/>
</div>

In order to render a progress bar that takes entire terminal space (width), <ProgressBar> needs to know how much space <Label> takes.

I'm short of ideas on how to implement this properly, so any thoughts are welcome!

@xdave
Copy link

xdave commented Jul 10, 2017

I think figuring this out would also solve the nested-div-newline problem.

@xdave
Copy link

xdave commented Jul 10, 2017

@marionebl
Copy link
Contributor

marionebl commented Sep 2, 2017

Perhaps this is overcomplicating things, but introducing a View using yoga-layout could solve this by introducing a full layout engine. I am not quite sure how one would go about building this. Reading and understanding the code of projects using yoga-layout should help, though:

  • react-vr
  • react-pdf
  • react-sketchapp

Is this something you'd be interested in?

@vadimdemedes
Copy link
Owner Author

I think this is definitely interesting, but might be an overkill for now. I still hope there will be a simple solution to that issue.

@karaggeorge
Copy link
Contributor

I'd like to give this one a try. What exactly are we looking for? Should it be that every element past the first one should render in less space than the entire terminal based on the previous elements?

Should that happen for elements in a div? or in general?

Also, would it make sense to automatically cut anything that goes over the terminal length for each line (instead of creating another line)? Or just let the developer take care of making sure all lines are shorter than the total available space?

@vadimdemedes
Copy link
Owner Author

vadimdemedes commented Apr 9, 2018

I'd like to give this one a try. What exactly are we looking for? Should it be that every element past the first one should render in less space than the entire terminal based on the previous elements?

I thought each component would have access to these props:

  • x - distance from the left side of terminal
  • y - distance from the first row of output

I think exposing these props would allow components to calculate the available space in the current row. Ink would also have to rerender the tree whenever terminal is resized. This is ideal implementation, but might be complex to implement.

Should that happen for elements in a div? or in general?

I think all elements should have access to these props.

Also, would it make sense to automatically cut anything that goes over the terminal length for each line (instead of creating another line)? Or just let the developer take care of making sure all lines are shorter than the total available space?

I think given the props above, developer should decide themselves whether to cut the output or not.


I just had an idea to perhaps simplify the first implementation and experiment:

<SpaceAware>
	<SpaceAware.Item>
		<Color red>Progress: </Color>
	</SpaceAware.Item>

	<SpaceAware.Item>
		{({ spaceAvailable }) => (
			<ProgressBar width={spaceAvailable} value={0.5}/>
		)}
	</SpaceAware.Item>
</SpaceAware>

Naming isn't final of course, but the idea is this: SpaceAware renders to string via renderToString() each SpaceAware.Item child and calculates spaceAvailable for each of those and pass it down via render function. I realize it's not the prettiest implementation, but it's a start and could serve as a temporary solution until the "complete" system of x and y coordinates is implemented.

@karaggeorge @sindresorhus What do you think?

@karaggeorge
Copy link
Contributor

@vadimdemedes I'm a bit confused with the x and y system. How can a component (like ProgressBar) calculate how wide it has to be just using the proposed x and y. I think maybe a left and right type of prop my help more. left being the x and right being the maximum space left for the component to take on the terminal. Actually, that would make more sense if only the right-most component received the right.

It might be also weird to use renderToString() to calculate the size, but the components will need these new props to render in the first place.

The x and y as described would actually help a lot if we want to only update a specific component that updated instead of the whole tree.

Another idea might be having the components pass a flex: true prop in a surrounding component which would then pass the available size left on the screen to its child after all non-flex components have been rendered. If more than one flex components exist in one line, they would split the space between them. Thoughts?

@vadimdemedes
Copy link
Owner Author

vadimdemedes commented Apr 9, 2018

I'm a bit confused with the x and y system. How can a component (like ProgressBar) calculate how wide it has to be just using the proposed x and y.

You can get the available space using terminal width - x = available width. The y coordinate is for placing mouse cursor, you can ignore it for this example.

I think having a flex prop would be complicated to explain and use.

Let me play with my idea and see if it even works. I'll submit it as a PR and ping you.

@karaggeorge
Copy link
Contributor

@vadimdemedes

You can get the available space using terminal width - x = available width

that would be true only if the component is the last one on the line, right?

@vadimdemedes
Copy link
Owner Author

Yes, I totally missed that, good catch.

@vadimdemedes
Copy link
Owner Author

@karaggeorge I implemented my idea but indeed is limited to 2 components on one row, side by side. I rethinked your idea with flex and I think it's the best way to solve this. My idea is to implement an alternative of CSS flexbox in Ink. That way layout is clearly determined and straightforward to understand. My thoughts:

// Progress: [======] (ProgressBar takes all the available space)
<Box>
	<Box>
		Progress:
	</Box>

	<Box flex={1}>
		<ProgressBar/>
	</Box>

	<Box>
		15%
	</Box>
</Box>
<Box height={20} alignItems="center" justifyContent="center">
	<HelloWorld/>
</Box>

It's indeed going to be a basic reimplementation of CSS flexbox, but using rows and columns instead of px.

@karaggeorge
Copy link
Contributor

@vadimdemedes Yeah, CSS flexbox is where I got that idea. I think it would make sense in this case, and it solves a couple of problems. What are you thinking about a full set of props? I was thinking just the flex-grow, but I see you incorporated more of them.

I'm wondering about the height. How would that display with the children of that element? Would it be a new line before and after the box? Like a div with a set height?

@vadimdemedes
Copy link
Owner Author

What are you thinking about a full set of props?

I think we should start with the minimum set first and expand over time.

I'm wondering about the height. How would that display with the children of that element? Would it be a new line before and after the box? Like a div with a set height?

Exactly 🙂

@karaggeorge
Copy link
Contributor

@vadimdemedes
I think that makes sense. I'll start working on PR. Do we want it to be a prop passed in to any component? Or do we want to have a wrapper that does it for the children? The way you had it before:

<SpaceAware>
	<A/>
</SpaceAware>

or <Flex/> as a wrapper, which would lead to A receiving an availableSpace prop.

@vadimdemedes
Copy link
Owner Author

@karaggeorge Sorry for late response. I thought we could go with #5 (comment) to provide a complete layout solution. I tried implementing <SpaceAware> and it looks more like a hack than a solution.

@marionebl
Copy link
Contributor

It's indeed going to be a basic reimplementation of CSS flexbox, but using rows and columns instead of px.

@vadimdemedes Does it make sense to explore yoga-layout to do this?

@vadimdemedes
Copy link
Owner Author

Yoga is built with cross platform in mind. To ensure Yoga can be used anywhere, it was written in portable C/C++ and has a low number of dependencies and small binary size. This means Yoga can be used on iOS and Android, sharing knowledge, and potentially code, between platforms.

@marionebl I guess this means that Ink will have to include a native dependency, which I'd like to avoid.

@sindresorhus
Copy link
Collaborator

Yoga has an asm.js build for browsers, which we could use in Node.js to not need a native dependency: https://github.com/facebook/yoga#build-platforms

@danikaze
Copy link

danikaze commented Apr 1, 2019

I tried to implement something similar.
Just a simple <AppTitle> component, which renders a colored bar with the centered title.
The problem is that Box doesn't render bgColor so I need to do something like this:

export function AppTitle(props: Props) {
  const text = props.children;
  const width = props.width;
  const middle = Math.floor((width - text.length)/ 2) + text.length;

  return (
    <Color bgBlue white bold>
      {text.padStart(middle, ' ').padEnd(width, ' ')}
    </Color>
  );
}

Works like a charm. width is passed as a prop (which I think I could automatically read from stdout via process or <StdoutContext>, but I'm getting it from redux state so the component it's re-rendered when the window is resized.

But there's a problem (of course), and it's that the component is rendered multiple times instead of being replaced (probably a different issue #153 ?)

@vadimdemedes
Copy link
Owner Author

Going to close this issue, as there's a <Box flexGrow={1}> component which will fill all available space and #168 is going to introduce an API to measure <Box> width.

@samuela
Copy link

samuela commented Nov 23, 2023

I'm curious how this is implemented under the hood: How does ink get the number of rows/cols for the current tty, and detect when that changes?

I'm currently interested in serving Ink sessions over a custom SSH server. The SSH protocol gives the the rows/cols of the user's terminal, but it's not clear to me how to pass these values to Ink's render. Can Ink accomplish something like this?

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

7 participants