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);
}
}