Skip to content

File Linux/ClientPlatformSpecific.cpp

File List > api > cppSDK > SDKClient > PlatformSpecific > Linux > ClientPlatformSpecific.cpp

Go to the documentation of this file

#include "ClientPlatformSpecific.hpp"

// signal types
#include <csignal>
// std::filesystem
#include <filesystem>
// std::*fstream
#include <fstream>
// CoreSdk_ShutDown
#include "ManusSDK.h"
// std::map
#include <map>
// Ncurses terminal functions.
#include <ncursesw/ncurses.h>
// Termios terminal functions.
#include <termios.h>
// spdlog
#include "spdlog/spdlog.h"

#define CALL_DEFAULT_SIGNAL_HANDLER(p_SignalType) \
    /* Reset the handler for this signal to the default. */ \
    signal(p_SignalType, SIG_DFL); \
    /* Re-raise this signal, causing the normal handler to run. */ \
    raise(p_SignalType);

const std::string SDKClientPlatformSpecific::s_SlashForFilesystemPath = "/";

static void HandleTerminationSignal(int p_Parameter)
{
    spdlog::error(
        "Termination signal sent with parameter {}.",
        p_Parameter);

    CoreSdk_ShutDown();

    CALL_DEFAULT_SIGNAL_HANDLER(SIGTERM);
}

static void HandleInterruptSignal(int p_Parameter)
{
    spdlog::error(
        "Interrupt signal sent with parameter {}.",
        p_Parameter);

    CoreSdk_ShutDown();

    CALL_DEFAULT_SIGNAL_HANDLER(SIGINT);
}

static void HandleQuitSignal(int p_Parameter)
{
    spdlog::error(
        "Quit signal sent with parameter {}.",
        p_Parameter);

    CoreSdk_ShutDown();

    CALL_DEFAULT_SIGNAL_HANDLER(SIGQUIT);
}

static void HandleHangupSignal(int p_Parameter)
{
    spdlog::error(
        "Hang-up signal sent with parameter {}.",
        p_Parameter);

    CoreSdk_ShutDown();

    CALL_DEFAULT_SIGNAL_HANDLER(SIGHUP);
}

static bool InitializeNcurses(void)
{
    const WINDOW* const t_Window = initscr();
    if (!t_Window)
    {
        spdlog::error("Failed to initialise the screen.");

        return false;
    }

    // Don't buffer input until a newline or carriage return is typed.
    if (cbreak() != OK)
    {
        spdlog::error("Failed to make input unbuffered.");

        return false;
    }

    // Don't echo input.
    if (noecho() != OK)
    {
        spdlog::error("Failed to disable input echoing.");

        return false;
    }

    // Don't make newlines when the return key is pressed.
    if (nonl() != OK)
    {
        spdlog::error("Failed to disable newlines.");

        return false;
    }

    // Do not flush the screen when interrupt/break/quit is pressed.
    if (intrflush(stdscr, FALSE) != OK)
    {
        spdlog::error("Failed to disable screen flushing.");

        return false;
    }

    // Make getch non-blocking.
    if (nodelay(stdscr, TRUE) != OK)
    {
        spdlog::error("Failed to make nodelay non-blocking.");

        return false;
    }

    // Enable handling the keypad ("function keys" like the arrow keys).
    if (keypad(stdscr, TRUE) != OK)
    {
        spdlog::error("Failed to enable keypad input.");

        return false;
    }

    return true;
}

static bool InitializeTermios(void)
{
    // For some reason, printf and spdlog strings require a carriage return
    // in addition to a newline after running initscr().
    // This code sets the "output modes" flag to treat newline characters
    // as newline+carriage-return characters.
    // https://arstechnica.com/civis/viewtopic.php?t=70699
    termios t_Settings;
    if (tcgetattr(STDIN_FILENO, &t_Settings) != 0)
    {
        spdlog::error("Failed to get Termios settings.");

        return false;
    }

    t_Settings.c_oflag |= ONLCR;
    if (tcsetattr(0, TCSANOW, &t_Settings) != 0)
    {
        spdlog::error("Failed to set Termios settings.");

        return false;
    }

    return true;
}

static bool SetUpSignalHandlers(void)
{
    {
        const __sighandler_t t_OldTerminationHandler = signal(
            SIGTERM,
            HandleTerminationSignal);
        if (t_OldTerminationHandler == SIG_ERR)
        {
            spdlog::error("Failed to set termination signal handler.");
            return false;
        }
    }

    {
        const __sighandler_t t_OldInterruptHandler = signal(
            SIGINT,
            HandleInterruptSignal);
        if (t_OldInterruptHandler == SIG_ERR)
        {
            spdlog::error("Failed to set interrupt signal handler.");
            return false;
        }
    }

    {
        const __sighandler_t t_OldQuitHandler = signal(
            SIGQUIT,
            HandleQuitSignal);
        if (t_OldQuitHandler == SIG_ERR)
        {
            spdlog::error("Failed to set quit signal handler.");
            return false;
        }
    }

    {
        const __sighandler_t t_OldHangupHandler = signal(
            SIGHUP,
            HandleHangupSignal);
        if (t_OldHangupHandler == SIG_ERR)
        {
            spdlog::error("Failed to set hang-up signal handler.");
            return false;
        }
    }

    return true;
}

