터미널 커서의 위치 구하기

SoByungJun·2023년 2월 10일
0

Linux/C++

목록 보기
2/2
post-thumbnail

이전 게시글 ( C++ 코드로 터미널 크기 구하기 ) 의 코드를 일부 재사용합니다.

#include <stdio.h>
#include <iostream>
#include <sstream>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

using namespace std;

/* ------------------------------------- DEFINE -------------------------------------- */
constexpr int CMD_MAX_LEN = 256;

// 좌표 저장 구조체 및 비교 연산자 정의
typedef struct _tCoord_ {
    int xPos;
    int yPos;

    bool operator == ( const _tCoord_& A ) {
        return (xPos == A.xPos) && (yPos == A.yPos);
    }

    bool operator != ( const _tCoord_& A ) {
        return (xPos != A.xPos) || (yPos != A.yPos);
    }

} COORD, *PCOORD;

// getCoord() 오류값 지정
constexpr COORD ERROR_GET_COORD = { -1, -1 };

// setCoord() 좌표값 이동 시 절대좌표 이동 or 상대좌표 이동 타입 체크
enum class MOVE_TYPE {
    ABSOLUTE_MOVEMENT,
    RELATIVE_MOVEMENT,
};
/* ------------------------------------- DEFINE -------------------------------------- */

/* --------------------------------- Console Control --------------------------------- */
string getStringResultFromCommand( const char* cmd ) noexcept
{
    string result;
    char   buffer[CMD_MAX_LEN] = {'\0',};

    // popen : 주어진 cmd 를 shell 로 실행하고 파이프 fd 반환
    FILE* stream = popen( cmd, "r" );
    if( stream ) {
        // popen 및 shell cmd 성공적으로 수행했다면, 결과 출력값을 string 으로 반환
        while( fgets( buffer, CMD_MAX_LEN, stream ) != NULL ) result.append( buffer );
        pclose( stream );
    }
    return result;
}

// if Terminal Size 50(Lines) x 100(Columns) -> 'stty size' return : 50 100
inline void getConsoleSize( int& width, int& height )
{
    // getStringResultFromCommand 이용해서 터미널 사이즈 받은 다음 토큰화
    istringstream iss( getStringResultFromCommand( "stty size" ) );
    string        tokenString;

    getline( iss, tokenString, ' ' );
    height = stoi( tokenString );

    getline( iss, tokenString, ' ' );
    width = stoi( tokenString );
}

int getConsoleWidth()
{
    try         { return stoi( getStringResultFromCommand( "stty size | awk '{print $2}'" ) ); }
    catch (...) { return 0; }
}

int getConsoleHeight()
{
    try         { return stoi( getStringResultFromCommand( "stty size | awk '{print $1}'" ) ); }
    catch (...) { return 0; }
}
/* --------------------------------- Console Control --------------------------------- */

/* --------------------- Static Function for get terminal coord ---------------------- */
constexpr int RD_EOF = -1;
constexpr int RD_EIO = -2;
static inline int rd( const int fd ) 
{
    unsigned char buffer[4];
    ssize_t       n;

    while( true ) {
        n = read( fd, buffer, 1 );

        if( n  > (ssize_t) 0 ) 
            return buffer[0];
        else 
        if( n == (ssize_t) 0 ) 
            return RD_EOF;
        else 
        if( n != (ssize_t)-1 ) 
            return RD_EIO;
        else 
        if( errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK ) 
            return RD_EIO;
    }
}

static inline int wr(const int fd, const char* const data, const size_t bytes )
{
    const char*       head = data;
    const char* const tail = data + bytes;
    ssize_t           n;

    while( head < tail ) {
        n = write( fd, head, (size_t)( tail - head ) );

        if( n  > (ssize_t) 0 )
            head += n;
        else
        if( n != (ssize_t)-1 )
            return EIO;
        else
        if( errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK )
            return errno;
    }
    return 0;
}

/* Return a new file descriptor to the current tty */
int current_tty(void)
{
    const char* dev = ttyname(STDIN_FILENO);

    if( !dev )  dev = ttyname(STDOUT_FILENO);
    if( !dev )  dev = ttyname(STDERR_FILENO);

    if( !dev ) {
        errno = ENOTTY;
        return -1;
    }

    int fd;
    do {
        fd = open( dev, O_RDWR | O_NOCTTY );
    } while( fd == -1 && errno == EINTR );

    return fd;
}
/* --------------------- Static Function for get terminal coord ---------------------- */

