Ink: Can ink be used as a "full" screen application?

Created on 14 Mar 2020  路  12Comments  路  Source: vadimdemedes/ink

Hey; I love the concept of this library. Just wondering if it's possible to make a "full" screen application using ink? By that I mean, something like htop which takes over the full screen, and when you quit - it leaves your original terminal completely in tact.

This is something that can be done with python's curses library, or when using blesses:

var blessed = require('blessed');

// Create a screen object.
var screen = blessed.screen({
  smartCSR: true
});

var box = blessed.box({
  top: 'center',
  left: 'center',
  width: '50%',
  height: '50%',
  content: 'Hello {bold}world{/bold}!',
  tags: true,
  border: {
    type: 'line'
  },
  style: {
    fg: 'white',
    bg: 'magenta',
    border: {
      fg: '#f0f0f0'
    },
    hover: {
      bg: 'green'
    }
  }
});

screen.append(box);

screen.key(['escape', 'q', 'C-c'], function(ch, key) {
  return process.exit(0);
});

screen.render()

Which opens the "full" screen application, and leaves the original terminal intact after quitting it. Any pointers/direction would be appreciated :+1:

Most helpful comment

@taras I learnt more about curses/ncurses and terminfo, via man terminfo.

I found out you can use ansi escape codes to swap to an alternate screen buffer for 'full screen' applications, and once you're done you can toggle back to the main buffer:

const enterAltScreenCommand = '\x1b[?1049h';
const leaveAltScreenCommand = '\x1b[?1049l';
process.stdout.write(enterAltScreenCommand);
process.on('exit', () => {
    process.stdout.write(leaveAltScreenCommand);
});

Therefore as a full example:

const React = require("react");
const { render, Color, useApp, useInput } = require("ink");

const Counter = () => {
  const [counter, setCounter] = React.useState(0);
  const { exit } = useApp();

  React.useEffect(() => {
    const timer = setInterval(() => {
      setCounter(prevCounter => prevCounter + 1);
    }, 100);

    return () => {
      clearInterval(timer);
    };
  });

  useInput((input, key) => {
    if (input === "q" || key.escape) {
      exit();
    }
  });

  return <Color green>{counter} tests passed</Color>;
};

const enterAltScreenCommand = "\x1b[?1049h";
const leaveAltScreenCommand = "\x1b[?1049l";
process.stdout.write(enterAltScreenCommand);
process.on("exit", () => {
  process.stdout.write(leaveAltScreenCommand);
});

render(<Counter />);

Working example that doesn't conflict with the existing buffer:

alt-screen-buffer

That seems to be what I was after - I'm not sure if that's something that should be baked into ink or not.

All 12 comments

@AlanFoster you can definitely do this by playing around the components that Ink comes with. You can render a Box component that has height that is the same height as terminal and it'll be full screen. Because Ink uses Yoga, which provides Flexbox APIs, the approach is similar to how you'd create a full screen web app with Box components.

@taras I learnt more about curses/ncurses and terminfo, via man terminfo.

I found out you can use ansi escape codes to swap to an alternate screen buffer for 'full screen' applications, and once you're done you can toggle back to the main buffer:

const enterAltScreenCommand = '\x1b[?1049h';
const leaveAltScreenCommand = '\x1b[?1049l';
process.stdout.write(enterAltScreenCommand);
process.on('exit', () => {
    process.stdout.write(leaveAltScreenCommand);
});

Therefore as a full example:

const React = require("react");
const { render, Color, useApp, useInput } = require("ink");

const Counter = () => {
  const [counter, setCounter] = React.useState(0);
  const { exit } = useApp();

  React.useEffect(() => {
    const timer = setInterval(() => {
      setCounter(prevCounter => prevCounter + 1);
    }, 100);

    return () => {
      clearInterval(timer);
    };
  });

  useInput((input, key) => {
    if (input === "q" || key.escape) {
      exit();
    }
  });

  return <Color green>{counter} tests passed</Color>;
};

const enterAltScreenCommand = "\x1b[?1049h";
const leaveAltScreenCommand = "\x1b[?1049l";
process.stdout.write(enterAltScreenCommand);
process.on("exit", () => {
  process.stdout.write(leaveAltScreenCommand);
});

render(<Counter />);

Working example that doesn't conflict with the existing buffer:

alt-screen-buffer

That seems to be what I was after - I'm not sure if that's something that should be baked into ink or not.

Nice one. It would be nice if it was. Maybe a component that you could wrap around the main container?

@AlanFoster Interesting! Was always wondering how it's being done. @taras Indeed, perhaps there could be a component that would take care of this.

Nice tips @AlanFoster! I'm using this for a CLI I'm working on now and have wrapped it into a small component locally. Happy to publish that if you would find it useful / weren't planning on pushing one up yourself.

@tknickman I'd be interested in reading that

@tknickman Sure, go ahead 馃憤I think doing that in a cross platform way will be an interesting challenge. It would be also be interesting to see what layouts/other components make sense to develop in a world of full screen cli apps.

This is probably a bit on the side of this issue, but I'm just getting started using Ink and want to create a "full screen app". Was wondering what the trick is to keep the "app" alive instead of exiting? Basically don't exit until I hit Q or ^C.

For what it's worth I just ended up using this for Full Screen. Not really enough code to warrant publishing it imo - but works great!

import { useEffect } from 'react';

const enterAltScreenCommand = '\x1b[?1049h';
const leaveAltScreenCommand = '\x1b[?1049l';

const exitFullScreen = () => {
  process.stdout.write(leaveAltScreenCommand);
};

const FullScreen = ({ children }) => {
  useEffect(() => {
    // destroy alternate screen on unmount
    return exitFullScreen;
  }, []);
  // trigger alternate screen
  process.stdout.write(enterAltScreenCommand);
  return children;
};

export { exitFullScreen };
export default FullScreen;

@tknickman you want an empty array as second argument to useEffect in FullScreen in order to only execute it once.

ah for sure great catch!

@tknickman turns out you also want process.stdout.write(enterAltScreenCommand); to be inside the useEffect so that that is only called once. 馃槈

Was this page helpful?
0 / 5 - 0 ratings