class ClientInput
{
public:
    void Update(void)
    {
        // Reset the state of the last update.
        for (
            InputMap_t::iterator t_Key = m_PressedLastUpdate.begin();
            t_Key != m_PressedLastUpdate.end();
            t_Key++)
        {
            t_Key->second = false;
        }

        // Copy the current state to the last update's state, and clear the
        // current state.
        for (
            InputMap_t::iterator t_Key = m_CurrentlyPressed.begin();
            t_Key != m_CurrentlyPressed.end();
            t_Key++)
        {
            m_PressedLastUpdate[t_Key->first] =
                t_Key->second;
            t_Key->second = false;
        }

        // Get the new state.
        int t_Ch = getch();
        while (t_Ch != ERR)
        {
            if (t_Ch >= 'a' && t_Ch <= 'z')
            {
                // Unlike with Windows' GetAsyncKeyState(), upper case and
                // lower case characters have different key numbers with
                // getch().
                // Since all WasKeyPressed calls (as of writing this) use
                // upper case, lower case keys need to be converted to work
                // on Linux.
                // Note that this does break the ability to check for lower
                // case key presses.
                t_Ch = toupper(t_Ch);
            }

            m_CurrentlyPressed[t_Ch] = true;

            t_Ch = getch();
        }
    }

    bool GetKey(const int p_Key)
    {
        bool t_IsPressed = IsPressed(p_Key);
        m_PreviousKeyState[p_Key] = t_IsPressed;

        return t_IsPressed;
    }

    bool GetKeyDown(const int p_Key)
    {
        const bool t_IsPressed = IsPressed(p_Key);

        const auto t_PreviousState = m_PreviousKeyState.find(p_Key);
        const bool t_PreviousValue =
            t_PreviousState == m_PreviousKeyState.end()
                ? false
                : t_PreviousState->second;

        const bool t_Down = t_IsPressed && !t_PreviousValue;
        m_PreviousKeyState[p_Key] = t_Down;

        return t_Down;
    }

    bool GetKeyUp(const int p_Key)
    {
        const bool t_IsPressed = IsPressed(p_Key);

        const auto t_PreviousState = m_PreviousKeyState.find(p_Key);
        const bool t_PreviousValue =
            t_PreviousState == m_PreviousKeyState.end()
                ? false
                : t_PreviousState->second;

        const bool t_Up = !t_IsPressed && t_PreviousValue;
        m_PreviousKeyState[p_Key] = t_Up;

        return t_Up;
    }

private:
    bool IsPressed(const int p_Key) const
    {
        auto t_CurrentlyPressed = m_CurrentlyPressed.find(p_Key);
        if (t_CurrentlyPressed == m_CurrentlyPressed.end())
        {
            return false;
        }

        return t_CurrentlyPressed->second;
    }

    bool WasJustPressed(const int p_Key) const
    {
        return !WasPressedLastUpdate(p_Key) && IsPressed(p_Key);
    }

    bool WasJustReleased(const int p_Key) const
    {
        return WasPressedLastUpdate(p_Key) && !IsPressed(p_Key);
    }

    bool WasPressedLastUpdate(const int p_Key) const
    {
        auto t_StateLastUpdate = m_PressedLastUpdate.find(p_Key);
        if (t_StateLastUpdate == m_PressedLastUpdate.end())
        {
            return false;
        }

        return t_StateLastUpdate->second;
    }

    typedef std::map<int, bool> InputMap_t;
    InputMap_t m_CurrentlyPressed;
    InputMap_t m_PressedLastUpdate;
    // The Windows GetKey* functions only update the key state when a GetKey*
    // function gets called. To make the Linux input work the same way, this
    // map is used.
    InputMap_t m_PreviousKeyState;
};

static ClientInput g_Input;

bool SDKClientPlatformSpecific::PlatformSpecificInitialization(void)
{
    const bool t_NcursesResult = InitializeNcurses();
    const bool t_TermiosResult = InitializeTermios();
    const bool t_SignalResult = SetUpSignalHandlers();

    return t_NcursesResult && t_TermiosResult && t_SignalResult;
}

bool SDKClientPlatformSpecific::PlatformSpecificShutdown(void)
{
    // Ncurses.
    endwin();

    return true;
}

void SDKClientPlatformSpecific::UpdateInput(void)
{
    g_Input.Update();
}

