notcurses icon indicating copy to clipboard operation
notcurses copied to clipboard

Help with using Notcurses through Neovim

Open dccsillag opened this issue 4 years ago • 41 comments

I've finally been able to take some time to tackle an issue in one of my projects, https://github.com/dccsillag/magma-nvim/issues/15. The idea over there is to use Notcurses in order to show images in the terminal in a more portable manner.

I've successfully come up with the following (sketched!) C code to show an image on some given position and dimensions:

#include <notcurses/notcurses.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    struct notcurses_options options = {};
    struct notcurses* nc = notcurses_init(&options, NULL);
    if (!nc) {
        puts("Couldn't initialize notcurses");
        return -1;
    }

    // Create ncplane
    struct ncplane_options ncp_opts = {
        .y = 0,
        .x = 0,
        .rows = 100,
        .cols = 120,
    };
    struct ncplane* n = ncplane_create(notcurses_stdplane(nc), &ncp_opts);

    // Show image
    struct ncvisual* ncv = ncvisual_from_file("out.png");
    if (!ncv) {
        notcurses_stop(nc);
        puts("couldn't load ncvisual from file");
        return -1;
    }
    struct ncvisual_options ncv_opts = {
        .scaling = NCSCALE_SCALE,
        .blitter = NCBLIT_PIXEL,
    };
    ncv_opts.n = n;
    if (!ncvisual_blit(nc, ncv, &ncv_opts)) {
        notcurses_stop(nc);
        puts("couldn't blit");
        return -1;
    }

    // Render
    if (notcurses_render(nc)) {
        notcurses_stop(nc);
        puts("Couldn't render");
        return -1;
    }

    sleep(1);

    ncplane_erase(n);

    if (notcurses_render(nc)) {
        notcurses_stop(nc);
        puts("Couldn't render");
        return -1;
    }

    sleep(1);

    ncplane_destroy(n);

    return notcurses_stop(nc);
}

In my case, I need to write my code in Python. Based on the code above, I came up with the following script which does the same:

from notcurses.notcurses import lib, ffi
import sys
import time


# Initialize Notcurses
nc_opts = ffi.new("struct notcurses_options *")
nc_opts.flags = (lib.NCOPTION_NO_ALTERNATE_SCREEN
                 | lib.NCOPTION_NO_CLEAR_BITMAPS
                 | lib.NCOPTION_NO_WINCH_SIGHANDLER
                 | lib.NCOPTION_PRESERVE_CURSOR
                 | lib.NCOPTION_SUPPRESS_BANNERS)
nc = lib.notcurses_init(nc_opts, sys.__stdout__)
assert nc

# Setup plane
ncplane_options = ffi.new("struct ncplane_options *")
ncplane_options.y = 0
ncplane_options.x = 0
ncplane_options.rows = 50
ncplane_options.cols = 50
n = lib.ncplane_create(lib.notcurses_stdplane(nc), ncplane_options)

# Show image
ncv = lib.ncvisual_from_file(b"out.png")
assert ncv != 0

ncv_opts = ffi.new("struct ncvisual_options *")
ncv_opts.scaling = lib.NCSCALE_SCALE
ncv_opts.blitter = lib.NCBLIT_PIXEL
ncv_opts.n = n
assert lib.ncvisual_blit(nc, ncv, ncv_opts)

lib.ncvisual_destroy(ncv)

lib.notcurses_render(nc)

time.sleep(1)

# Erase the image
lib.ncplane_erase(n)

lib.notcurses_render(nc)

time.sleep(1)

# Clean up
lib.ncplane_destroy(n)
lib.notcurses_stop(nc)

This works as expected when run as python test.py (with that code in test.py). However, I need to run something along the lines of this Python code above from Neovim. When attempting to do so, via:

:py3 exec(open("/tmp/notcurses-tests/test.py").read())

I get no errors, but I see no output. The same thing happens if I do

:py3 os.system("./a.out")

where a.out is the executable compiled from the C code above.

I expect this was probably because Neovim did something like redirect stdout to somewhere weird.

