Search code examples
pythonpython-3.xsubprocess

subprocess stdin not working with less as process


I have a function that renders an input file to a custom format. I want that a user can start this script in one terminal to view the rendered output with a pager. When he edits the source-file in another terminal and saves, these changed should be rerendered and updated in the pager.

Detecting these changes and rerendering them works. The problem is the updating of the pager.

After some search I found that less can do this reloading when pressing 'R':

R                    Repaint screen, discarding buffered input.

So I tried to implement this behavior in my code:

pager_process = subprocess.Popen(['less', filepath], stdin=subprocess.PIPE)
...
pager_process.stdin.write(b'R')  # HERE: this should reload less but doesn't

But this didn't work.

Any idea or deeper knowledge that I don't know of?

Original Code
source = Path(source)  # some sort of input file

def render_source() -> str:
    try:
        return convert(fp=source)  # renders the input-file in a wished format
    except SyntaxError as error:
        return '\n'.join(traceback.format_exception(type(error), error, error.__traceback__))  # in case rendering fails

def update_tmpfile(new: str) -> None:
    tmp.truncate(0)
    tmp.seek(0)
    tmp.write(new)
    tmp.flush()

with tempfile.NamedTemporaryFile('w+') as tmp:
    last_mtime = source.stat().st_mtime_ns
    content = render_source()
    update_tmpfile(content)
    pager_process = subprocess.Popen(['less', tmp.name], stdin=subprocess.PIPE)

    try:
        while pager_process.poll() is None:  # not exited
            time.sleep(0.1)

            current_mtime = source.stat().st_mtime_ns
            if last_mtime != current_mtime:  # source changed -> rerender
                last_mtime = current_mtime

                content = render_source()
                update_tmpfile(content)
                pager_process.stdin.write(b'R')  # HERE: this should reload less but doesn't
    except Exception:
        pager_process.kill()
        raise

Solution

  • After some research I finally managed to find a solution.

    Instead of an external pager process I use the pypager library

    I create a pager instance with

    pager = pypager.Pager()
    pager.add_source(pypager.StringSource(""))  # 0 index buffer
    pager.add_source(pypager.StringSource(""))  # page-buffer that gets replaced
    

    and can update the rendered content with

    content = render_source()
    pager.remove_current_source()
    pager.add_source(pypager.StringSource(content))
    pager.application.invalidate()