COORD getCoord()
{
    struct termios  save_attr, new_attr;
    int             result, rows, cols;
    COORD           co = ERROR_GET_COORD;

    // fflush last buffer
    fflush(stdout);

    int tty = current_tty();
    if( tty == -1 ) 
        return ERROR_GET_COORD;

    // Save current terminal setting
    do {
        result = tcgetattr( tty, &save_attr );
    } while( result == -1 && errno == EINTR );

    if( result == -1 ) 
        return ERROR_GET_COORD;

    // Get current terminal settings for basis, too
    do {
        result = tcgetattr( tty, &new_attr );
    } while( result == -1 && errno == EINTR );

    if( result == -1 ) 
        return ERROR_GET_COORD;

    // Disable ICANON, ECHO, CREAD
    new_attr.c_lflag &= ~ICANON;
    new_attr.c_lflag &= ~ECHO;
    new_attr.c_lflag &= ~CREAD;

    do {
        // Set modified setting
        do {
            result = tcsetattr( tty, TCSANOW, &new_attr );
        } while( result == -1 && errno == EINTR );

        if( result == -1 ) {
            break;
        }

        // Request cursor coord from the teminal
        result = wr( tty, "\033[6n", 4 );
        if( result ) {
            break;
        }

        // Expect an ESC
        result = rd(tty);
        if( result != 27 ) break;

        // Expect [ after ESC
        result = rd(tty);
        if( result != '[' ) break;

        // Parse rows(yPos)
        rows   = 0;
        result = rd(tty);
        while( result >= '0' && result <= '9' ) {
            rows   = 10 * rows + result - '0';
            result = rd(tty);
        }

        if( result != ';' ) break;

        // Parse cols(xPos)
        cols   = 0;
        result = rd(tty);
        while( result >= '0' && result <= '9' ) {
            cols   = 10 * cols + result - '0';
            result = rd(tty);
        }

        if( result != 'R' ) break;

        // Success
        co.xPos = cols - 1;
        co.yPos = rows - 1;
    } while( 0 );

    // Restore saved terminal setting
    do {
        result = tcsetattr( tty, TCSANOW, &save_attr );
    } while( result == -1 && errno == EINTR );

    return co;
}

/*
    ABSOLUTE_MOVEMENT : 
        최상단좌측을 (0,0) 이라 했을 때, 길이 방향 'x', 높이 방향 'y' 아래로 절대좌표 이동

    RELATIVE_MOVEMENT : 
        현 위치에서 길이 방향 'x', 높이 방향 'y'만큼 상대좌표 이동
*/
inline void setCoord( const COORD& coord, MOVE_TYPE type = MOVE_TYPE::ABSOLUTE_MOVEMENT )
{
    int maxConsoleX = getConsoleWidth()  - 1;
    int maxConsoleY = getConsoleHeight() - 1;

    switch (type)
    {
    case MOVE_TYPE::ABSOLUTE_MOVEMENT:
        {
            // setCoord : zero-start / getConsoleSize : 1 start
            // getConsoleHeight() or getConsoleWidth() 값으로 setCoord 할 때를 위한 값 조절
            COORD curPos = {
                .xPos = ( coord.xPos == getConsoleWidth()  ) ? maxConsoleX : coord.xPos,
                .yPos = ( coord.yPos == getConsoleHeight() ) ? maxConsoleY : coord.yPos,
            };

            if( curPos.xPos >= 0 && curPos.xPos <= maxConsoleX &&
                curPos.yPos >= 0 && curPos.yPos <= maxConsoleY ){
                printf("\033[%d;%df", curPos.yPos+1, curPos.xPos+1);
            }
        }
        break;
    
    case MOVE_TYPE::RELATIVE_MOVEMENT:
        {
            COORD curPos = getCoord();
            if( curPos != ERROR_GET_COORD ){
                curPos.xPos += coord.xPos;
                curPos.yPos += coord.yPos;

                if( curPos.xPos >= 0 && curPos.xPos <= maxConsoleX &&
                    curPos.yPos >= 0 && curPos.yPos <= maxConsoleY ){
                    printf("\033[%d;%df", curPos.yPos+1, curPos.xPos+1);
                }
            }
        }
        break;
    }
}

inline void setCoord( const int xPos, const int yPos, MOVE_TYPE type = MOVE_TYPE::ABSOLUTE_MOVEMENT )
{
    setCoord( {xPos, yPos}, type );
}

int main()
{
    COORD startPos = { 4, 4 };
    COORD curPos;

    setCoord( startPos );   // startPos 위치로 이동
    printf("Move Cursor : (%d, %d)", startPos.xPos, startPos.yPos );

    startPos = { 6, 6 };
    setCoord( startPos );   // startPos 위치로 이동
    curPos = getCoord();    // 이동한 위치 읽어옴

    printf("[ %d, %d ] setCoord (%d, %d) (absolute)", curPos.xPos, curPos.yPos, startPos.xPos, startPos.yPos);
    /* [ 4, 4 ] setCoord (4, 4) (absolute) */

    // 출력이 종료된 지점을 기준으로 상대 좌표 이동도 가능
    setCoord( {0, 1}, MOVE_TYPE::RELATIVE_MOVEMENT ); // 이전 출력 후 마지막 커서 지점에서 x 방향으로는 그대로, y축 방향으로는 1칸 이동
    curPos = getCoord();    // 이동한 위치 읽어옴
    printf("[ %d, %d ] setCoord (0, 1) (relative)", curPos.xPos, curPos.yPos);

    // 터미널의 오른쪽 가장자리로 이동해서, 높이만큼 글자 입력하기
    for( size_t i = 0; i < getConsoleHeight(); ++i ) {
        if( i == 0 ) setCoord( { getConsoleWidth(), 0 }, MOVE_TYPE::ABSOLUTE_MOVEMENT );    // 오른쪽 맨 위 모서리로 이동
        else         setCoord( { 0, 1 }, MOVE_TYPE::RELATIVE_MOVEMENT );                    // 아래 칸 이동

        printf("%c", 'A'+(int)i);
    }

    setCoord({0, getConsoleHeight()});
    return 0;
}

profile
좋아서 하는 일

0개의 댓글