diff --git a/dependencies/tim b/dependencies/tim index 6d0190c..2a685df 160000 --- a/dependencies/tim +++ b/dependencies/tim @@ -1 +1 @@ -Subproject commit 6d0190c9c0a795ed6e21f844c2c79247fcdafefd +Subproject commit 2a685dfcd052ed3ed57d2b006be25f081e8ec8b8 diff --git a/dependencies/tlibc b/dependencies/tlibc index c4fd225..82a6293 160000 --- a/dependencies/tlibc +++ b/dependencies/tlibc @@ -1 +1 @@ -Subproject commit c4fd22542d1ca917b44a4e3da874f2f018f42665 +Subproject commit 82a6293f2184aba23ba9abe053be503412721d53 diff --git a/src/cli/ClientCLI/ClientCLI.c b/src/cli/ClientCLI/ClientCLI.c index 3c46761..c5cdce3 100644 --- a/src/cli/ClientCLI/ClientCLI.c +++ b/src/cli/ClientCLI/ClientCLI.c @@ -2,35 +2,7 @@ #include "tlibc/filesystem.h" #include "tlibc/term.h" #include "network/tcp-chat-protocol/v1.h" -#include "tim.h" - -static const str greeting_art = STR( - " ^,,^ ╱|\n" - " ( •·•) Meum! (o.o`7\n" - " / ` | Meum... |`˜ \\\n" - "\\(_,J J L l`,)/\n" -); - -static const str farewell_art = STR( - " ^,,^ ╱|\n" - " ( -.-) (>,<`7\n" - " / ` | Goodbye! |`˜ \\\n" - "\\(_,J J L l`,)/\n" -); - - -#define is_alias(LITERAL) str_equals(command, STR(LITERAL)) - -Result(bool) start_screen(Array(char) username_buf, Array(char) password_buf, - str* out_username, str* out_password); -static Result(void) ClientCLI_openUserDB(ClientCLI* self); -static Result(void) ClientCLI_execCommand(ClientCLI* self, str command, bool* stop); -static Result(SavedServer*) ClientCLI_joinNewServer(ClientCLI* self); -static Result(SavedServer*) ClientCLI_selectServerFromCache(ClientCLI* self); -static Result(void) ClientCLI_showSavedServer(ClientCLI* self, SavedServer* server); -static Result(void) ClientCLI_register(ClientCLI* self); -static Result(void) ClientCLI_login(ClientCLI* self); - +#include void ClientCLI_destroy(ClientCLI* self){ if(!self) @@ -44,296 +16,51 @@ void ClientCLI_destroy(ClientCLI* self){ void ClientCLI_construct(ClientCLI* self){ zeroStruct(self); + + self->style.common = (TimStyle){ + .brd = Color256_MidGray, + .bg = Color256_NavyBlue, + .fg = Color256_MidGray + }; + self->style.focused = (TimStyle){ + .brd = Color256_White, + .bg = Color256_DeepSkyBlue, + .fg = Color256_White + }; + self->style.error = (TimStyle){ + .brd = Color256_MidGray, + .bg = Color256_DarkRed, + .fg = Color256_White + }; + self->saved_servers = List_SavedServer_alloc(0); } -Result(void) ClientCLI_run(ClientCLI* self) { +void ClientCLI_run(ClientCLI* self) { Deferral(32); - // ask username and password - Array(char) username_buf = Array_char_alloc(USERNAME_SIZE_MAX + 1); - Array(char) password_buf = Array_char_alloc(PASSWORD_SIZE_MAX + 1); - Defer( - Array_char_destroy(&username_buf); - Array_char_destroy(&password_buf); - ); - str username = str_null, password = str_null; - try(bool start, i, start_screen(username_buf, password_buf, &username, &password)); - if(!start){ - Return RESULT_VOID; - } + StartScreenContext start_screen_ctx; + StartScreenContext_construct(&start_screen_ctx, self); + Defer(StartScreenContext_destroy(&start_screen_ctx)); + MainScreenContext main_screen_ctx; + MainScreenContext_construct(&main_screen_ctx, self); + Defer(MainScreenContext_destroy(&main_screen_ctx)); - tim_reset_terminal(); - term_clear(); - printf(FMT_str"\n", greeting_art.len, greeting_art.data); - - // create client - try(self->client, p, Client_create(username, password)); - // erase password from memory - Array_char_memset(&password_buf, 0); - - // init db - try_void(ClientCLI_openUserDB(self)); - - char input_buf[1024]; - str command_input = str_null; - bool stop = false; - while(!stop){ - sleepMsec(50); - printf("> "); - try_void(term_readLine(input_buf, sizeof(input_buf))); - - command_input = str_from_cstr(input_buf); - str_trim(&command_input, true); - if(command_input.len == 0) - continue; - - ResultVar(void) com_result = ClientCLI_execCommand(self, command_input, &stop); - if(com_result.error){ - Error_addCallPos(com_result.error, ErrorCallPos_here()); - str e_str = Error_toStr(com_result.error); - printf("\n"FMT_str"\n", e_str.len, e_str.data); - str_destroy(e_str); - Error_free(com_result.error); + while(tim_run(FPS)){ + switch(self->state){ + case ClientCLIState_Exit: + Return; + default: + assert(false && "invalid ClientCLI state"); + break; + case ClientCLIState_StartScreen: + start_screen(&start_screen_ctx); + break; + case ClientCLIState_MainScreen: + main_screen(&main_screen_ctx); + break; } } - Return RESULT_VOID; -} - - -static Result(void) ClientCLI_openUserDB(ClientCLI* self){ - Deferral(8); - - str username = Client_getUserName(self->client); - // TODO: encrypt user database - // Array(u8) user_data_key = Client_getUserDataKey(self->client); - - // build database file path - try(char* user_dir, p, path_getUserDir()); - Defer(free(user_dir)); - char* db_path = strcat_malloc( - user_dir, - path_seps".local"path_seps"tcp-chat-client"path_seps"user-db"path_seps, - username.data, ".sqlite" - ); - Defer(free(db_path)); - printf("loading database '%s'\n", db_path); - - try(self->db, p, ClientDatabase_open(db_path)); - try(self->queries, p, ClientQueries_compile(self->db)); - - // load whole servers table to list - try_void(SavedServer_getAll(self->queries, &self->saved_servers)); - - Return RESULT_VOID; -} - -static Result(void) ClientCLI_execCommand(ClientCLI* self, str command, bool* stop){ - Deferral(64); - - if(is_alias("q") || is_alias("quit") || is_alias("exit")){ - printf(FMT_str"\n", farewell_art.len, farewell_art.data); - *stop = true; - } - else if(is_alias("clear")){ - term_clear(); - } - else if(is_alias("h") || is_alias("help")){ - printf( - "COMMANDS:\n" - "Without connection:\n" - " h, help Show this message.\n" - " q, quit, exit Close the program.\n" - " clear Clear the screen.\n" - "Connection:\n" - " j, join Join a new server and select it.\n" - " s, select Select a server you joined before.\n" - "After connection:\n" - " r, register Create account on selected server\n" - " l, login Authorize on selected server\n" - "Authorized:\n" - ); - } - else if (is_alias("j") || is_alias("join")){ - // ask address and key, connect to server - try_void(ClientCLI_joinNewServer(self)); - } - else if(is_alias("s") || is_alias("select")){ - // show scrollable list of servers, get selected one - try_void(ClientCLI_selectServerFromCache(self)); - } - else if(is_alias("r") || is_alias("register")){ - try_void(ClientCLI_register(self)); - } - else if(is_alias("l") || is_alias("login")){ - try_void(ClientCLI_login(self)); - // TODO: call Client_runIO(): - // function with infinite loop which sends and receives messages - // with navigation across server channels - } - else { - printf("ERROR: unknown command.\n" - "Use 'h' to see list of avaliable commands\n"); - } - - Return RESULT_VOID; -} - -static Result(void) ClientCLI_joinNewServer(ClientCLI* self){ - Deferral(8); - bool success = false; - - // ask server address - const u32 address_alloc_size = HOSTADDR_SIZE_MAX + 1; - str address = str_construct((char*)malloc(address_alloc_size), address_alloc_size, true); - Defer(if(!success) str_destroy(address)); - printf("Enter server address (ip:port):\n"); - try_void(term_readLine(address.data, address.len)); - address.len = strlen(address.data); - str_trim(&address, true); - - // ask server public key - const u32 server_pk_alloc_size = PUBLIC_KEY_BASE64_SIZE_MAX + 1; - str server_pk = str_construct((char*)malloc(server_pk_alloc_size), server_pk_alloc_size, true); - Defer(if(!success) str_destroy(server_pk)); - printf("Enter server public key (RSA-Public-:):\n"); - try_void(term_readLine(server_pk.data, server_pk.len)); - server_pk.len = strlen(server_pk.data); - str_trim(&server_pk, true); - - printf("Connecting to server...\n"); - try_void(Client_connect(self->client, address.data, server_pk.data)); - printf("Connection established\n"); - - str server_name = str_null; - try_void(Client_getServerName(self->client, &server_name)); - Defer(if(!success) str_destroy(server_name)); - str server_description = str_null; - try_void(Client_getServerDescription(self->client, &server_description)); - Defer(if(!success) str_destroy(server_description)); - - SavedServer server = SavedServer_construct( - address, - server_pk, - server_name, - server_description - ); - try_void(SavedServer_createOrUpdate(self->queries, &server)); - List_SavedServer_pushMany(&self->saved_servers, &server, 1); - - try_void(ClientCLI_showSavedServer(self, &server)); - - success = true; - Return RESULT_VOID; -} - -static Result(void) ClientCLI_selectServerFromCache(ClientCLI* self){ - Deferral(8); - bool success = false; - - u32 servers_count = self->saved_servers.len; - if(servers_count == 0){ - printf("No saved servers found\n"); - Return RESULT_VOID; - } - - for(u32 i = 0; i < servers_count; i++){ - SavedServer* server = &self->saved_servers.data[i]; - printf("[%02u] "FMT_str" "FMT_str"\n", - i, str_unwrap(server->address), str_unwrap(server->name)); - } - - char buf[32]; - u32 selected_i = -1; - while(true) { - printf("Type 'q' to cancel\n"); - printf("Select server number: "); - try_void(term_readLine(buf, sizeof(buf))); - str input_line = str_from_cstr(buf); - str_trim(&input_line, true); - if(str_equals(input_line, STR("q"))){ - Return RESULT_VOID; - } - if(sscanf(buf, FMT_u32, &selected_i) != 1){ - printf("ERROR: not a number\n"); - } - else if(selected_i >= servers_count){ - printf("ERROR: not a server number\n"); - } - else break; - } - SavedServer* selected_server = &self->saved_servers.data[selected_i]; - - printf("Connecting to '"FMT_str"'...\n", str_unwrap(selected_server->address)); - try_void(Client_connect(self->client, selected_server->address.data, selected_server->pk_base64.data)); - printf("Connection established\n"); - - // update server name - bool server_info_changed = false; - str updated_server_name = str_null; - try_void(Client_getServerName(self->client, &updated_server_name)); - Defer(if(!success) str_destroy(updated_server_name)); - if(!str_equals(updated_server_name, selected_server->name)){ - server_info_changed = true; - selected_server->name = updated_server_name; - } - - // update server description - str updated_server_description = str_null; - try_void(Client_getServerDescription(self->client, &updated_server_description)); - Defer(if(!success) str_destroy(updated_server_description)); - if(!str_equals(updated_server_description, selected_server->description)){ - server_info_changed = true; - selected_server->description = updated_server_description; - } - - if(server_info_changed){ - try_void(SavedServer_createOrUpdate(self->queries, selected_server)); - } - - try_void(ClientCLI_showSavedServer(self, selected_server)); - - success = true; - Return RESULT_VOID; -} - -static Result(void) ClientCLI_showSavedServer(ClientCLI* self, SavedServer* server){ - Deferral(8); - (void)self; - - printf("Server Name: "FMT_str"\n", str_unwrap(server->name)); - printf("Host Address: "FMT_str"\n", str_unwrap(server->address)); - printf("Description:\n"FMT_str"\n\n", str_unwrap(server->description)); - printf("Public Key:\n" FMT_str"\n\n", str_unwrap(server->pk_base64)); - printf("Type 'register' if you don't have an account on the server.\n"); - printf("Type 'login' to authorize on the server.\n"); - - Return RESULT_VOID; -} - -static Result(void) ClientCLI_register(ClientCLI* self){ - Deferral(8); - - i64 user_id = 0; - try_void(Client_register(self->client, &user_id)); - printf("Registered successfully\n"); - printf("user_id: "FMT_i64"\n", user_id); - try_assert(user_id > 0); - // TODO: use user_id somewhere - - Return RESULT_VOID; -} - -static Result(void) ClientCLI_login(ClientCLI* self){ - Deferral(8); - - i64 user_id = 0, landing_channel_id = 0; - try_void(Client_login(self->client, &user_id, &landing_channel_id)); - printf("Authorized successfully\n"); - printf("user_id: "FMT_i64", landing_channel_id: "FMT_i64"\n", user_id, landing_channel_id); - try_assert(user_id > 0); - // TODO: use user_id, landing_channel_id somewhere - - Return RESULT_VOID; + Return; } diff --git a/src/cli/ClientCLI/ClientCLI.h b/src/cli/ClientCLI/ClientCLI.h index 606b859..202e8b2 100644 --- a/src/cli/ClientCLI/ClientCLI.h +++ b/src/cli/ClientCLI/ClientCLI.h @@ -4,10 +4,25 @@ #include "tlibc/collections/HashMap.h" #include "tlibc/collections/List.h" #include "db/client_db.h" +#include "tim.h" #define FPS 30 +typedef enum ClientCLIState { + ClientCLIState_StartScreen, + ClientCLIState_Exit, + ClientCLIState_MainScreen, + ClientCLIState_ServerChannels, + ClientCLIState_ChannelChat, +} ClientCLIState; + typedef struct ClientCLI { + ClientCLIState state; + struct { + TimStyle common; + TimStyle focused; + TimStyle error; + } style; Client* client; tsqlite_connection* db; ClientQueries* queries; @@ -16,7 +31,7 @@ typedef struct ClientCLI { void ClientCLI_construct(ClientCLI* self); void ClientCLI_destroy(ClientCLI* self); -Result(void) ClientCLI_run(ClientCLI* self); +void ClientCLI_run(ClientCLI* self); enum { @@ -26,3 +41,51 @@ enum { Color256_DeepSkyBlue = 0x18, Color256_DarkRed = 0x58, }; + +typedef struct TextInputState { + TimEditState edit; + cstr label; + struct { + TimStyle common; + TimStyle focused; + } style; + TimKey result_key; +} TextInputState; + +void TextInputState_construct(TextInputState* ctx, cstr label, + Array(char) buf, NULLABLE(cstr) initial_value, bool masked, + TimStyle common, TimStyle focused); + +/// tim_edit with tim_label over its upper border. +void text_input(TextInputState* ctx, i32 x, i32 y, i32 w, TimStyle style); + +void TimScrollItem_fromTextInputState(TimScrollItem* item, TextInputState* input); + +/// Intended to use in TimScrollItem +/// @param data TextInputState* +void draw_item_text_input(bool is_selected, TimRect place, void* data); + + +List_declare(TimScrollItem); + +typedef struct StartScreenContext { + ClientCLI* client; + char* err_msg; // heap only + TextInputState input_username; + TextInputState input_password; + List(TimScrollItem) scroll_list; + TimScrollState scroll_state; +} StartScreenContext; + +void StartScreenContext_construct(StartScreenContext* ctx, ClientCLI* client); +void StartScreenContext_destroy(StartScreenContext* ctx); +void start_screen(StartScreenContext* ctx); + + +typedef struct MainScreenContext { + ClientCLI* client; +} MainScreenContext; + +void MainScreenContext_construct(MainScreenContext* ctx, ClientCLI* client); +void MainScreenContext_destroy(MainScreenContext* ctx); +void main_screen(MainScreenContext* ctx); diff --git a/src/cli/ClientCLI/main_screen.c b/src/cli/ClientCLI/main_screen.c new file mode 100644 index 0000000..6017f2b --- /dev/null +++ b/src/cli/ClientCLI/main_screen.c @@ -0,0 +1,259 @@ +#include "ClientCLI.h" +#include "tlibc/term.h" +#include "network/tcp-chat-protocol/v1.h" +#include "tim.h" + + +static const str greeting_art = STR( + " ^,,^ ╱|\n" + " ( •·•) Meum! (o.o`7\n" + " / ` | Meum... |`˜ \\\n" + "\\(_,J J L l`,)/\n" +); + +static const str farewell_art = STR( + " ^,,^ ╱|\n" + " ( -.-) (>,<`7\n" + " / ` | Goodbye! |`˜ \\\n" + "\\(_,J J L l`,)/\n" +); + + +static Result(SavedServer*) joinNewServer(ClientCLI* self); +static Result(SavedServer*) selectServerFromCache(ClientCLI* self); +static Result(void) showSavedServer(ClientCLI* self, SavedServer* server); +static Result(void) registerAtServer(ClientCLI* self); +static Result(void) loginAtServer(ClientCLI* self); + + +void MainScreenContext_construct(MainScreenContext* ctx, ClientCLI* client){ + zeroStruct(ctx); + ctx->client = client; +} + +void MainScreenContext_destroy(MainScreenContext* ctx){ + +} + +void main_screen(MainScreenContext* ctx){ + tim_frame(0, 0, ~0, ~0, ctx->client->style.common); + + if(tim_button("[Esc/Q] Quit", A, A, A, A, ctx->client->style.common) + || tim_is_key_press('q') + || tim_is_key_press(TimKey_Escape)) + { + ctx->client->state = ClientCLIState_Exit; + } +} + + +/* +static Result(void) ClientCLI_execCommand(ClientCLI* self, str command, bool* stop){ + Deferral(64); + + if(is_alias("q") || is_alias("quit") || is_alias("exit")){ + printf(FMT_str"\n", farewell_art.len, farewell_art.data); + *stop = true; + } + else if(is_alias("clear")){ + term_clear(); + } + else if(is_alias("h") || is_alias("help")){ + printf( + "COMMANDS:\n" + "Without connection:\n" + " h, help Show this message.\n" + " q, quit, exit Close the program.\n" + " clear Clear the screen.\n" + "Connection:\n" + " j, join Join a new server and select it.\n" + " s, select Select a server you joined before.\n" + "After connection:\n" + " r, register Create account on selected server\n" + " l, login Authorize on selected server\n" + "Authorized:\n" + ); + } + else if (is_alias("j") || is_alias("join")){ + // ask address and key, connect to server + try_void(joinNewServer(self)); + } + else if(is_alias("s") || is_alias("select")){ + // show scrollable list of servers, get selected one + try_void(selectServerFromCache(self)); + } + else if(is_alias("r") || is_alias("register")){ + try_void(registerAtServer(self)); + } + else if(is_alias("l") || is_alias("login")){ + try_void(loginAtServer(self)); + // TODO: call Client_runIO(): + // function with infinite loop which sends and receives messages + // with navigation across server channels + } + else { + printf("ERROR: unknown command.\n" + "Use 'h' to see list of avaliable commands\n"); + } + + Return RESULT_VOID; +} +*/ + +static Result(void) joinNewServer(ClientCLI* self){ + Deferral(8); + bool success = false; + + // ask server address + const u32 address_alloc_size = HOSTADDR_SIZE_MAX + 1; + str address = str_construct((char*)malloc(address_alloc_size), address_alloc_size, true); + Defer(if(!success) str_destroy(address)); + printf("Enter server address (ip:port):\n"); + try_void(term_readLine(address.data, address.len)); + address.len = strlen(address.data); + str_trim(&address, true); + + // ask server public key + const u32 server_pk_alloc_size = PUBLIC_KEY_BASE64_SIZE_MAX + 1; + str server_pk = str_construct((char*)malloc(server_pk_alloc_size), server_pk_alloc_size, true); + Defer(if(!success) str_destroy(server_pk)); + printf("Enter server public key (RSA-Public-:):\n"); + try_void(term_readLine(server_pk.data, server_pk.len)); + server_pk.len = strlen(server_pk.data); + str_trim(&server_pk, true); + + printf("Connecting to server...\n"); + try_void(Client_connect(self->client, address.data, server_pk.data)); + printf("Connection established\n"); + + str server_name = str_null; + try_void(Client_getServerName(self->client, &server_name)); + Defer(if(!success) str_destroy(server_name)); + str server_description = str_null; + try_void(Client_getServerDescription(self->client, &server_description)); + Defer(if(!success) str_destroy(server_description)); + + SavedServer server = SavedServer_construct( + address, + server_pk, + server_name, + server_description + ); + try_void(SavedServer_createOrUpdate(self->queries, &server)); + List_SavedServer_pushMany(&self->saved_servers, &server, 1); + + try_void(showSavedServer(self, &server)); + + success = true; + Return RESULT_VOID; +} + +static Result(void) selectServerFromCache(ClientCLI* self){ + Deferral(8); + bool success = false; + + u32 servers_count = self->saved_servers.len; + if(servers_count == 0){ + printf("No saved servers found\n"); + Return RESULT_VOID; + } + + for(u32 i = 0; i < servers_count; i++){ + SavedServer* server = &self->saved_servers.data[i]; + printf("[%02u] "FMT_str" "FMT_str"\n", + i, str_unwrap(server->address), str_unwrap(server->name)); + } + + char buf[32]; + u32 selected_i = -1; + while(true) { + printf("Type 'q' to cancel\n"); + printf("Select server number: "); + try_void(term_readLine(buf, sizeof(buf))); + str input_line = str_from_cstr(buf); + str_trim(&input_line, true); + if(str_equals(input_line, STR("q"))){ + Return RESULT_VOID; + } + if(sscanf(buf, FMT_u32, &selected_i) != 1){ + printf("ERROR: not a number\n"); + } + else if(selected_i >= servers_count){ + printf("ERROR: not a server number\n"); + } + else break; + } + SavedServer* selected_server = &self->saved_servers.data[selected_i]; + + printf("Connecting to '"FMT_str"'...\n", str_unwrap(selected_server->address)); + try_void(Client_connect(self->client, selected_server->address.data, selected_server->pk_base64.data)); + printf("Connection established\n"); + + // update server name + bool server_info_changed = false; + str updated_server_name = str_null; + try_void(Client_getServerName(self->client, &updated_server_name)); + Defer(if(!success) str_destroy(updated_server_name)); + if(!str_equals(updated_server_name, selected_server->name)){ + server_info_changed = true; + selected_server->name = updated_server_name; + } + + // update server description + str updated_server_description = str_null; + try_void(Client_getServerDescription(self->client, &updated_server_description)); + Defer(if(!success) str_destroy(updated_server_description)); + if(!str_equals(updated_server_description, selected_server->description)){ + server_info_changed = true; + selected_server->description = updated_server_description; + } + + if(server_info_changed){ + try_void(SavedServer_createOrUpdate(self->queries, selected_server)); + } + + try_void(showSavedServer(self, selected_server)); + + success = true; + Return RESULT_VOID; +} + +static Result(void) showSavedServer(ClientCLI* self, SavedServer* server){ + Deferral(8); + (void)self; + + printf("Server Name: "FMT_str"\n", str_unwrap(server->name)); + printf("Host Address: "FMT_str"\n", str_unwrap(server->address)); + printf("Description:\n"FMT_str"\n\n", str_unwrap(server->description)); + printf("Public Key:\n" FMT_str"\n\n", str_unwrap(server->pk_base64)); + printf("Type 'register' if you don't have an account on the server.\n"); + printf("Type 'login' to authorize on the server.\n"); + + Return RESULT_VOID; +} + +static Result(void) registerAtServer(ClientCLI* self){ + Deferral(8); + + i64 user_id = 0; + try_void(Client_register(self->client, &user_id)); + printf("Registered successfully\n"); + printf("user_id: "FMT_i64"\n", user_id); + try_assert(user_id > 0); + // TODO: use user_id somewhere + + Return RESULT_VOID; +} + +static Result(void) loginAtServer(ClientCLI* self){ + Deferral(8); + + i64 user_id = 0, landing_channel_id = 0; + try_void(Client_login(self->client, &user_id, &landing_channel_id)); + printf("Authorized successfully\n"); + printf("user_id: "FMT_i64", landing_channel_id: "FMT_i64"\n", user_id, landing_channel_id); + try_assert(user_id > 0); + // TODO: use user_id, landing_channel_id somewhere + + Return RESULT_VOID; +} diff --git a/src/cli/ClientCLI/start_screen.c b/src/cli/ClientCLI/start_screen.c index 95680a6..09f944f 100644 --- a/src/cli/ClientCLI/start_screen.c +++ b/src/cli/ClientCLI/start_screen.c @@ -1,149 +1,158 @@ #include "ClientCLI.h" #include "network/tcp-chat-protocol/v1.h" -#include "tim.h" +#include "tlibc/filesystem.h" -typedef struct TextInputState { - TimEditState edit; - cstr label; - TimStyle style_default; - TimStyle style_focused; - TimKey result_key; -} TextInputState; +static void draw_buttons(bool is_selected, TimRect place, void* data); +static Result(void) openUserDB(StartScreenContext* ctx); -void draw_item_text_input(bool is_selected, TimRect place, void* data){ - TextInputState* ctx = data; - TimStyle style = is_selected ? ctx->style_focused : ctx->style_default; - ctx->result_key = tim_edit(&ctx->edit, place.x, place.y, place.w, style); - tim_label(ctx->label, place.x + 3, place.y, A, 1, style); + +#define handleError(R) _handleError(ctx, R) +static void _handleError(StartScreenContext* ctx, ResultVar(void) r){ + free(ctx->err_msg); + ctx->err_msg = Error_toStr(r.error).data; + Error_free(r.error); } -typedef struct StartScreenContext { - TimStyle style_default; - TimStyle style_focused; - TimStyle style_error; - TextInputState input_username; - TextInputState input_password; - bool exit; - bool start; - char* err_msg; // heap only -} StartScreenContext; +static void clearError(StartScreenContext* ctx){ + free(ctx->err_msg); + ctx->err_msg = NULL; +} -void draw_start_screen_buttons(bool is_selected, TimRect place, void* data){ +void StartScreenContext_construct(StartScreenContext* ctx, ClientCLI* client){ + zeroStruct(ctx); + ctx->client = client; + + Array(char) username_buf = Array_char_alloc(USERNAME_SIZE_MAX + 1); + TextInputState_construct(&ctx->input_username, "[username]", + username_buf, NULL, false, + ctx->client->style.common, ctx->client->style.focused); + + Array(char) password_buf = Array_char_alloc(PASSWORD_SIZE_MAX + 1); + TextInputState_construct(&ctx->input_password, "[password]", + password_buf, NULL, true, + ctx->client->style.common, ctx->client->style.focused); + + ctx->scroll_list = List_TimScrollItem_alloc(8); + + TimScrollItem item_input_username; + TimScrollItem_fromTextInputState(&item_input_username, &ctx->input_username); + List_TimScrollItem_push(&ctx->scroll_list, item_input_username); + + TimScrollItem item_input_password; + TimScrollItem_fromTextInputState(&item_input_password, &ctx->input_password); + List_TimScrollItem_push(&ctx->scroll_list, item_input_password); + + TimScrollItem item_buttons = { + .data = ctx, + .focus_target = NULL, + .h = 3, + .draw = draw_buttons + }; + List_TimScrollItem_push(&ctx->scroll_list, item_buttons); + + ctx->scroll_state = (TimScrollState){ .items = ctx->scroll_list.data, .len = ctx->scroll_list.len }; +} + +void StartScreenContext_destroy(StartScreenContext* ctx){ + free(ctx->input_username.edit.s); + free(ctx->input_password.edit.s); + free(ctx->err_msg); + List_TimScrollItem_destroy(&ctx->scroll_list); +} + +void start_screen(StartScreenContext* ctx) +{ + tim_fill(tim_cell(" ", ctx->client->style.common.fg, ctx->client->style.common.bg), 0, 0, A, A); + tim_scope(A, A, 40, 13) { + tim_scroll(&ctx->scroll_state, 1, 1, ~1, ~1, ctx->client->style.common); + } + + if(ctx->err_msg){ + i32 below_scroll = tim->scopes[tim->scope].h/2 + 6; + tim_label("ERROR: ", A, below_scroll, A, A, ctx->client->style.error); + tim_label(ctx->err_msg, A, below_scroll + 1, A, A, ctx->client->style.error); + } +} + + + +static void draw_buttons(bool is_selected, TimRect place, void* data){ StartScreenContext* ctx = data; - TimStyle style = is_selected ? ctx->style_focused : ctx->style_default; + TimStyle style = is_selected ? ctx->client->style.focused : ctx->client->style.common; i32 start_w = place.w / 2; i32 dist = 1; i32 quit_x = place.x + start_w + dist; i32 quit_w = place.w - start_w - dist; + if(tim_button("[Esc/Q] Quit", quit_x, place.y, quit_w, A, ctx->client->style.common) + || tim_is_key_press('q') + || ctx->input_username.result_key == TimKey_Escape + || ctx->input_password.result_key == TimKey_Escape) + { + ctx->client->state = ClientCLIState_Exit; + return; + } if(tim_button("[Enter] Start", place.x, place.y, start_w, A, style) || tim_is_key_press(TimKey_Enter)) { - ctx->start = true; - } - else if(tim_button("[Esc/Q] Quit", quit_x, place.y, quit_w, A, ctx->style_default) - || tim_is_key_press('q')) - { - ctx->exit = true; + clearError(ctx); + + // check username + str username = str_from_cstr(ctx->input_username.edit.s); + str_trim(&username, true); + str name_error_str = validateUsername_str(username); + if(name_error_str.data){ + ctx->err_msg = name_error_str.data; + return; + } + + // check password + str password = str_from_cstr(ctx->input_password.edit.s); + str_trim(&password, true); + if(password.len < PASSWORD_SIZE_MIN || password.len > PASSWORD_SIZE_MAX){ + ctx->err_msg = sprintf_malloc( + "password length (in bytes) must be >= %i and <= %i", + PASSWORD_SIZE_MIN, PASSWORD_SIZE_MAX + ); + return; + } + + + // create client + try_handle(ctx->client->client, p, Client_create(username, password), handleError); + + // init user DB + try_handle_void(openUserDB(ctx), handleError); + + // erase password from memory + memset(ctx->input_password.edit.s, 0, ctx->input_password.edit.capacity); + + ctx->client->state = ClientCLIState_MainScreen; + return; } } -Result(bool) start_screen(Array(char) username_buf, Array(char) password_buf, - str* out_username, str* out_password) -{ - Deferral(16); +static Result(void) openUserDB(StartScreenContext* ctx){ + Deferral(8); - bool result = false; - - StartScreenContext ctx = { - .style_default = { - .brd = Color256_MidGray, - .bg = Color256_NavyBlue, - .fg = Color256_MidGray - }, - .style_focused = { - .brd = Color256_White, - .bg = Color256_DeepSkyBlue, - .fg = Color256_White - }, - .style_error = { - .brd = Color256_MidGray, - .bg = Color256_DarkRed, - .fg = Color256_White - }, - }; + str username = Client_getUserName(ctx->client->client); + // TODO: encrypt user database + // Array(u8) user_data_key = Client_getUserDataKey(ctx->client->client); - TimEditState_construct(&ctx.input_username.edit, username_buf.data, username_buf.len, NULL); - ctx.input_username.label = "[username]"; - ctx.input_username.style_default = ctx.style_default; - ctx.input_username.style_focused = ctx.style_focused; + // build database file path + try(char* user_dir, p, path_getUserDir()); + Defer(free(user_dir)); + char* db_path = strcat_malloc( + user_dir, + path_seps".local"path_seps"tcp-chat-client"path_seps"user-db"path_seps, + username.data, ".sqlite" + ); + Defer(free(db_path)); - TimEditState_construct(&ctx.input_password.edit, password_buf.data, password_buf.len, NULL); - ctx.input_password.edit.masked = true; - ctx.input_password.label = "[password]"; - ctx.input_password.style_default = ctx.style_default; - ctx.input_password.style_focused = ctx.style_focused; + try(ctx->client->db, p, ClientDatabase_open(db_path)); + try(ctx->client->queries, p, ClientQueries_compile(ctx->client->db)); - TimScrollItem items[] = { - // username input - { .data = &ctx.input_username, .focus_target = &ctx.input_username.edit, .h = 3, .draw = draw_item_text_input }, - // password input - { .data = &ctx.input_password, .focus_target = &ctx.input_password.edit, .h = 3, .draw = draw_item_text_input }, - // buttons - { .data = &ctx, .focus_target = NULL, .h = 3, .draw = draw_start_screen_buttons }, - }; - - TimScrollState scroll_list = { .items = items, .len = ARRAY_LEN(items) }; + // load whole servers table to list + try_void(SavedServer_getAll(ctx->client->queries, &ctx->client->saved_servers)); - while(tim_run(FPS)){ - tim_fill(tim_cell(" ", ctx.style_default.fg, ctx.style_default.bg), 0, 0, A, A); - tim_scope(A, A, 40, 13) { - tim_scroll(&scroll_list, 1, 1, ~1, ~1, ctx.style_default); - } - - if(ctx.err_msg){ - i32 below_scroll = tim->scopes[tim->scope].h/2 + 6; - tim_label("ERROR: ", A, below_scroll, A, A, ctx.style_error); - tim_label(ctx.err_msg, A, below_scroll + 1, A, A, ctx.style_error); - } - - if(ctx.exit - || ctx.input_username.result_key == TimKey_Escape - || ctx.input_password.result_key == TimKey_Escape) - { - result = false; - break; - } - if(ctx.start){ - ctx.start = false; - free(ctx.err_msg); - ctx.err_msg = NULL; - - // check username - str username = str_from_cstr(username_buf.data); - str_trim(&username, true); - str name_error_str = validateUsername_str(username); - if(name_error_str.data){ - ctx.err_msg = name_error_str.data; - continue; - } - - // check password - str password = str_from_cstr(password_buf.data); - str_trim(&password, true); - if(password.len < PASSWORD_SIZE_MIN || password.len > PASSWORD_SIZE_MAX){ - ctx.err_msg = sprintf_malloc( - "password length (in bytes) must be >= %i and <= %i", - PASSWORD_SIZE_MIN, PASSWORD_SIZE_MAX - ); - continue; - } - - *out_username = username; - *out_password = password; - result = true; - break; - } - } - - Return RESULT_VALUE(i, result); -} \ No newline at end of file + Return RESULT_VOID; +} diff --git a/src/cli/ClientCLI/widgets.c b/src/cli/ClientCLI/widgets.c new file mode 100644 index 0000000..d3fbf62 --- /dev/null +++ b/src/cli/ClientCLI/widgets.c @@ -0,0 +1,31 @@ +#include "ClientCLI.h" + +void TextInputState_construct(TextInputState* ctx, cstr label, + Array(char) buf, NULLABLE(cstr) initial_value, bool masked, + TimStyle common, TimStyle focused) +{ + TimEditState_construct(&ctx->edit, buf.data, buf.len, initial_value); + ctx->edit.masked = masked; + ctx->label = label; + ctx->style.common = common; + ctx->style.focused = focused; +} + +void text_input(TextInputState* ctx, i32 x, i32 y, i32 w, TimStyle style){ + ctx->result_key = tim_edit(&ctx->edit, x, y, w, style); + tim_label(ctx->label, x + 3, y, A, 1, style); +} + +void TimScrollItem_fromTextInputState(TimScrollItem* item, TextInputState* input){ + zeroStruct(item); + item->data = input; + item->focus_target = &input->edit; + item->h = 3; + item->draw = draw_item_text_input; +} + +void draw_item_text_input(bool is_selected, TimRect place, void* data){ + TextInputState* ctx = data; + TimStyle style = is_selected ? ctx->style.focused : ctx->style.common; + text_input(ctx, place.x, place.y, place.w, style); +} diff --git a/src/cli/modes/ClientMode.c b/src/cli/modes/ClientMode.c index 46afc7e..cc0de22 100644 --- a/src/cli/modes/ClientMode.c +++ b/src/cli/modes/ClientMode.c @@ -9,7 +9,7 @@ Result(void) run_ClientMode(cstr config_path) { ClientCLI_construct(&client); Defer(ClientCLI_destroy(&client)); // start infinite loop on main thread - try_void(ClientCLI_run(&client)); + ClientCLI_run(&client); Return RESULT_VOID; }