]> jxnshi.xyz Git - mesange.git/commitdiff
Update
authorjxnshi <jxnshi@cock.li>
Wed, 1 Jan 2025 19:52:05 +0000 (20:52 +0100)
committerjxnshi <jxnshi@cock.li>
Wed, 1 Jan 2025 19:52:05 +0000 (20:52 +0100)
client-cli/client-cli
client-cli/command.odin [new file with mode: 0644]
client-cli/main.odin
client-cli/ncurses [deleted submodule]
client-cli/state.odin
client-cli/util.odin [new file with mode: 0644]

index 3c95ce5b03593e549d055183996ae2fdaac4bf9c..7fdf5e61620046cd7b0b42ca8fb8f01b886fa15b 100755 (executable)
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
new file mode 100644 (file)
index 0000000..a9e076f
--- /dev/null
@@ -0,0 +1,30 @@
+package main
+
+import "core:os"
+import "core:sync"
+
+import fpath "core:path/filepath"
+
+handle_command :: proc(app: ^App, command: string) -> bool {
+    sync.mutex_lock(&app.mutex)
+    defer sync.mutex_unlock(&app.mutex)
+
+    // State specific commands.
+    #partial switch app.state {
+    case .Ask_Config:
+        switch command {
+        case ":r",   ":retry":
+        case ":del", ":delete":
+            config_path := fpath.join({ app.storage_path, "config.json" }, context.temp_allocator)
+            os.remove_directory(config_path)
+        }
+    }
+
+    // General commands.
+    switch command {
+    case ":q", ":quit": app.running = false
+    case: return false
+    }
+
+    return true
+}
index e06d47d3a15caa91bdb0cc1ef68bc61f2d9a74b3..2b6a6298fe1210d9e57b8edd05f76f749ce6230d 100644 (file)
@@ -8,12 +8,17 @@ import "core:log"
 import "core:net"
 import "core:os"
 import "core:strings"
+import "core:sync"
+import "core:thread"
 
 import fpath "core:path/filepath"
 
-import nc "ncurses"
+import nc "../ncurses"
 
