3

I'm creating a command-line calculator tool, and I'd like to get it to format the user's input as they type, similar to what Fish and Powershell do (screenshots).

Currently, the only easy method for doing this that I can think of is getting characters one at a time, and then reprinting the formatted line to the screen.

# This code is probably very dodgy, 
# it's just to give you an idea what I'm thinking
# Use external getch module to get input without echoing to screen
from getch import getch 
inp = ""
while True:
    char = getch()
    if char == '\n': break
    if char == '\b': inp = inp[:-1]
    else: inp += char
    # Print with carriage return so that next input overwrites it
    print(colourFormatInput(inp) + '\r') 
# Outside loop: process input and devliver results

Whilst this would techincally work, I feel like it is a ton of manual effort, and would only become more complex if I wanted to add functionality such as using arrow keys to move the cursor's position.

Is there a simple way to get this kind of functionality without having to code all of it up myself?

Miguel Guthridge
  • 1,444
  • 10
  • 27
  • Does this answer your question? [How to print colored text to the terminal?](https://stackoverflow.com/questions/287871/how-to-print-colored-text-to-the-terminal) – Klaus D. Jul 04 '21 at 04:28
  • Have you tried `rich`? It might be somerhing for you https://github.com/willmcgugan/rich – Prayson W. Daniel Jul 04 '21 at 04:29
  • Sadly it looks like both of those are for printing output, rather than formatting input, so although they could help with formatting, they wouldn't do much for solving the issues I'm having with formatting user input as they type. – Miguel Guthridge Jul 04 '21 at 04:31

3 Answers3

2

I use the library colorama.

import colorama
import sys


def hook(tp, *args):
    if tp is KeyboardInterrupt:
        print(colorama.Fore.RESET)
        exit()


def colored_input(text: str, color):
    sys.excepthook = hook
    inp = input(text + color)
    print(colorama.Fore.RESET, end="", flush=True)
    sys.excepthook = sys.__excepthook__
    return inp


name = colored_input("What's your name? ", colorama.Fore.RED)
age = colored_input("What's your age? ", colorama.Fore.YELLOW)

print(f"Nice to meet you {name}({age})")

I use sys.excepthook to catch the KeyboardInterrupt so I can reset the color back when the user types CTRL+C, and then I set the original excepthook back (sys.__excepthook__)

enter image description here

Jonathan1609
  • 1,809
  • 1
  • 5
  • 22
1

You can do this with prompt_toolkit

here is the documentation if you need:

You can add Syntax Highlighting to input by the following as given in examples:

Adding syntax highlighting is as simple as adding a lexer. All of the Pygments lexers can be used after wrapping them in a PygmentsLexer. It is also possible to create a custom lexer by implementing the Lexer abstract base class.

from pygments.lexers.html import HtmlLexer
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.lexers import PygmentsLexer

text = prompt('Enter HTML: ', lexer=PygmentsLexer(HtmlLexer))
print('You said: %s' % text)

Output of the above

In the same way as above you can create custom prompt_toolkit.lexers.Lexer for calculator highlighting just like the following example. Here I create a custom helper class:

from typing import Callable
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.shortcuts import prompt
import prompt_toolkit.lexers
import re

class CustomRegexLexer(prompt_toolkit.lexers.Lexer):
    def __init__(self, regex_mapping):
        super().__init__()
        self.regex_mapping = regex_mapping

    def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]:
        def lex(_: int):
            line = document.text
            tokens = []
            while len(line) != 0:
                for pattern, style_string in self.regex_mapping.items():
                    match: re.Match = pattern.search(line)

                    if not match:
                        continue
                    else:
                        # print(f"found_match: {match}")
                        pass
                    match_string = line[:match.span()[1]]
                    line = line[match.span()[1]:]
                    tokens.append((style_string, match_string))
                    break
            return tokens
        return lex

Now with the above helper class implemented we can create our regex patterns and their respective styles, to learn more about what you can have in styling string go to this page

# Making regex for different operators. Make sure you add `^` anchor
# to the start of all the patterns
operators_allowed = ["+", "-", "/", "*", "(", ")", "=", "^"]
operators = re.compile("^["+''.join([f"\\{x}" for x in operators_allowed])+"]")
numbers = re.compile(r"^\d+(\.\d+)?")
text = re.compile(r"^.")


regex_mapping = {
    operators: "#ff70e5",  # Change colors according to your requirement
    numbers: "#ffa500",
    text: "#2ef5ff",
}

MyCalculatorLexer = CustomRegexLexer(regex_mapping)

With the lexers created you can now use the lexer in the function prompt:

text = prompt("Enter Equation: ", lexer=MyCalculatorLexer)

# Or

def input_maths(message):
    return prompt(message, lexer=MyCalculatorLexer)

text = input_maths("Enter Equation: ")

Here is some example output:

Example Output

And now everything works. Also do check out prompt_toolkit, you can create tons of custom things as shown in there gallery

AmaanK
  • 1,032
  • 5
  • 25
  • I have corrected a bug, please check if it works, unfortunately before correction it crashed – AmaanK Jul 04 '21 at 04:57
  • Getting an error `type object 'CalculatorLexer' has no attribute 'lex_document'`. – Miguel Guthridge Jul 04 '21 at 05:06
  • please wait until my computer restarts. i unfortunately crashed it twice now – AmaanK Jul 04 '21 at 05:08
  • I will correct the errors as soon as I can, I am myself facing problems now: https://stackoverflow.com/questions/68242016/custom-pygments-lexer-with-prompt-toolkit-raises-error – AmaanK Jul 04 '21 at 05:39
  • It looks like this could be an issue with the Regex lexer its self, since running it using `python -m pygments -x -l lexer.py:CalculatorLexer temp.txt` where temp.txt is some random maths input causes it to freeze up. – Miguel Guthridge Jul 04 '21 at 05:53
  • Please try again for above, I have tried to correct it by adding newline regex if it does not work you may just mark it as unsolved and go on for `prompt-toolkit` documentation :( – AmaanK Jul 04 '21 at 05:55
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/234490/discussion-between-xcodz-dot-and-hdsq). – AmaanK Jul 04 '21 at 06:06
-1

Another example:


CGREEN = '\33[32m'
CYELLOW = '\33[33m'
CBLUE = '\33[34m'
CVIOLET = '\33[35m'
CBEIGE = '\33[36m'
CWHITE = '\33[37m'
CGREY = '\33[90m'
CRED = '\033[91m'
CYELLOW = '\33[33m'
CYELLOW2 = '\33[93m'
CEND = '\033[0m'

print(CGREEN + "This is green text" + CEND)
print(CYELLOW + "This is yellow text" + CEND)

# Another interesting example (courtesy: https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html)

for i in range(0, 16):
    for j in range(0, 16):
        code = str(i * 16 + j)
        colorCode = u"\u001b[48;5;" + code + "m"
        print(colorCode + " Color {} ".format(code) + CEND)
Bhakta Raghavan
  • 664
  • 6
  • 16
  • This is just plain printing text in color but this answer does not show anything related to input as asked by OP and colored text for your code is not supported cross-platform you also havenot given anything of why this is correct answer. Like no one asked for a program that prints text in all the colors. Please re-read the question if you did not understand – AmaanK Jul 04 '21 at 10:58