]> jxnshi.xyz Git - mesange.git/commitdiff
Add room navigation
authorjxnshi <jxnshi@cock.li>
Fri, 17 Jan 2025 00:22:10 +0000 (01:22 +0100)
committerjxnshi <jxnshi@cock.li>
Fri, 17 Jan 2025 00:22:10 +0000 (01:22 +0100)
client-cli/client-cli
client-cli/command.odin
client-cli/main.odin
client-cli/profile.odin
client-cli/state.odin
ncurses/window.odin
server/server

index 0f0eb987e19c9bba1df4d64cd523f5afa24dbd66..63faac56ca9366faace229754610c73b165e839a 100755 (executable)
Binary files a/client-cli/client-cli and b/client-cli/client-cli differ
index 0c871257a5bf9ad7eed1c340081582bc93a21bed..d3bc000db17896a586e814fb32949195f8f27bcc 100644 (file)
@@ -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 {
index 0df2174c0693f9418a2020715fe56bdb85d2e359..92d73d24155735318956222dc3bca908b94a12e7 100644 (file)
@@ -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
-                }
-            }
-
-            
-        }
     }
 }
index 1b986bcfdf79957b94e7805d916f38f35701cec7..30ef64674c43d8c0cacda0dd06fca6563c9796a6 100644 (file)
@@ -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
     }
 }
index d3f427e6e78f31a9dd4203ef8c3345ab2f60714c..25a2ffb20797ff38bfe1ae7f8f89910c83dbc4f1 100644 (file)
@@ -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:
+        }
     }
 }
index 287fcabe023b6fbdf954a41a83eb45655498c578..f0d8c844aefb480eb1ea68a9c0a60550faf269bb 100644 (file)
@@ -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.
index f9dce45af6a5a1e7f96827a96c55f40ecdfd09c0..ee7f0622c53f823c886b6f5d45c8e028a96420a3 100755 (executable)
Binary files a/server/server and b/server/server differ