-import "../common"
+COLOR_FIRST :: 16
+COLOR_SECOND :: 17
+COLOR_THIRD :: 18
+COLOR_FOURTH :: 19
 
 Config :: struct {
     selected_profile: Maybe(string),
@@ -28,7 +33,7 @@ config_deinit :: proc(config: Config) {
 config_update :: proc(config: Config) {
     home_path := os.get_env("HOME")
 
-    storage_path := fpath.join({ home_path, "mesange-cli" }, context.temp_allocator)
+    storage_path := fpath.join({ home_path, "mesange" }, context.temp_allocator)
     config_path := fpath.join({ storage_path, "config.json" }, context.temp_allocator)
 
     config_file, err1 := os.open(config_path, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0777)
@@ -60,12 +65,16 @@ Profile :: struct {
 }
 
 App :: struct {
+    mutex: sync.Mutex,
+    running: bool,
+
     screen: ^nc.Window,
     storage_path: string,
     state: State,
 
     box_message: Maybe(^nc.Window),
     info_bar: ^nc.Window,
+    input_window: ^nc.Window,
     
     config: Config,
     profile_password: Maybe(string),
@@ -75,15 +84,27 @@ App :: struct {
 app_init :: proc() -> App {
     screen := nc.initscr()
 
+    nc.cbreak()
        nc.keypad(screen, true)
        nc.curs_set(0)
 
+       nc.use_default_colors()
+       nc.start_color()
+
+       nc.init_color(COLOR_FIRST, 800, 800, 800)
+       nc.init_color(COLOR_SECOND, 500, 500, 500)
+       nc.init_color(COLOR_THIRD, 200, 200, 200)
+       nc.init_color(COLOR_FOURTH, 000, 000, 000)
+
+       nc.init_pair(1, COLOR_FIRST, COLOR_THIRD)
+       nc.init_pair(2, COLOR_FIRST, COLOR_FOURTH)
+
        screen_height, screen_width := nc.getmaxyx(screen)
 
-       info_bar := nc.newwin(0, 0, 3, screen_width)
+       info_bar := nc.newwin(1, screen_width, screen_height - 2, 0)
+       input_window := nc.newwin(1, screen_width, screen_height - 1, 0)
 
-       nc.box(info_bar, 0, 0)
-       nc.wrefresh(info_bar)
+       nc.refresh()
 
     home_path := os.get_env("HOME")
     storage_path := fpath.join({ home_path, "mesange" })
@@ -97,15 +118,25 @@ app_init :: proc() -> App {
         }
     }
 
-    return {
+    app := App{
+        running = true,
+
         screen = screen,
         storage_path = storage_path,
+
+        info_bar = info_bar,
+        input_window = input_window,
     }
+
+    app_clear_info_bar(app)
+
+    return app
 }
 
 app_deinit :: proc(app: App) {
     config_deinit(app.config)
 
+    nc.delwin(app.input_window)
     nc.delwin(app.info_bar)
 
     if box_message, ok := app.box_message.?; ok {
@@ -121,11 +152,21 @@ app_set_state :: proc(app: ^App, state: State) {
     nc.clear()
     nc.refresh()
 
+    app_clear_info_bar(app^)
+
     app.state = state
 }
 
 app_set_info_bar :: proc(app: ^App, content: string) {
-    
+    app_clear_info_bar(app^)
+
+    c_content := strings.clone_to_cstring(content, context.temp_allocator)
+
+    nc.wattron(app.info_bar, nc.COLOR_PAIR(1))
+    nc.wprintw(app.info_bar, c_content)
+    nc.wattroff(app.info_bar, nc.COLOR_PAIR(1))
+
+    nc.wrefresh(app.info_bar)
 }
 
 app_set_box_message :: proc(app: ^App, lines: []string) {
@@ -150,19 +191,38 @@ app_set_box_message :: proc(app: ^App, lines: []string) {
         (screen_width - width) / 2,
     )
 
+    color := nc.COLOR_PAIR(2)
+
+    nc.wattron(box_message, color)
+
     for line, i in lines {
         c_line := strings.clone_to_cstring(line, context.temp_allocator)
         nc.mvwprintw(box_message, i32(i + 1), 1, c_line)
     }
 
     nc.box(box_message, 0, 0)
+    nc.wattroff(box_message, color)
+
     nc.wrefresh(box_message)
 
+    sync.mutex_lock(&app.mutex)
     app.box_message = box_message
+    sync.mutex_unlock(&app.mutex)
 }
 
 app_clear_info_bar :: proc(app: App) {
-    
+    height, width := nc.getmaxyx(app.info_bar)
+    color := nc.COLOR_PAIR(1)
+
+    nc.wclear(app.info_bar)
+    nc.wattron(app.info_bar, color)
+
+    for _ in 0..<width {
+        nc.waddch(app.info_bar, ' ')
+    }
+
+    nc.wattroff(app.info_bar, color)
+       nc.wrefresh(app.info_bar)
 }
 
 app_clear_box_message :: proc(app: ^App) {
@@ -178,30 +238,42 @@ app_clear_box_message :: proc(app: ^App) {
 app_get_input :: proc(app: App, allocator := context.allocator) -> string {
     context.allocator = allocator
 
+    nc.wclear(app.input_window)
+    nc.wrefresh(app.input_window)
+
     buffer: [256]u8
 
     screen_height, screen_width := nc.getmaxyx(app.screen)
 
        nc.curs_set(1)
-    nc.mvgetnstr(screen_height - 1, 0, raw_data(buffer[:]), len(buffer))
+    nc.wgetnstr(app.input_window, raw_data(buffer[:]), len(buffer))
        nc.curs_set(0)
 
     return strings.clone_from_cstring(cstring(raw_data(&buffer)))
 }
 
+handle_state :: proc(app: ^App) {
+    for {
+        switch app.state {
+        case .Load_Config: state_load_config(app)
+        case .Ask_Config: state_ask_config(app)
+
+        case .Load_Profile: state_load_profile(app)
+        case .Ask_Profile: state_ask_profile(app)
+        case .Ask_Profile_Password: state_ask_profile_password(app)
+        case .Ask_Profile_Seed_Phrase: state_ask_profile_seed_phrase(app)
+        }
+    }
+}
+
 main :: proc() {
     app := app_init()
     defer app_deinit(app)
 
-    for {
-        switch app.state {
-        case .Load_Config: state_load_config(&app)
-        case .Ask_Config: state_ask_config(&app)
+    handle_state_thread := thread.create_and_start_with_poly_data(&app, handle_state, context)
+    defer thread.terminate(handle_state_thread, 0)
+
+    for app.running {
 
-        case .Load_Profile: state_load_profile(&app)
-        case .Ask_Profile: state_ask_profile(&app)
-        case .Ask_Profile_Password: state_ask_profile_password(&app)
-        case .Ask_Profile_Seed_Phrase: state_ask_profile_seed_phrase(&app)
-        }
     }
 }
diff --git a/client-cli/ncurses b/client-cli/ncurses
deleted file mode 160000 (submodule)
index 8712a2b..0000000
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 8712a2b7a368771baf35ce0e860b577c3f87052d
index 1cf3dde527c17c1c7947db4ddfbe0defb783749e..86ea699c521544a8a12dc08ad2fb7410534e99ff 100644 (file)
@@ -50,13 +50,13 @@ state_ask_config :: proc(app: ^App) {
 
     input := app_get_input(app^, context.temp_allocator)
 
-    switch input {
-    case ":retry":
-    case ":delete":
-        config_path := fpath.join({ app.storage_path, "config.json" }, context.temp_allocator)
-        os.remove_directory(config_path)
-
-    case:
+    if input[0] == ':' {
+        if !handle_command(app, input) {
+            app_set_info_bar(app, "Invalid command.")
+            return
+        }
+    } else {
+        app_set_info_bar(app, "Invalid input.")
         return
     }
 
@@ -100,10 +100,22 @@ state_ask_profile :: proc(app: ^App) {
 
     input := app_get_input(app^)
 
+    if input[0] == ':' {
+        if !handle_command(app, input) {
+            app_set_info_bar(app, "Invalid command.")
+            return
+        }
+    }
+
     if len(input) == 0 {
         return
     }
 
+    if !is_identifier(input) {
+        app_set_info_bar(app, "Invalid profile name.")
+        return
+    }
+
     app.config.selected_profile = app_get_input(app^)
 
     config_update(app.config)
diff --git a/client-cli/util.odin b/client-cli/util.odin
new file mode 100644 (file)
index 0000000..009a89d
--- /dev/null
@@ -0,0 +1,39 @@
+package main
+
+is_alphabetic :: proc(char: u8) -> bool {
+    switch char {
+        case 'A'..='Z', 'a'..='z': return true
+        case: return false
+    }
+}
+
+is_alphanumeric :: proc(char: u8) -> bool {
+    if is_alphabetic(char) {
+        return true
+    }
+
+    switch char {
+        case '0'..='1': return true
+        case: return false
+    }
+}
+
+is_identifier :: proc(input: string) -> bool {
+    if len(input) == 0 {
+        return false
+    }
+
+    if !is_alphabetic(input[0]) && input[0] != '_' {
+        return false
+    }
+
+    for char in input {
+        char := u8(char)
+
+        if !is_alphanumeric(char) && char != '_' {
+            return false
+        }
+    }
+
+    return true
+}