From: jxnshi Date: Fri, 17 Jan 2025 00:22:10 +0000 (+0100) Subject: Add room navigation X-Git-Url: https://jxnshi.xyz/repos?a=commitdiff_plain;h=79e8adec9bf23ef8a9500baba445cfcf9c5830e8;p=mesange.git Add room navigation --- diff --git a/client-cli/client-cli b/client-cli/client-cli index 0f0eb98..63faac5 100755 Binary files a/client-cli/client-cli and b/client-cli/client-cli differ diff --git a/client-cli/command.odin b/client-cli/command.odin index 0c87125..d3bc000 100644 --- a/client-cli/command.odin +++ b/client-cli/command.odin @@ -21,9 +21,6 @@ handle_command :: proc(app: ^App, command: string) -> Maybe(Handle_Command_Error return .Not_A_Command } - sync.mutex_lock(&app.mutex) - defer sync.mutex_unlock(&app.mutex) - command_split, err := strings.split(command, " ", context.temp_allocator) command_name := command_split[0] args := command_split[1:] @@ -80,6 +77,11 @@ handle_command :: proc(app: ^App, command: string) -> Maybe(Handle_Command_Error room_details := strings.split_n(args[0], ":", 2) + if len(room_details) != 2 { + app_set_info_bar(app, "Invalid room.") + return .Invalid_Arguments + } + room, ok := room_init(room_details[0], room_details[1]) if !ok { diff --git a/client-cli/main.odin b/client-cli/main.odin index 0df2174..92d73d2 100644 --- a/client-cli/main.odin +++ b/client-cli/main.odin @@ -32,13 +32,6 @@ json_marshal_options := json.Marshal_Options{ use_enum_names = true, } -Nav_Action :: enum { - up, - down, - select, - escape, -} - App :: struct { mutex: sync.Mutex, running: bool, @@ -65,12 +58,9 @@ App :: struct { seed_phrase_entropy: u128, seed_phrase_checksum: u8, - connecting: bool, host: Maybe(net.TCP_Socket), - connection_thread: Maybe(^thread.Thread), - nav_action: Nav_Action, - selected_room: u32, + selected_room: int, } app_init :: proc(storage_path: string) -> App { @@ -130,10 +120,6 @@ app_deinit :: proc(app: ^App) { sync.mutex_lock(&app.mutex) defer sync.mutex_unlock(&app.mutex) - if connection_thread, ok := app.connection_thread.?; ok { - thread.destroy(connection_thread) - } - if host, ok := app.host.?; ok { net.close(host) } @@ -172,6 +158,9 @@ app_update_room_list_window :: proc(app: ^App) { return } + sync.mutex_lock(&app.mutex) + defer sync.mutex_unlock(&app.mutex) + nc.wclear(app.room_list_window) c_room_name_buffer: [ROOM_NAME_MAX_SIZE + 1]u8 @@ -179,7 +168,7 @@ app_update_room_list_window :: proc(app: ^App) { for &room, i in app.profile.rooms { room_color := nc.COLOR_PAIR(3) - if u32(i) == app.selected_room { + if i == app.selected_room { room_color = nc.COLOR_PAIR(1) } @@ -189,7 +178,9 @@ app_update_room_list_window :: proc(app: ^App) { mem.set(raw_data(c_room_name_buffer[:]), ' ', len(c_room_name_buffer)) mem.copy_non_overlapping(raw_data(c_room_name_buffer[1:]), raw_data(room_name_bytes), len(room_name)) - c_room_name_buffer[len(c_room_name_buffer) - 1] = 0 + _, room_name_rune_count, _ := utf8.grapheme_count(room_name) + + c_room_name_buffer[1 + len(room_name) + 31 - room_name_rune_count + 1] = 0 c_room_name := strings.unsafe_string_to_cstring(string(c_room_name_buffer[:])) @@ -217,13 +208,19 @@ app_set_state :: proc(app: ^App, state: State) { screen_height, screen_width := nc.getmaxyx(app.screen) old_state := app.state - app.state = state + + { + sync.mutex_lock(&app.mutex) + defer sync.mutex_unlock(&app.mutex) + + app.state = state + } color := nc.COLOR_PAIR(3) if old_state != .Main && state == .Main { - app.room_list_window = nc.newwin(screen_height - 2, 30, 0, 0) - app.room_window = nc.newwin(screen_height - 2, screen_width - 30, 0, 30) + app.room_list_window = nc.newwin(screen_height - 2, 34, 0, 0) + app.room_window = nc.newwin(screen_height - 2, screen_width - 34, 0, 34) app_update_room_list_window(app) @@ -336,11 +333,47 @@ app_clear_box_message :: proc(app: ^App) { } } +handle_getch :: proc(app: ^App, buffer_len, cursor_pos: ^int) -> (Maybe(int), bool) { + char := int(nc.wgetch(app.input_window)) + + switch char { + case '\n': + return nil, false + + case nc.KEY_BACKSPACE: + if cursor_pos^ != 0 { + buffer_len^ -= 1 + cursor_pos^ -=1 + } + + return nil, true + + case nc.KEY_LEFT: + if cursor_pos^ != 0 { + cursor_pos^ -= 1 + } + + return nil, true + + case nc.KEY_RIGHT: + if cursor_pos^ != buffer_len^ { + cursor_pos^ += 1 + } + + return nil, true + + case 0o400..=0o777: + return nil, true + + case: + return char, true + } +} + app_get_input :: proc( app: ^App, hidden := false, allow_empty := false, - only_command := false, allocator := context.allocator, ) -> string { context.allocator = allocator @@ -350,70 +383,150 @@ app_get_input :: proc( nc.wrefresh(app.input_window) buffer: [256]u8 - - buffer_len: u32 - cursor_pos: u32 + buffer_len, cursor_pos: int screen_height, screen_width := nc.getmaxyx(app.screen) nc.curs_set(1) - input_loop: for { - char := nc.wgetch(app.input_window) + for { + maybe_char, keep_going := handle_getch(app, &buffer_len, &cursor_pos) - if only_command && (buffer_len == 0 || buffer[0] != ':') && char != ':' { + if !keep_going { + break + } + + char, ok := maybe_char.? + + if !ok { continue } - switch char { - case '\n': - break input_loop + if char == 27 { + buffer_len = 0 + cursor_pos = 0 - case 27: - buffer_len = 0 - cursor_pos = 0 + continue + } - case nc.KEY_BACKSPACE: - if cursor_pos != 0 { - buffer_len -= 1 - cursor_pos -= 1 - } + buffer_len += 1 - case nc.KEY_LEFT: - if cursor_pos != 0 { - cursor_pos -= 1 - } + mem.copy( + raw_data(buffer[cursor_pos + 1 : buffer_len]), + raw_data(buffer[cursor_pos : buffer_len - 1]), + int(buffer_len - cursor_pos - 1) + ) + + buffer[cursor_pos] = u8(char) + + cursor_pos += 1 + + buffer[buffer_len] = 0 + output := string(buffer[:buffer_len + 1]) + + nc.wclear(app.input_window) + + if !hidden || (buffer_len != 0 && buffer[0] == ':') { + nc.mvwprintw(app.input_window, 0, 0, strings.unsafe_string_to_cstring(output)) + nc.wmove(app.input_window, 0, i32(cursor_pos)) + } - case nc.KEY_RIGHT: - if cursor_pos != buffer_len { - cursor_pos += 1 + nc.wrefresh(app.input_window) + } + + nc.curs_set(0) + + nc.wclear(app.input_window) + + c_input := cstring(raw_data(buffer[:])) + + if len(c_input) == 0 && !allow_empty { + continue + } + + app_set_info_bar(app, "") + + return strings.clone_from_cstring(cstring(raw_data(buffer[:]))) + } +} + +Event :: enum { + up, + down, + select, + escape, +} + +Command_Or_Event :: union { + string, + Event, +} + +app_get_command_or_event :: proc(app: ^App, allocator := context.allocator) -> Command_Or_Event { + context.allocator = allocator + + for { + nc.wclear(app.input_window) + nc.wrefresh(app.input_window) + + buffer: [256]u8 + buffer_len, cursor_pos: int + + screen_height, screen_width := nc.getmaxyx(app.screen) + + nc.curs_set(1) + + for { + maybe_char, keep_going := handle_getch(app, &buffer_len, &cursor_pos) + + if !keep_going { + break + } + + char, ok := maybe_char.? + + if ok { + if buffer_len == 0 { + switch char { + case 'k': return Event.up + case 'j': return Event.down + case '\n': return Event.select + case 27: return Event.escape } + } - case 0o400..=0o777: + if char == 27 { + buffer_len = 0 + cursor_pos = 0 - case: - buffer_len += 1 + continue + } - mem.copy( - raw_data(buffer[cursor_pos + 1 : buffer_len]), - raw_data(buffer[cursor_pos : buffer_len - 1]), - int(buffer_len - cursor_pos - 1) - ) + if (buffer_len == 0 && char != ':') && buffer[0] != ':' { + continue + } - buffer[cursor_pos] = u8(char) + buffer_len += 1 - cursor_pos += 1 + mem.copy( + raw_data(buffer[cursor_pos + 1 : buffer_len]), + raw_data(buffer[cursor_pos : buffer_len - 1]), + int(buffer_len - cursor_pos - 1) + ) + + buffer[cursor_pos] = u8(char) + + cursor_pos += 1 } buffer[buffer_len] = 0 + output := string(buffer[:buffer_len + 1]) nc.wclear(app.input_window) - if !hidden || (buffer_len != 0 && buffer[0] == ':') { - nc.mvwprintw(app.input_window, 0, 0, strings.unsafe_string_to_cstring(output)) - nc.wmove(app.input_window, 0, i32(cursor_pos)) - } + nc.mvwprintw(app.input_window, 0, 0, strings.unsafe_string_to_cstring(output)) + nc.wmove(app.input_window, 0, i32(cursor_pos)) nc.wrefresh(app.input_window) } @@ -424,7 +537,7 @@ app_get_input :: proc( c_input := cstring(raw_data(buffer[:])) - if len(c_input) == 0 && !allow_empty { + if len(c_input) == 0 { continue } @@ -448,6 +561,18 @@ app_reset_seed_phrase :: proc(app: ^App) { app.seed_phrase_checksum = entropy_hash[0] & 0b1111 } +app_get_room_index :: proc(app: ^App, name: string) -> Maybe(int) { + for &room, i in app.profile.rooms { + if room_get_name(&room) != name { + continue + } + + return i + } + + return nil +} + app_add_room :: proc(app: ^App, room: Room) -> bool { room := room @@ -466,20 +591,22 @@ app_add_room :: proc(app: ^App, room: Room) -> bool { } app_remove_room :: proc(app: ^App, name: string) -> bool { - for &room, i in app.profile.rooms { - if room_get_name(&room) != name { - continue - } + room_index, ok := app_get_room_index(app, name).? - ordered_remove(&app.profile.rooms, i) + if !ok { + return false + } - profile_update(app.profile, app) - app_update_room_list_window(app) + ordered_remove(&app.profile.rooms, room_index) + profile_update(app.profile, app) - return true + if app.selected_room >= len(app.profile.rooms) { + app.selected_room = len(app.profile.rooms) - 1 } - return false + app_update_room_list_window(app) + + return true } handle_state :: proc(app: ^App) { @@ -583,22 +710,5 @@ main :: proc() { time.sleep(1_000_000) free_all(context.temp_allocator) } - - #partial switch app.state { - case .Main: - { - sync.mutex_lock(&app.mutex) - defer sync.mutex_unlock(&app.mutex) - - if !app.connecting && app.connection_thread != nil { - connection_thread := app.connection_thread.? - - thread.destroy(connection_thread) - app.connection_thread = nil - } - } - - - } } } diff --git a/client-cli/profile.odin b/client-cli/profile.odin index 1b986bc..30ef646 100644 --- a/client-cli/profile.odin +++ b/client-cli/profile.odin @@ -10,11 +10,14 @@ import "core:mem" import "core:os" import "core:strings" import "core:sync" +import "core:unicode/utf8" import chacha "core:crypto/chacha20poly1305" import fpath "core:path/filepath" -ROOM_NAME_MAX_SIZE :: 128 +ROOM_NAME_MAX_LEN :: 30 +ROOM_NAME_MAX_SIZE :: ROOM_NAME_MAX_LEN * 4 + ROOM_KEY_SIZE :: 32 Room_Name :: small_array.Small_Array(ROOM_NAME_MAX_SIZE, u8) @@ -25,7 +28,9 @@ Room :: struct { } room_init :: proc(name: string, key: string) -> (Room, bool) { - if len(name) > ROOM_NAME_MAX_SIZE || len(key) != ROOM_KEY_SIZE { + _, name_len, _ := utf8.grapheme_count(name) + + if name_len > ROOM_NAME_MAX_LEN || len(key) != ROOM_KEY_SIZE { return {}, false } @@ -159,6 +164,7 @@ profile_update :: proc(profile: Profile, app: ^App) { if err1 != nil { log.errorf("Failed to open profile file with error: %v", err1) app_set_info_bar(app, "Failed to save profile.") + return } @@ -170,6 +176,7 @@ profile_update :: proc(profile: Profile, app: ^App) { if err2 != nil { log.errorf("Failed to marshal profile with error: %v", err2) app_set_info_bar(app, "Failed to save profile.") + return } @@ -193,6 +200,7 @@ profile_update :: proc(profile: Profile, app: ^App) { if rand.read(iv[:]) < len(iv) { log.error("Failed to generate iv.") app_set_info_bar(app, "Failed to save profile.") + return } @@ -205,6 +213,7 @@ profile_update :: proc(profile: Profile, app: ^App) { if err3 != nil { log.errorf("Failed to write profile to file with error: %v", err3) app_set_info_bar(app, "Failed to save profile.") + return } } diff --git a/client-cli/state.odin b/client-cli/state.odin index d3f427e..25a2ffb 100644 --- a/client-cli/state.odin +++ b/client-cli/state.odin @@ -382,22 +382,8 @@ state_ask_profile_confirm_password :: proc(app: ^App) { app_set_state(app, .Connect_To_Host) } -handle_connection_to_host :: proc(app: ^App) { - { - sync.mutex_lock(&app.mutex) - defer sync.mutex_unlock(&app.mutex) - - app.connecting = true - } - - defer { - sync.mutex_lock(&app.mutex) - defer sync.mutex_unlock(&app.mutex) - - app.connecting = false - } - - app_set_info_bar(app, "Connecting to host...") +state_connect_to_host :: proc(app: ^App) { + app_set_info_bar(app, "Connecting to host..") host_endpoint: net.Endpoint @@ -414,7 +400,9 @@ handle_connection_to_host :: proc(app: ^App) { if err != nil { log.errorf("Failed to resolve host with error: %v", err) + app_set_info_bar(app, "Connection failed.") app_set_state(app, .Invalid_Host) + return } } @@ -427,7 +415,9 @@ handle_connection_to_host :: proc(app: ^App) { if err != nil { log.errorf("Failed to connect to host with error: %v", err) + app_set_info_bar(app, "Connection failed.") app_set_state(app, .Invalid_Host) + return } @@ -439,17 +429,6 @@ handle_connection_to_host :: proc(app: ^App) { app.host = host } -} - -state_connect_to_host :: proc(app: ^App) { - app_set_info_bar(app, "Connecting to host..") - - { - sync.mutex_lock(&app.mutex) - defer sync.mutex_unlock(&app.mutex) - - app.connection_thread = thread.create_and_start_with_poly_data(app, handle_connection_to_host, context) - } app_set_state(app, .Main) } @@ -459,7 +438,7 @@ state_invalid_host :: proc(app: ^App) { app, { "Failed to connect to host.", - "Enter either a new host,", + "Either enter a new host,", "or :retry." } ) @@ -483,18 +462,50 @@ state_invalid_host :: proc(app: ^App) { app.profile.host = input } + profile_update(app.profile, app) + app_set_state(app, .Connect_To_Host) } state_main :: proc(app: ^App) { - input := app_get_input(app, only_command = true) + command_or_event := app_get_command_or_event(app) - if err, ok := handle_command(app, input).?; ok { - if err != .Not_A_Command { + defer { + #partial switch thing in command_or_event { + case string: delete(thing) + } + } + + switch thing in command_or_event{ + case string: + command := thing + + if err, ok := handle_command(app, command).?; ok { + if err != .Not_A_Command { + return + } + } else { return } - } else { - delete(input) - return + + case Event: + event := thing + + switch event { + case Event.up: + if app.selected_room != 0 { + app.selected_room -= 1 + app_update_room_list_window(app) + } + + case Event.down: + if app.selected_room + 1 != len(app.profile.rooms) { + app.selected_room += 1 + app_update_room_list_window(app) + } + + case Event.select: + case Event.escape: + } } } diff --git a/ncurses/window.odin b/ncurses/window.odin index 287fcab..f0d8c84 100644 --- a/ncurses/window.odin +++ b/ncurses/window.odin @@ -112,6 +112,8 @@ foreign ncurses { // Resize the window. If either dimension is larger than the current values, the window's data is filled with blanks that have the current background rendition (as set by wbkgndset) merged into them. wresize :: proc(win: ^Window, h, w: c.int) -> c.int --- + + nodelay :: proc(win: ^Window, rf: bool) -> c.int --- } // Get the current coordinates of the cursor. diff --git a/server/server b/server/server index f9dce45..ee7f062 100755 Binary files a/server/server and b/server/server differ