I tried a bunch of hacky and non-portable solutions (e.g., go up the ppid tree to get the containing pty's file descriptor) but none of them worked at all.

So now I'm drawing a blank. What could I do to use basic notcurses functionality (creating a plane, drawing an ncvisual to it, rendering that ncvisual to a part of the terminal grid, and later erasing it) from a program that likes to do weird things with stdout? Or even, is there something I could do to help debug this?

In any case, thank you for your time!

dccsillag avatar Oct 29 '21 10:10 dccsillag

i'll look into this ASAP, but python is definitely not my area of expertise, so it might take a bit of exploration and experimentation. thanks for the report! i'll see what i can do to help you =].

dankamongmen avatar Oct 29 '21 16:10 dankamongmen

Thank you very much! I understand Python is not quite your thing, take your time -- if you need/want help, hit me up.

dccsillag avatar Oct 29 '21 16:10 dccsillag

BTW, this isn't a Python-specific issue -- it's more like a Neovim-specific issue, as evidenced by the fact that doing a system call of the compiled C code has the same result. That means that if we can solve this via C this should be solved on the Python end as well.

dccsillag avatar Oct 29 '21 22:10 dccsillag

yeah, realized that when i looked at it later. it's friday night here, and i've got some people coming over, but i've got this slated for my first thing to look into tomorrow =]. sorry for the delay

dankamongmen avatar Oct 30 '21 00:10 dankamongmen

No worries, take your time! Again, thank you very much!

dccsillag avatar Oct 30 '21 00:10 dccsillag

alright, taking a look at this now =]

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

ahhh ok so your entire issue is getting something displayed up from within NeoVim. gotcha. if it captures stdout, it will indeed not display. what if you tried something like ./a.out > /dev/tty? is redirection possible in this context?

if not, a small wrapper that opened /dev/tty and dup()ed it onto stdout ought work.

alternatively you could write the output to /dev/tty from within neovim?

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

perhaps i ought add a NOTCURSES_OPTION_FORCETTY that attempts to write to the controlling tty rather than stdout...hrmmm.

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

perhaps i ought add a NOTCURSES_OPTION_FORCETTY that attempts to write to the controlling tty rather than stdout...hrmmm.

this feels kinda unnecessary, since the calling code ought always be able to set up stdout the way they want. this is at least true in C. i'd think python could do so as well; let me look around a bit.

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

2021-10-31-052045_1453x795_scrot

so here you can see what's going down. first i run ncplayer -k, which dumps to stdout. i then rediect to atma.txt. nothing is displayed, but i can then cat that output, and it is displayed.

so yeah, neovim is taking that output up, presumably by creating a pair of pipes before execing and dup()ing that pipes to the child process's standard I/O. that way, it gets the output as a buffer. so you either need it to display that output without any processing (i doubt it will let you do this), or redup() /dev/tty in your program.

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

btw note that the second output is oddly placed, obscuring the line from which it was launched. that's because there's cursor-positioning info inside that file. this is another reason why it would be better to redup() /dev/tty, as opposed to printing the output from within neovim.

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

if this whole concept of dup()ing /dev/tty onto stdout isn't something well known to most developers, maybe i ought go ahead and do the option...

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

Just woke up to this! :smile:

Okay, I can try duping into /dev/tty. But I have two questions:

  1. How portable is dup()ing to /dev/tty? In particular, is it a POSIX thing or is it a Linux thing?
  2. dup()ing is easy. However, I'm not sure how I can get the controlling tty; I actually already have some code which will give me the controlling pty, and I can try to use that, but first I'd like to know if they're the same thing; and, at the same time, know if there's some nice way of getting it (I think the code we have right now travels up the ppid tree until reaching a process with a pty)

dccsillag avatar Oct 31 '21 09:10 dccsillag

Also, just to check, I'd be using dup2, not dup, right?

dccsillag avatar Oct 31 '21 09:10 dccsillag

dup2(), which is what you'll probably actually end up using, is UNIX Magic From Beyond the Dawn of Time, i.e. i'm pretty sure it precedes POSIX. it's certainly discussed at length in the first edition of Advanced Programming in the Unix Environment (Stevens 1992). i use it on BSDs and macOS. it's not on windows.

