mirror of https://gitlab.com/nakst/essence
332 lines
9.9 KiB
C++
332 lines
9.9 KiB
C++
// This file is part of the Essence operating system.
|
|
// It is released under the terms of the MIT license -- see LICENSE.md.
|
|
// Written by: nakst.
|
|
|
|
// TODO Don't use EsTextbox for the output..
|
|
// TODO Put the connection settings in a Panel.Popup.
|
|
|
|
#define ES_INSTANCE_TYPE Instance
|
|
#include <essence.h>
|
|
|
|
struct Instance : EsInstance {
|
|
EsTextbox *textboxNick;
|
|
EsTextbox *textboxAddress;
|
|
EsTextbox *textboxPort;
|
|
EsTextbox *textboxOutput;
|
|
EsTextbox *textboxInput;
|
|
EsButton *buttonConnect;
|
|
|
|
EsThreadInformation networkingThread;
|
|
|
|
EsMutex inputCommandMutex;
|
|
char *inputCommand;
|
|
size_t inputCommandBytes;
|
|
};
|
|
|
|
const EsStyle styleSmallTextbox = {
|
|
.inherit = ES_STYLE_TEXTBOX_BORDERED_SINGLE,
|
|
|
|
.metrics = {
|
|
.mask = ES_THEME_METRICS_PREFERRED_WIDTH,
|
|
.preferredWidth = 100,
|
|
},
|
|
};
|
|
|
|
const EsStyle styleOutputTextbox = {
|
|
.inherit = ES_STYLE_TEXTBOX_NO_BORDER,
|
|
|
|
.metrics = {
|
|
.mask = ES_THEME_METRICS_FONT_FAMILY,
|
|
.fontFamily = ES_FONT_MONOSPACED,
|
|
},
|
|
};
|
|
|
|
const EsStyle styleInputTextbox = {
|
|
.inherit = ES_STYLE_TEXTBOX_BORDERED_SINGLE,
|
|
|
|
.metrics = {
|
|
.mask = ES_THEME_METRICS_FONT_FAMILY,
|
|
.fontFamily = ES_FONT_MONOSPACED,
|
|
},
|
|
};
|
|
|
|
int TextboxInputCallback(EsElement *element, EsMessage *message) {
|
|
Instance *instance = element->instance;
|
|
|
|
if (message->type == ES_MSG_KEY_DOWN) {
|
|
if (message->keyboard.scancode == ES_SCANCODE_ENTER) {
|
|
size_t inputCommandBytes = 0;
|
|
char *inputCommand = EsTextboxGetContents(instance->textboxInput, &inputCommandBytes);
|
|
|
|
if (inputCommandBytes) {
|
|
EsMutexAcquire(&instance->inputCommandMutex);
|
|
EsHeapFree(instance->inputCommand);
|
|
instance->inputCommand = inputCommand;
|
|
instance->inputCommandBytes = inputCommandBytes;
|
|
EsMutexRelease(&instance->inputCommandMutex);
|
|
} else {
|
|
EsHeapFree(inputCommand);
|
|
}
|
|
|
|
EsTextboxClear(instance->textboxInput, false);
|
|
return ES_HANDLED;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void NetworkingThread(EsGeneric argument) {
|
|
Instance *instance = (Instance *) argument.p;
|
|
|
|
char errorMessage[4096];
|
|
size_t errorMessageBytes = 0;
|
|
|
|
char message[4096];
|
|
size_t messageBytes = 0;
|
|
|
|
char nick[64];
|
|
size_t nickBytes = 0;
|
|
|
|
char *password = nullptr;
|
|
size_t passwordBytes = 0;
|
|
|
|
char *address = nullptr;
|
|
size_t addressBytes = 0;
|
|
|
|
char buffer[1024];
|
|
uintptr_t bufferPosition = 0;
|
|
|
|
EsConnection connection = {};
|
|
connection.sendBufferBytes = 65536;
|
|
connection.receiveBufferBytes = 65536;
|
|
|
|
{
|
|
EsMessageMutexAcquire();
|
|
address = EsTextboxGetContents(instance->textboxAddress, &addressBytes);
|
|
EsMessageMutexRelease();
|
|
EsError error = EsAddressResolve(address, addressBytes, ES_FLAGS_DEFAULT, &connection.address);
|
|
|
|
if (error != ES_SUCCESS) {
|
|
errorMessageBytes = EsStringFormat(errorMessage, sizeof(errorMessage),
|
|
"The address name '%s' could not be found.\n", addressBytes, address);
|
|
goto exit;
|
|
}
|
|
}
|
|
|
|
{
|
|
EsMessageMutexAcquire();
|
|
size_t portBytes;
|
|
char *port = EsTextboxGetContents(instance->textboxPort, &portBytes);
|
|
EsMessageMutexRelease();
|
|
connection.address.port = EsIntegerParse(port, portBytes);
|
|
EsHeapFree(port);
|
|
}
|
|
|
|
{
|
|
EsMessageMutexAcquire();
|
|
char *_nick = EsTextboxGetContents(instance->textboxNick, &nickBytes);
|
|
EsMessageMutexRelease();
|
|
if (nickBytes > sizeof(nick)) nickBytes = sizeof(nick);
|
|
EsMemoryCopy(nick, _nick, nickBytes);
|
|
EsHeapFree(_nick);
|
|
|
|
for (uintptr_t i = 0; i < nickBytes; i++) {
|
|
if (nick[i] == ':') {
|
|
password = nick + i + 1;
|
|
passwordBytes = nickBytes - i - 1;
|
|
nickBytes = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
EsError error = EsConnectionOpen(&connection, ES_CONNECTION_OPEN_WAIT);
|
|
|
|
if (error != ES_SUCCESS) {
|
|
errorMessageBytes = EsStringFormat(errorMessage, sizeof(errorMessage),
|
|
"Could not open the connection (%d).", error);
|
|
goto exit;
|
|
}
|
|
|
|
messageBytes = EsStringFormat(message, sizeof(message), "%z%s%zNICK %s\r\nUSER %s localhost %s :%s\r\n",
|
|
password ? "PASS " : "", passwordBytes, password, password ? "\r\n" : "",
|
|
nickBytes, nick, nickBytes, nick, addressBytes, address, nickBytes, nick);
|
|
EsConnectionWriteSync(&connection, message, messageBytes);
|
|
}
|
|
|
|
while (true) {
|
|
// TODO Ping the server every 2 minutes.
|
|
// TODO If we've received no messages for 5 minutes, timeout.
|
|
|
|
uintptr_t inputBytes = 0;
|
|
|
|
while (true) {
|
|
char *inputCommand = nullptr;
|
|
size_t inputCommandBytes = 0;
|
|
|
|
EsMutexAcquire(&instance->inputCommandMutex);
|
|
inputCommand = instance->inputCommand;
|
|
inputCommandBytes = instance->inputCommandBytes;
|
|
instance->inputCommand = nullptr;
|
|
instance->inputCommandBytes = 0;
|
|
EsMutexRelease(&instance->inputCommandMutex);
|
|
|
|
if (inputCommand) {
|
|
messageBytes = EsStringFormat(message, sizeof(message), "%s\r\n", inputCommandBytes, inputCommand);
|
|
EsConnectionWriteSync(&connection, message, messageBytes);
|
|
EsHeapFree(inputCommand);
|
|
}
|
|
|
|
size_t bytesRead;
|
|
EsError error = EsConnectionRead(&connection, buffer + bufferPosition, sizeof(buffer) - bufferPosition, &bytesRead);
|
|
|
|
if (error != ES_SUCCESS) {
|
|
errorMessageBytes = EsStringFormat(errorMessage, sizeof(errorMessage), "The connection was lost (%d).", error);
|
|
goto exit;
|
|
}
|
|
|
|
bufferPosition += bytesRead;
|
|
|
|
if (bufferPosition >= 2) {
|
|
for (uintptr_t i = 0; i < bufferPosition - 1; i++) {
|
|
if (buffer[i] == '\r' && buffer[i + 1] == '\n') {
|
|
buffer[i] = 0;
|
|
inputBytes = i + 2;
|
|
goto gotMessage;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bufferPosition == sizeof(buffer)) {
|
|
errorMessageBytes = EsStringFormat(errorMessage, sizeof(errorMessage), "The server sent an invalid message.");
|
|
goto exit;
|
|
}
|
|
}
|
|
|
|
gotMessage:;
|
|
|
|
EsMessageMutexAcquire();
|
|
EsTextboxInsert(instance->textboxOutput, buffer);
|
|
EsTextboxInsert(instance->textboxOutput, "\n");
|
|
EsMessageMutexRelease();
|
|
|
|
{
|
|
EsPrint("=================\n%z\n", buffer);
|
|
|
|
const char *command = nullptr, *user = nullptr, *parameters = nullptr, *text = nullptr;
|
|
char *position = buffer;
|
|
|
|
if (*position == ':') {
|
|
user = ++position;
|
|
while (*position && *position != ' ') position++;
|
|
if (*position) *position++ = 0;
|
|
} else {
|
|
user = address;
|
|
}
|
|
|
|
while (*position && *position == ' ') position++;
|
|
command = position;
|
|
while (*position && *position != ' ') position++;
|
|
if (*position) *position++ = 0;
|
|
while (*position && *position == ' ') position++;
|
|
|
|
if (*position != ':') {
|
|
parameters = position;
|
|
while (*position && *position != ' ') position++;
|
|
if (*position) *position++ = 0;
|
|
}
|
|
|
|
while (*position && *position == ' ') position++;
|
|
if (*position == ':') text = position + 1;
|
|
|
|
if (0 == EsCRTstrcmp(command, "PING")) {
|
|
messageBytes = EsStringFormat(message, sizeof(message), "PONG :%z\r\n", text ?: parameters);
|
|
EsConnectionWriteSync(&connection, message, messageBytes);
|
|
}
|
|
|
|
EsPrint("command: '%z'\nuser: '%z'\nparameters: '%z'\ntext: '%z'\n\n", command, user, parameters, text);
|
|
}
|
|
|
|
EsMemoryMove(buffer + inputBytes, buffer + bufferPosition, -inputBytes, false);
|
|
bufferPosition -= inputBytes;
|
|
}
|
|
|
|
exit:;
|
|
|
|
if (connection.handle) {
|
|
EsConnectionClose(&connection);
|
|
}
|
|
|
|
EsMessageMutexAcquire();
|
|
|
|
if (errorMessageBytes) {
|
|
EsDialogShow(instance->window, EsLiteral("Connection failed"), errorMessage, errorMessageBytes,
|
|
ES_ICON_DIALOG_ERROR, ES_DIALOG_ALERT_OK_BUTTON);
|
|
}
|
|
|
|
EsElementSetDisabled(instance->textboxAddress, false);
|
|
EsElementSetDisabled(instance->textboxNick, false);
|
|
EsElementSetDisabled(instance->textboxPort, false);
|
|
EsElementSetDisabled(instance->textboxInput, true);
|
|
EsElementSetDisabled(instance->buttonConnect, false);
|
|
|
|
EsMessageMutexRelease();
|
|
|
|
EsHeapFree(address);
|
|
}
|
|
|
|
void ConnectCommand(Instance *instance, EsElement *, EsCommand *) {
|
|
EsElementSetDisabled(instance->textboxAddress, true);
|
|
EsElementSetDisabled(instance->textboxNick, true);
|
|
EsElementSetDisabled(instance->textboxPort, true);
|
|
EsElementSetDisabled(instance->textboxInput, false);
|
|
EsElementSetDisabled(instance->buttonConnect, true);
|
|
|
|
EsThreadCreate(NetworkingThread, &instance->networkingThread, instance);
|
|
}
|
|
|
|
void _start() {
|
|
_init();
|
|
|
|
while (true) {
|
|
EsMessage *message = EsMessageReceive();
|
|
|
|
if (message->type == ES_MSG_INSTANCE_CREATE) {
|
|
// Create an new instance.
|
|
|
|
Instance *instance = EsInstanceCreate(message, "IRC Client");
|
|
EsWindow *window = instance->window;
|
|
EsWindowSetIcon(window, ES_ICON_INTERNET_CHAT);
|
|
|
|
// Create the toolbar.
|
|
|
|
EsElement *toolbar = EsWindowGetToolbar(window);
|
|
EsPanel *section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
|
|
EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, EsLiteral("Nick:"));
|
|
instance->textboxNick = EsTextboxCreate(section, ES_FLAGS_DEFAULT, EsStyleIntern(&styleSmallTextbox));
|
|
EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, 0, 5, 0);
|
|
section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
|
|
EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, EsLiteral("Address:"));
|
|
instance->textboxAddress = EsTextboxCreate(section, ES_FLAGS_DEFAULT, EsStyleIntern(&styleSmallTextbox));
|
|
EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, 0, 5, 0);
|
|
section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
|
|
EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, EsLiteral("Port:"));
|
|
instance->textboxPort = EsTextboxCreate(section, ES_FLAGS_DEFAULT, EsStyleIntern(&styleSmallTextbox));
|
|
EsSpacerCreate(toolbar, ES_CELL_H_FILL);
|
|
instance->buttonConnect = EsButtonCreate(toolbar, ES_FLAGS_DEFAULT, 0, EsLiteral("Connect"));
|
|
EsButtonOnCommand(instance->buttonConnect, ConnectCommand);
|
|
|
|
// Create the main area.
|
|
|
|
EsPanel *panel = EsPanelCreate(window, ES_PANEL_VERTICAL | ES_CELL_FILL, ES_STYLE_PANEL_WINDOW_DIVIDER);
|
|
instance->textboxOutput = EsTextboxCreate(panel, ES_CELL_FILL | ES_TEXTBOX_MULTILINE, EsStyleIntern(&styleOutputTextbox));
|
|
EsPanelCreate(panel, ES_CELL_H_FILL, ES_STYLE_SEPARATOR_HORIZONTAL);
|
|
EsPanel *inputArea = EsPanelCreate(panel, ES_PANEL_HORIZONTAL | ES_CELL_H_FILL, ES_STYLE_PANEL_FILLED);
|
|
instance->textboxInput = EsTextboxCreate(inputArea, ES_CELL_FILL, EsStyleIntern(&styleInputTextbox));
|
|
instance->textboxInput->messageUser = TextboxInputCallback;
|
|
EsElementSetDisabled(instance->textboxInput);
|
|
}
|
|
}
|
|
}
|