5

I need to design a script that uses the top portion of the terminal as output where some lines are printed after each second in an infinite loop, and the bottom portion keeps taking user input and also printing them in the above portion (among the regular periodic outputs).

In other words, I need to design a sort of shell.

I tried multithreading with the naive approach like this:

#!/usr/bin/python3

from math import acos
from threading import Thread
from random import choice
from time import sleep
from queue import Queue, Empty

commandQueue = Queue()

def outputThreadFunc():
    outputs = ["So this is another output","Yet another output","Is this even working"] # Just for demo
    while True:
        print(choice(outputs))
        try:
            inp = commandQueue.get(timeout=0.1)
            if inp == 'exit':
                return
            else:
                print(inp)
        except Empty:
            pass        
        sleep(1)

def inputThreadFunc():
    while True:
        command = input("> ") # The shell
        if command == 'exit':
            return
        commandQueue.put(command)

# MAIN CODE
outputThread = Thread(target=outputThreadFunc)
inputThread = Thread(target=inputThreadFunc)
outputThread.start()
inputThread.start()
outputThread.join()
inputThread.join()

print("Exit")

But as obviously expected, the output lines merge with the input lines as the user keeps typing.

Any ideas?

9

3 Answers 3

2
+25

As discussed in comments, used curses library.

Update

used two subwin for input and output

#!/usr/bin/python3

import curses

from math import acos
from threading import Thread
from random import choice
from time import sleep
from queue import Queue, Empty


commandQueue = Queue()

stdscr = curses.initscr()
stdscr.keypad(True)

upperwin = stdscr.subwin(2, 80, 0, 0)
lowerwin = stdscr.subwin(2,0)

def outputThreadFunc():
    outputs = ["So this is another output","Yet another output","Is this even working"] # Just for demo
    while True:
        upperwin.clear()
        upperwin.addstr(f"{choice(outputs)}")
        try:
            inp = commandQueue.get(timeout=0.1)
            if inp == 'exit':
                return
            else:
                upperwin.addch('\n')
                upperwin.addstr(inp)
        except Empty:
            pass

        upperwin.refresh()
        sleep(1)
        


def inputThreadFunc():
    while True:
        global buffer

        lowerwin.addstr("->")

        command = lowerwin.getstr()

        if command:
            command = command.decode("utf-8")
            commandQueue.put(command)
            lowerwin.clear()

            lowerwin.refresh()
            if command == 'exit':
                return

            
        


# MAIN CODE
outputThread = Thread(target=outputThreadFunc)
inputThread = Thread(target=inputThreadFunc)
outputThread.start()
inputThread.start()
outputThread.join()
inputThread.join()

stdscr.keypad(False)
curses.endwin()
print("Exit")


Old Solution

I've edited your example to use getch insted of input

#!/usr/bin/python3

import curses
import datetime

from math import acos
from threading import Thread
from random import choice
from time import sleep
from queue import Queue, Empty

INFO_REFRESH_SECONDS = 1

commandQueue = Queue()
buffer = list()  # stores your input buffer
stdscr = curses.initscr()
stdscr.keypad(True)

def outputThreadFunc():
    outputs = ["So this is another output","Yet another output","Is this even working"] # Just for demo
    info = choice(outputs), datetime.datetime.now()
    while True:

        if datetime.datetime.now() - info[1] > datetime.timedelta(seconds=INFO_REFRESH_SECONDS):
            # refresh info after certain period of time

            info = choice(outputs), datetime.datetime.now()  # timestamp which info was updated

        inp = ''
        buffer_text = ''.join(buffer)
        try:
            command = commandQueue.get(timeout=0.1)
            if command == 'exit':
                return
            inp = f"\n{command}"
        except Empty:
            pass 
        output_string = f"{info[0]}{inp}\n->{buffer_text}"
        stdscr.clear()
        stdscr.addstr(output_string)
        stdscr.refresh()
        if inp:
            # to make sure you see the command
            sleep(1)
        


def inputThreadFunc():
    while True:
        global buffer

        # get one character at a time
        key = stdscr.getch()
        curses.echo()

        if chr(key) == '\n':
            command = ''.join(buffer)
            commandQueue.put(command)
            if command == 'exit':
                return
            buffer = []
        elif key == curses.KEY_BACKSPACE:
            
            if buffer:
                buffer.pop()
        else:
            buffer.append(chr(key))

            
        


# MAIN CODE
outputThread = Thread(target=outputThreadFunc)
inputThread = Thread(target=inputThreadFunc)
outputThread.start()
inputThread.start()
outputThread.join()
inputThread.join()

stdscr.keypad(False)
curses.endwin()
print("Exit")

Sign up to request clarification or add additional context in comments.

3 Comments

How do I modify this so that the random lines I chose do not get erased after each run of the loop, and the input string in its incomplete form do not get mixed up with it?
@CaptainWoof I've updated the answer. There is timestamp along with info and variable INFO_REFRESH_SECONDS. Alternatively you can start another thread to update info value just like i updated buffer in inputThread and displayed in outputThread
@CaptainWoof Update Used subwin one win for output and one for input.
1

The simplest solution is to use two scripts; One, a server that prints the output, and the other, a client that sends the user's input to the server. Then you can use a standard solution like tmux to open the two scripts in two panes.

Comments

0

If you would like to avoid using libraries, ANSI escape codes can solve your problem. By printing print(f'\001[{n}A'), you move your cursor up a line, and by printing print(f'\u001b[{n}B'), you move your cursor down (with a predefined variable n for how far it should move). In your case, this can be extended to:

print('\n'*30)
for x in range(30):
    #get input
    in=input()
    #process input
    out = process(in)
    print('\u033[30A',out)
    print('u001[30B')

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.