Search code examples
c++bashansi-escape

Bash PS1 prompt breaks line wrapping when generating ANSI escapes by an external program


I am looking at making a custom bash prompt using a C++ script. The plan is to call the script from within the PS1 declaration in .bash_profile like this:

PS1='$(bashprompt.out)'

This part works totally fine. The problem that I am running into is that the ANSI sequences are being printed to the terminal. You can see this in the following image, where it prematurely returns to the beginning of the line.

Test returning to the start of line

I am defining the colors in the format as follows:

#define DGRAYORANGE "\033[48;5;202;38;5;243m"

In a bash script, I know the way to avoid this issue is to denote the color as non-printing by using \[ \] (like explained here https://www.funtoo.org/Prompt_Magic), but that does not work in C++.

This is what the prompt should look like: This is an example prompt i made using bash and not C++.

Here is the C++ code that is being used to make the colored prompt:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pwd.h>
#include <string>
#include <iostream>

#define ARROW ""
#define DGRAYORANGE "\033[48;5;202;38;5;243m"
#define ORANGEDGRAY "\033[38;5;202;48;5;243m"
#define DGRAYDEFAULT "\033[38;5;243;48;5;0m"
#define WHITEDGRAY "\033[38;5;255;48;5;243m"
#define WHITEORANGE "\033[38;5;255;48;5;202m"
#define RESET "\033[0m"


using namespace std;

void PrintPrompt(string, string, string);
string FormatCWD(char *);



int main() {
    int buffersize = 256;
    char hostname[buffersize];
    char cwd[buffersize];
    struct passwd* pw;

    gethostname(hostname,sizeof(hostname));
    pw = getpwuid(getuid());
    getcwd(cwd,sizeof(cwd));

    string stringCwd = FormatCWD(cwd);
    string stringUsername = pw->pw_name;
    string stringHostname = hostname;

    PrintPrompt(stringCwd,stringHostname,stringUsername);
}

void PrintPrompt(string cwd, string hostname, string username){
    string cwdString = cwd;
    cout << WHITEDGRAY << username <<
            DGRAYORANGE << ARROW << " " <<
            WHITEORANGE << hostname <<
            ORANGEDGRAY << ARROW << " " <<
            WHITEDGRAY << cwd << DGRAYDEFAULT <<
            ARROW << RESET << " " << endl;
}

string FormatCWD(char * cwd) {
    string stringCwd = cwd;

    int size = stringCwd.length();
    int slashCount = 0;
    int slashIndex = 0;
    for (int i=0; i<size; i++) {
        if (stringCwd.at(i) == '/') {
            slashIndex = i;
            slashCount++;
        }
    }

    string outputCwd = "";
    if (slashIndex != 0) {
        for (int i=slashIndex+1; i<size; i++) {
            outputCwd += stringCwd.at(i);
        }
    }

    return outputCwd;
}

Is there any way to fix this issue by marking the escape sequences as non-printing (like in bash)? Thank you for any help you guys give.


Solution

  • Note first that \[ and \] are interpreted by Bash itself, not the console, so they must be explicitly present in the PS1 value, not in the output of the program generating actual prompt.

    Now, you want the non-escape chars to count, while colors are not to be counted. For this, you can modify your program to have two modes:

    • Colored output (for actual prompt)
    • Text-only output (for character counting)

    Then you can use something like

    PS1='$(/tmp/test)\[\r$(/tmp/test c)\]'
    

    which would work for code modified to the following:

    void PrintPrompt(string cwd, string hostname, string username, bool colors){
        string cwdString = cwd;
        cout << (colors ? WHITEDGRAY  : "") << username <<
                (colors ? DGRAYORANGE : "") << ARROW << " " <<
                (colors ? WHITEORANGE : "") << hostname <<
                (colors ? ORANGEDGRAY : "") << ARROW << " " <<
                (colors ? WHITEDGRAY  : "") << cwd << (colors ? DGRAYDEFAULT : "") <<
                ARROW << (colors ? RESET : "") << endl;
    }
    

    with colors being passed as e.g. argc>1.

    We use the \r escape sequence to go to the beginning of the line, enabling us to overwrite our already printed non-colored text with the fancy-colored one, making Bash think that the additional text and escape sequences (including the carriage return) don't take any space.

    Screenshot:

    screenshot of resulting output