Redirecting python stdin to a "text input" widget?

Hello !

In the project I’m working on (StreamPy), which is basically an implementation of the python console in a streamlit app, I need to redirect python stdin to a custom object with a readline() method supposed to take the input via some kind of text widget appearing on the app, rather than from the terminal. This would allow to use python ‘input’ command as usual and, more generally, deal with various scripts taking inputs from stdin (“press Enter to continue” and all that kind of stuff) directly from the app.

Otherwise, the app waits for input from the terminal… which is not really handy.

The issue here, is the intrinsic “blocking” behavior of reading from stdin. Whenever a program needs input from there, it pauses execution until the user completes the input, and then resumes executing.

Unfortunately the standard st.text_input widget doesn’t block script execution when called… it will just return None, every new refresh loop, until a string is entered. Which is not what we want here.

So, running the app locally on my computer, I tried to redirect stdin to a simple tkinter input box to achieve the desired behavior. Which worked rather well. But it’s not pretty, visually speaking, and won’t work if the app runs on the cloud’s server.

It would be prettier to use a special widget for that purpose, but this “text input” widget should be designed to render and immediately pause the app’s script execution until a string is returned.

I there a way to achieve this kind of “blocking” behavior for a text input widget ?

I mean, is this even possible to achieve with a custom component, given the current implementation of streamlit ?

Thanks for your help !

Cheers

Baptiste

Hi @B4PT0R

Have you tried using the subprocess module particularly the Popen() method to run command-line commands and have it return the output.

Another route could be to leverage LLMs, for example, using the LangChain framework’s Shell tool to interact with the shell:

Hope this helps!

1 Like

Hi @dataprofessor !

Thanks for the reply.

The issue isn’t about interactive code execution, or output redirection. All this already works fine.

Let me attempt to clarify.

When the user types code in the console’s input cell, the execution is handled by a code.InteractiveConsole object whose stdout and stderr are redirected to st.write widgets popping in the app in real time. This part already works well.

The problem I have arises when having to deal with stdin redirection.

If I don’t implement any, anytime the user runs a command requiring keyboard input (such as input(“Enter text here:”) ) the console will try to read keyboard inputs from the terminal, which is not optimal in terms of user experience, at all. Ideally, I’d like to be able to redirect the reading to a text_input (or other) widget within the app itself.

The redirection of stdin is not particularly difficult to implement per se. What makes the thing problematic is the lack of a (modal) text input widget to handle this redirection properly. As a matter of fact, when the python console will need input from stdin, it will call stdin.readline() method and pause execution until a string is returned. By redirecting stdin to a custom object implementing readline the way we want, we have a shot at rendering a text_input widget (I tried, the widget renders) but its non-modal nature will make it return None instantly and code execution will resume without giving the user a chance to input any string…

So my question is really about : Is there a way, even complex, to achieve a custom MODAL text_input widget ?

What would be really neat, would be to have to possibility to choose it directly within the st.text_input API :

txt=st.text_input("Enter text here:", modal=True) 

So that the text_input widget blocks execution until a string is actually inputted into it.

But I guess dreams don’t always come true… :slight_smile:
I’d be happy enough already with any hacky solution to this problem.

I hope I made it clearer this time. It is not very easy to verbalize.

Anyways, thanks for trying to help !

Cheers!

Baptiste

Hi!

To follow up on this topic I now understand why a modal widget won’t be possible to achieve given Streamlit’s functioning…

It HAS TO rerun the whole script for python to receive the incoming outputs from the React widgets…

Which make stdin redirection plainly impossible.

I think I’ve found a nice hack to this situation though, which will work at least when the app is run locally.

It involves the pynput module to listen to keystrokes… Need to test this thoroughly.

Stay tuned !

Hello,
I’m facing the same issue. Any update regarding this?
Thanks!

Hello.
Yes, I found a way to make it work, but it’s very hacky.
The idea is to implement some kind of instant websocket queue communication between the front end and the python back end. Since Streamlit cloud server doesn’t allow to open websockets ports (as far as I know), I worked around this using a firestore database to serve as a channel of communication between backend and frontend (probably overkill to use firebase here, but I used it anyway for user authentication and cloud storage).
It consists in two parts:

  1. A listener, whose task is to watch for changes to a specific field in the firestore document.
  2. a custom ‘input’ javascript component. whose task is to write to this field in the firestore document.
    I wrapped it all up in a function that:
    -starts the listener
    -renders the widget
    -waits for a response from the listener (actually blocking the script in a sleep loop until a string is received)
    -stops the listener and returns the string when received.

then I redirect stdin to this function and that’s it.

All this is quite heavy to implement, for sure, but definitely worth the masochistic challenge :slight_smile:

Quite slower than what you might expect from a conventional stdin redirection, but reasonably fast to be usable.

You can check the code here and here

You can try it here. Just create an account (free) and try to run some input command in the interpreter.

Hope this helps!

Cheers!

This is what I have done. So far it’s working.

def login():
        f, name = _tempfile.mkstemp(dir='./')
        gcloud_command = 'gcloud auth application-default login --quiet --no-launch-browser'
        gcloud_process = subprocess.Popen(gcloud_command,
                                        shell=True,
                                        stdin=subprocess.PIPE,
                                        stdout=f,
                                        stderr=subprocess.STDOUT,
                                        universal_newlines=True,
                                        text=True)
        return f, name, gcloud_process

f, name, gcloud_process = login()

while True:
    _time.sleep(1)
    _os.fsync(f)
    prompt = open(name).read()
    if 'https' in prompt:
        break

prompt = prompt.rstrip()
prompt = '\n'.join(
    [line for line in prompt.splitlines() if 'launch-browser' not in line]
)
st.write(prompt)
code = st.text_input(' ', placeholder="Enter Code here")
while True:
    if code == "":
        _time.sleep(1)
    else:
        st.session_state['shell_open'] = False
        break

st.write(code)
gcloud_process.communicate(input=code.strip())

Hello,

I’ve designed a custom component solving this problem. Check it out!

Cheers!