/dev/tty is an alias for the controlling tty =]

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

dup2(), which is what you'll probably actually end up using, is UNIX Magic From Beyond the Dawn of Time, i.e. i'm pretty sure it precedes POSIX. it's certainly discussed at length in the first edition of Advanced Programming in the Unix Environment (Stevens 1992). i use it on BSDs and macOS. it's not on windows.

Gotcha! I'll implement it and then ask for MacOS people to test it then. :)

/dev/tty is an alias for the controlling tty =]

Ah! I didn't know that! Yeah, I'm glad I asked that, haha

dccsillag avatar Oct 31 '21 09:10 dccsillag

some other things to note:

  1. you'll need to position the cursor appropriately. if neovim leaves it where you want it, use NOTCURSES_OPTION_PRESERVE_CURSOR and the standard plane will be initialized with the cursor at that location.

  2. you'll definitely want NOTCURSES_OPTION_INHIBIT_ALTERNATE_SCREEN (or whatever) and NOTCURSES_OPTION_NOBANNERS

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

yep! sorry for being so slow in getting back to you on this; i saw your two code samples and filed this away as a "will have to go debug user's code" problem

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

honestly ncplayer -k filename might do exactly what you want, once you have the dup(2).

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

yep! sorry for being so slow in getting back to you on this; i saw your two code samples and filed this away as a "will have to go debug user's code" problem

No worries!

dccsillag avatar Oct 31 '21 09:10 dccsillag

Quick question -- with what options should I open /dev/tty? write-only?

dccsillag avatar Oct 31 '21 09:10 dccsillag

like i'm not promising antyhing but the following might work:

#!/bin/sh

ncplayer -k "$@" > /dev/tty

since you can hit /dev/tty from any scope, this uses the shell to do the rebinding, and then shows whatever with ncplayer. it might not work perfectly, but it ought suffice to test the basic idea.

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

Ah, fair enough

dccsillag avatar Oct 31 '21 09:10 dccsillag

I'll try that

dccsillag avatar Oct 31 '21 09:10 dccsillag

Quick question -- with what options should I open /dev/tty? write-only?

all you ought need is write, yeah, unless you don't lol

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

this is all contingent on neovim not redrawing the screen after it calls out, which it might do because of exactly this kind of thing. if that's the case, nothing's going to work; you'll need some different kind of neovim integration.

dankamongmen avatar Oct 31 '21 09:10 dankamongmen

all you ought need is write, yeah, unless you don't lol

LOL

like i'm not promising antyhing but the following might work:

#!/bin/sh

ncplayer -k "$@" > /dev/tty

since you can hit /dev/tty from any scope, this uses the shell to do the rebinding, and then shows whatever with ncplayer. it might not work perfectly, but it ought suffice to test the basic idea.

I tried that, it sort of didn't work; I'll try to record a video, but essentially, instead of the escapes being interpreted by the terminal emulator, they were displayed in escaped form. Maybe neovim is the actual /dev/tty?

dccsillag avatar Oct 31 '21 09:10 dccsillag

this is all contingent on neovim not redrawing the screen after it calls out, which it might do because of exactly this kind of thing. if that's the case, nothing's going to work; you'll need some different kind of neovim integration.

I'm pretty sure we don't need to worry about that, but we'll see.

dccsillag avatar Oct 31 '21 09:10 dccsillag

I tried that, it sort of didn't work; I'll try to record a video, but essentially, instead of the escapes being interpreted by the terminal emulator, they were displayed in escaped form. Maybe neovim is the actual /dev/tty?

For some reason I can't reproduce that anymore... what happens now is the following: when run from Neovim, test.sh (which has that shell script you sent) exits with exit code 256, having shown nothing.

dccsillag avatar Oct 31 '21 09:10 dccsillag

ok, i'll have to look at what neovim is doing, exactly. i assume the script does work outside of neovim?

dankamongmen avatar Oct 31 '21 09:10 dankamongmen