// 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 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, &styleSmallTextbox); EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL); EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, EsLiteral("Address:")); instance->textboxAddress = EsTextboxCreate(section, ES_FLAGS_DEFAULT, &styleSmallTextbox); EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL); EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, EsLiteral("Port:")); instance->textboxPort = EsTextboxCreate(section, ES_FLAGS_DEFAULT, &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, &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, &styleInputTextbox); instance->textboxInput->messageUser = TextboxInputCallback; EsElementSetDisabled(instance->textboxInput); } } }