/*static*/ bool SDKClientPlatformSpecific::CopyString(
    char* const p_Target,
    const size_t p_MaxLengthThatWillFitInTarget,
    const std::string& p_Source)
{
    if (!p_Target)
    {
        SPDLOG_ERROR(
            "Tried to copy a string, but the target was null. The string was \"{}\".",
            p_Source.c_str());

        return false;
    }

    if (p_MaxLengthThatWillFitInTarget == 0)
    {
        SPDLOG_ERROR(
            "Tried to copy a string, but the target's size is zero. The string was \"{}\".",
            p_Source.c_str());

        return false;
    }

    if (p_MaxLengthThatWillFitInTarget <= p_Source.length())
    {
        SPDLOG_ERROR(
            "Tried to copy a string that was longer than {} characters, which makes it too big for its target buffer. The string was \"{}\".",
            p_MaxLengthThatWillFitInTarget,
            p_Source.c_str());

        return false;
    }

    strcpy(p_Target, p_Source.c_str());

    return true;
}

bool SDKClientPlatformSpecific::ResizeWindow(
    const short int p_ConsoleWidth,
    const short int p_ConsoleHeight,
    const short int p_ConsoleScrollback)
{
    // https://apple.stackexchange.com/questions/33736/can-a-terminal-window-be-resized-with-a-terminal-command/47841#47841
    // Use a control sequence to resize the window.
    // Seems to be supported by the default terminal used in Gnome, as well
    // as Mac OS.
    // \\e[ -> ASCII ESC character (number 27, or 0x1B)
    //      -> control sequence introducer
    // 8;   -> resize the window
    // y;xt -> The first number is the height, the second the width.
    // Scrollback can't and doesn't need to be set here for Linux.
    printf("\e[8;%d;%dt", p_ConsoleHeight, p_ConsoleWidth);

    ClearConsole();

    // None of the ncurses functions for resizing the terminal actually
    // seem to do anything.
    /*if (resizeterm(180, 180) != OK)
    {
        return false;
    }*/

    return true;
}

void SDKClientPlatformSpecific::ApplyConsolePosition(
    const int p_ConsoleCurrentOffset)
{
    printf("\e[%d;1H", p_ConsoleCurrentOffset);
}

/*static*/ void SDKClientPlatformSpecific::ClearConsole(void)
{
    // https://stackoverflow.com/questions/4062045/clearing-terminal-in-linux-with-c-code
    // Use a control sequence to clear the terminal and move the cursor.
    // Seems to be supported by the default terminal used in Gnome,
    // as well as Mac OS.
    // \\e[ -> ASCII ESC character (number 27, or 0x1B) -> control sequence introducer
    // 2    -> the entire screen
    // J    -> clear the screen
    //printf("\e[2J\n");

    // Move the cursor to row 1 column 1.
    //printf("\e[1;1H\n");

    if (clear() != OK)
    {
        spdlog::error("Failed to clear the screen.");
    }

    refresh();
}

bool SDKClientPlatformSpecific::GetKey(const int p_Key)
{
    return g_Input.GetKey(p_Key);
}

bool SDKClientPlatformSpecific::GetKeyDown(const int p_Key)
{
    return g_Input.GetKeyDown(p_Key);
}

bool SDKClientPlatformSpecific::GetKeyUp(const int p_Key)
{
    return g_Input.GetKeyUp(p_Key);
}

std::string SDKClientPlatformSpecific::GetDocumentsDirectoryPath_UTF8(void)
{
    const char* const t_Xdg = getenv("XDG_DOCUMENTS_DIR");

    // Backup - the documents folder is usually going to be in $HOME/Documents.
    const char* const t_Home = getenv("HOME");
    if (!t_Xdg && !t_Home)
    {
        return std::string("");
    }

    const std::string t_DocumentsDir =
        (!t_Xdg || strlen(t_Xdg) == 0)
            ? std::string(t_Home) + std::string("/Documents")
            : std::string(t_Xdg);

    return t_DocumentsDir;
}

std::ifstream SDKClientPlatformSpecific::GetInputFileStream(
    std::string p_Path_UTF8)
{
    return std::ifstream(p_Path_UTF8, std::ifstream::binary);
}

std::ofstream SDKClientPlatformSpecific::GetOutputFileStream(
    std::string p_Path_UTF8)
{
    return std::ofstream(p_Path_UTF8, std::ofstream::binary);
}

bool SDKClientPlatformSpecific::DoesFolderOrFileExist(std::string p_Path_UTF8)
{
    return std::filesystem::exists(p_Path_UTF8);
}

void SDKClientPlatformSpecific::CreateFolderIfItDoesNotExist(
    std::string p_Path_UTF8)
{
    if (!DoesFolderOrFileExist(p_Path_UTF8))
    {
        std::filesystem::create_directory(p_Path_UTF8);
    }
}