]> jxnshi.xyz Git - mesange.git/commitdiff
Working on profiles
authorjxnshi <jxnshi@cock.li>
Thu, 2 Jan 2025 01:55:49 +0000 (02:55 +0100)
committerjxnshi <jxnshi@cock.li>
Thu, 2 Jan 2025 01:55:49 +0000 (02:55 +0100)
client-cli/client-cli
client-cli/command.odin
client-cli/main.odin
client-cli/state.odin
ncurses/attributes.odin [new file with mode: 0644]
ncurses/io.odin [new file with mode: 0644]
ncurses/ncurses.odin [new file with mode: 0644]
ncurses/window.odin [new file with mode: 0644]

index 7fdf5e61620046cd7b0b42ca8fb8f01b886fa15b..47655fcbc5c844bf7325104921c626a50279e05d 100755 (executable)
Binary files a/client-cli/client-cli and b/client-cli/client-cli differ
index a9e076ffb44528f68b724eb2d031536c9c141985..bd17fb080939af1ded8717e03af0fd100c7bbd45 100644 (file)
@@ -11,7 +11,7 @@ handle_command :: proc(app: ^App, command: string) -> bool {
 
     // State specific commands.
     #partial switch app.state {
-    case .Ask_Config:
+    case .Invalid_Config:
         switch command {
         case ":r",   ":retry":
         case ":del", ":delete":
index 2602eb86ff01603babde6e6713211e08870bb456..bd9fd59c2e247c4c91404778bc6734589d8a0b3d 100644 (file)
@@ -1,15 +1,18 @@
 package main
 
+import "core:crypto/aes"
 import "core:crypto/ed25519"
 import "core:encoding/csv"
 import "core:encoding/json"
 import "core:fmt"
 import "core:log"
+import "core:math/rand"
 import "core:net"
 import "core:os"
 import "core:strings"
 import "core:sync"
 import "core:thread"
+import "core:time"
 
 import fpath "core:path/filepath"
 
@@ -20,9 +23,12 @@ COLOR_SECOND :: 17
 COLOR_THIRD :: 18
 COLOR_FOURTH :: 19
 
-COLOR_FIRST :: 16
-COLOR_SECOND :: 17
-COLOR_THIRD :: 18
+json_marshal_options := json.Marshal_Options{
+    spec = .JSON,
+    pretty = true,
+    use_spaces = true,
+    use_enum_names = true,
+}
 
 Config :: struct {
     selected_profile: Maybe(string),
@@ -34,33 +40,21 @@ config_deinit :: proc(config: Config) {
     }
 }
 
-config_update :: proc(config: Config) {
-    home_path := os.get_env("HOME")
-
-    storage_path := fpath.join({ home_path, "mesange" }, context.temp_allocator)
-    config_path := fpath.join({ storage_path, "config.json" }, context.temp_allocator)
-
+config_update :: proc(config: Config, app: App) {
+    config_path := fpath.join({ app.storage_path, "config.json" }, context.temp_allocator)
     config_file, err1 := os.open(config_path, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0777)
 
     if err1 != nil {
-        fmt.eprintln("Failed to open config file with error: .", err1)
-        os.exit(1)
+        app_set_info_bar(app, "Failed to save config.")
+        return
     }
 
     config_file_stream := os.stream_from_handle(config_file)
-
-    marshal_options := json.Marshal_Options{
-        spec = .JSON,
-        pretty = true,
-        use_spaces = true,
-        use_enum_names = true,
-    }
-
-    err2 := json.marshal_to_writer(config_file_stream, config, &marshal_options)
+    err2 := json.marshal_to_writer(config_file_stream, config, &json_marshal_options)
 
     if err2 != nil {
-        fmt.eprintln("Failed to marshal config.")
-        os.exit(1)
+        app_set_info_bar(app, "Failed to save config.")
+        return
     }
 }
 
@@ -68,6 +62,112 @@ Profile :: struct {
     private_key: ed25519.Private_Key,
 }
 
+profile_load_from_name :: proc(name: string, app: App) -> (Profile, bool) {
+    buffer: [4096]u8
+    profile_content: string
+
+    // Loads encrypted.
+    if profile_password, ok := app.profile_password.?; ok {
+        encrypted_profile_path := fpath.join({ app.storage_path, selected_profile }, context.temp_allocator)
+        encrypted_profile_content, profile_read := os.read_entire_file_from_filename(encrypted_profile_path, context.temp_allocator)
+
+        if !profile_read {
+            return {}, false
+        }
+
+        ecb_context: aes.Context_ECB
+        aes.init_ecb(&ecb_context, app.profile_password.?)
+
+        iv := encrypted_profile_content[:aes.GCM_IV_SIZE]
+        tag := encrypted_profile_content[len(iv):][:aes.GCM_TAG_SIZE]
+        ciphertext := encrypted_profile_content[len(iv) + len(tag):]
+
+        if !aes.decrypt(&ecb_context, buffer[:], iv, {}, ciphertext, tag) {
+            app_set_info_bar(app, "Invalid password.")
+            app_set_state(app, .Ask_Profile_Password)
+            return
+        }
+
+        profile_content = string(buffer[:len(ciphertext)])
+    }
+    
+    // Loads clear text.
+    else {
+        profile_filename := strings.concatenate(app.config.selected_profile.?, ".json", context.temp_allocator)
+        profile_path = fpath.join({ app.storage_path, profile_filename }, context.temp_allocator)
+
+        profile_content, profile_read := os.read_entire_file_from_filename(profile_path, context.temp_allocator)
+
+        if !profile_read {
+            return {}, false
+        }
+    }
+}
+
+profile_update :: proc(profile: Profile, app: App) {
+    profile_path: string
+
+    if _, ok := app.profile_password.?; ok {
+        profile_path = fpath.join({ app.storage_path, app.config.selected_profile.? }, context.temp_allocator)
+    } else {
+        profile_filename := strings.concatenate(app.config.selected_profile.?, ".json", context.temp_allocator)
+        profile_path = fpath.join({ app.storage_path, profile_filename }, context.temp_allocator)
+    }
+
+    profile_file, err1 := os.open(profile_path, os.O_WRONLY, | os.O_CREATE | os.O_TRUNC, 0777)
+
+    if err1 != nil {
+        app_set_info_bar(app, "Failed to save profile.")
+        return
+    }
+
+    // Saves encrypted.
+    if profile_password, ok := app.profile_password.?; ok {
+        json_string_builder: strings.Builder
+        strings.builder_init_none(&json_string_builder, context.temp_allocator)
+
+        err2 := json.marshal_to_builder(&json_string_builder, profile, &json_marshal_options)
+
+        if err2 != nil {
+            app_set_info_bar(app, "Failed to save profile.")
+            return
+        }
+
+        json_string := strings.to_string(json_string_builder)
+
+        aes_context: aes.Context_GCM
+        aes.init_gcm(&aes_context, profile_password)
+
+        buffer: [4096]u8
+        iv := buffer[:aes.GCM_IV_SIZE]
+        tag := buffer[len(iv):][:aes.GCM_TAG_SIZE]
+
+        rand.read(iv[:])
+
+        aes.seal_gcm(&ecb_context, buffer[len(iv) + len(tag):], tag, iv, {}, transmute([]u8)json_string)
+
+        content := string(buffer[:len(iv) + len(tag) + len(json_string)])
+
+        _, err3 := os.write_string(profile_file, content)
+
+        if err3 != nil {
+            app_set_info_bar(app, "Failed to save profile.")
+            return
+        }
+    }
+    
+    // Saves clear text.
+    else {
+        profile_file_stream := os.stream_from_handle(profile_file)
+        err2 := json.marshal_to_writer(profile_file_stream, config, &json_marshal_options)
+
+        if err2 != nil {
+            app_set_info_bar(app, "Failed to save profile.")
+            return
+        }
+    }
+}
+
 App :: struct {
     mutex: sync.Mutex,
     running: bool,
@@ -79,7 +179,7 @@ App :: struct {
     box_message: Maybe(^nc.Window),
     info_bar: ^nc.Window,
     input_window: ^nc.Window,
-    
+
     config: Config,
     profile_password: Maybe(string),
     profile: Profile,
@@ -97,17 +197,11 @@ app_init :: proc() -> App {
 
        nc.init_color(COLOR_FIRST, 800, 800, 800)
        nc.init_color(COLOR_SECOND, 500, 500, 500)
-<<<<<<< HEAD
        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)
-=======
-       nc.init_color(COLOR_THIRD, 100, 100, 100)
-
-       nc.init_pair(1, nc.COLOR_WHITE, COLOR_THIRD)
->>>>>>> refs/remotes/origin/master
 
        screen_height, screen_width := nc.getmaxyx(screen)
 
@@ -129,11 +223,8 @@ app_init :: proc() -> App {
     }
 
     app := App{
-<<<<<<< HEAD
         running = true,
 
-=======
->>>>>>> refs/remotes/origin/master
         screen = screen,
         storage_path = storage_path,
 
@@ -141,11 +232,8 @@ app_init :: proc() -> App {
         input_window = input_window,
     }
 
-<<<<<<< HEAD
     app_clear_info_bar(app)
-=======
     app_set_state(&app, .Load_Config)
->>>>>>> refs/remotes/origin/master
 
     return app
 }
@@ -167,17 +255,13 @@ app_set_state :: proc(app: ^App, state: State) {
     nc.clear()
     nc.refresh()
 
-<<<<<<< HEAD
-=======
     app_clear_box_message(app)
->>>>>>> refs/remotes/origin/master
     app_clear_info_bar(app^)
 
     app.state = state
 }
 
 app_set_info_bar :: proc(app: ^App, content: string) {
-<<<<<<< HEAD
     app_clear_info_bar(app^)
 
     c_content := strings.clone_to_cstring(content, context.temp_allocator)
@@ -187,15 +271,6 @@ app_set_info_bar :: proc(app: ^App, content: string) {
     nc.wattroff(app.info_bar, nc.COLOR_PAIR(1))
 
     nc.wrefresh(app.info_bar)
-=======
-    c_content := strings.clone_to_cstring(content, context.temp_allocator)
-
-    color := nc.COLOR_PAIR(1)
-
-    nc.wattron(app.info_bar, color)
-    nc.wprintw(app.info_bar, c_content)
-    nc.wattroff(app.info_bar, color)
->>>>>>> refs/remotes/origin/master
 }
 
 app_set_box_message :: proc(app: ^App, lines: []string) {
@@ -220,15 +295,9 @@ app_set_box_message :: proc(app: ^App, lines: []string) {
         (screen_width - width) / 2,
     )
 
-<<<<<<< HEAD
     color := nc.COLOR_PAIR(2)
 
     nc.wattron(box_message, color)
-=======
-    nc.refresh()
-
-    nc.wattron(box_message, COLOR_FIRST)
->>>>>>> refs/remotes/origin/master
 
     for line, i in lines {
         c_line := strings.clone_to_cstring(line, context.temp_allocator)
@@ -236,11 +305,7 @@ app_set_box_message :: proc(app: ^App, lines: []string) {
     }
 
     nc.box(box_message, 0, 0)
-<<<<<<< HEAD
     nc.wattroff(box_message, color)
-=======
-    nc.wattroff(box_message, COLOR_FIRST)
->>>>>>> refs/remotes/origin/master
 
     nc.wrefresh(box_message)
 
@@ -251,7 +316,6 @@ app_set_box_message :: proc(app: ^App, lines: []string) {
 
 app_clear_info_bar :: proc(app: App) {
     height, width := nc.getmaxyx(app.info_bar)
-<<<<<<< HEAD
     color := nc.COLOR_PAIR(1)
 
     nc.wclear(app.info_bar)
@@ -262,20 +326,6 @@ app_clear_info_bar :: proc(app: App) {
     }
 
     nc.wattroff(app.info_bar, color)
-=======
-
-       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)
->>>>>>> refs/remotes/origin/master
        nc.wrefresh(app.info_bar)
 }
 
@@ -312,7 +362,7 @@ handle_state :: proc(app: ^App) {
     for {
         switch app.state {
         case .Load_Config: state_load_config(app)
-        case .Ask_Config: state_ask_config(app)
+        case .Invalid_Config: state_invalid_config(app)
 
         case .Load_Profile: state_load_profile(app)
         case .Ask_Profile: state_ask_profile(app)
@@ -323,6 +373,10 @@ handle_state :: proc(app: ^App) {
 }
 
 main :: proc() {
+    now := transmute(u64)time.now()
+
+    context.random_generator := rand.create(now)
+
     app := app_init()
     defer app_deinit(app)
 
index 86ea699c521544a8a12dc08ad2fb7410534e99ff..eefad0e42f16248ea6964f5d212aaf4dbfe82879 100644 (file)
@@ -1,5 +1,6 @@
 package main
 
+import "core:crypto/aes"
 import "core:encoding/json"
 import "core:fmt"
 import "core:os"
@@ -9,7 +10,7 @@ import fpath "core:path/filepath"
 
 State :: enum {
     Load_Config,
-    Ask_Config,
+    Invalid_Config,
 
     Load_Profile,
     Ask_Profile,
@@ -27,7 +28,7 @@ state_load_config :: proc(app: ^App) {
         err := json.unmarshal(raw_config, &config)
 
         if err != nil {
-            app_set_state(app, .Ask_Config)
+            app_set_state(app, .Invalid_Config)
             return
         }
     } else {        
@@ -39,7 +40,7 @@ state_load_config :: proc(app: ^App) {
     app_set_state(app, .Load_Profile)
 }
 
-state_ask_config :: proc(app: ^App) {
+state_invalid_config :: proc(app: ^App) {
     app_set_box_message(
         app,
         {
@@ -78,13 +79,57 @@ state_load_profile :: proc(app: ^App) {
         return
     }
 
-    profile_content: string
+    profile, ok := profile_load_from_name(selected_profile, app)
+
+    if !ok {
+        app_set_info_bar("Could not load profile, creating one.")
+    }
+
+    load_profile: {
+        profile_content: string
+
+        load_profile_content: {
+            load_encrypted: {
+                encrypted_profile_content, profile_read := os.read_entire_file_from_filename(encrypted_profile_path, context.temp_allocator)
+
+                if !profile_read {
+                    break load_encrypted
+                }
+
+                ecb_context: aes.Context_ECB
+                aes.init_ecb(&ecb_context, app.profile_password.?)
+
+                tag := encrypted_profile_content[:aes.GCM_TAG_SIZE]
+
+                buffer: [4096]u8
+                iv := encrypted_profile_content[:aes.GCM_IV_SIZE]
+                tag := encrypted_profile_content[len(iv):][:aes.GCM_TAG_SIZE]
+                ciphertext := encrypted_profile_content[len(iv) + len(tag):]
+
+                if !aes.decrypt(&ecb_context, buffer[:], iv, {}, ciphertext, tag) {
+                    app_set_info_bar(app, "Invalid password.")
+                    app_set_state(app, .Ask_Profile_Password)
+                    return
+                }
+
+                profile_content = string(buffer[:len(ciphertext)])
+
+                break load_profile_content
+            }
+
+            load_unencrypted: {
+                profile_filename := strings.concatenate(app.config.selected_profile.?, ".json", context.temp_allocator)
+                profile_path = fpath.join({ app.storage_path, profile_filename }, context.temp_allocator)
+
+                profile_content, profile_read := os.read_entire_file_from_filename(profile_path, context.temp_allocator)
+
+                if !profile_read {
+                    break load_unencrypted
+                }
+            }
 
-    load_profile_content: {
-        encrypted_profile_content, profile_read := os.read_entire_file_from_filename(encrypted_profile_path)
 
-        if profile_read {
-            
+            break load_profile
         }
     }
 }
diff --git a/ncurses/attributes.odin b/ncurses/attributes.odin
new file mode 100644 (file)
index 0000000..9a04f5e
--- /dev/null
@@ -0,0 +1,120 @@
+package ncurses
+
+import "core:c"
+foreign import ncurses "system:ncurses"
+
+@(private)
+NCURSES_ATTR_SHIFT :: 8
+
+@(private = "file")
+ncurses_bits :: #force_inline proc(mask, shift: c.uint) -> c.int {
+       return c.int(mask << (shift + NCURSES_ATTR_SHIFT))
+}
+
+// -- Attributes
+
+A_NORMAL := c.int(c.uint(1) - c.uint(1))
+A_ATTRIBUTES := ncurses_bits(~(c.uint(1) - c.uint(1)), 0)
+A_CHARTEXT := ncurses_bits(1, 0) - 1
+A_COLOR := ncurses_bits((c.uint(1) << 8) - c.uint(1), 0)
+A_STANDOUT := ncurses_bits(1, 8)
+A_UNDERLINE := ncurses_bits(1, 9)
+A_REVERSE := ncurses_bits(1, 10)
+A_BLINK := ncurses_bits(1, 11)
+A_DIM := ncurses_bits(1, 12)
+A_BOLD := ncurses_bits(1, 13)
+A_ALTCHARSET := ncurses_bits(1, 14)
+A_INVIS := ncurses_bits(1, 15)
+A_PROTECT := ncurses_bits(1, 16)
+A_HORIZONTAL := ncurses_bits(1, 17)
+A_LEFT := ncurses_bits(1, 18)
+A_LOW := ncurses_bits(1, 19)
+A_RIGHT := ncurses_bits(1, 20)
+A_TOP := ncurses_bits(1, 21)
+A_VERTICAL := ncurses_bits(1, 22)
+A_ITALIC := ncurses_bits(1, 23) /* ncurses extension */
+
+// -- X/Open Attributes
+// They're equivalent to the A_ attributes on ncurses
+
+WA_ATTRIBUTES := A_ATTRIBUTES
+WA_NORMAL := A_NORMAL
+WA_STANDOUT := A_STANDOUT
+WA_UNDERLINE := A_UNDERLINE
+WA_REVERSE := A_REVERSE
+WA_BLINK := A_BLINK
+WA_DIM := A_DIM
+WA_BOLD := A_BOLD
+WA_ALTCHARSET := A_ALTCHARSET
+WA_INVIS := A_INVIS
+WA_PROTECT := A_PROTECT
+WA_HORIZONTAL := A_HORIZONTAL
+WA_LEFT := A_LEFT
+WA_LOW := A_LOW
+WA_RIGHT := A_RIGHT
+WA_TOP := A_TOP
+WA_VERTICAL := A_VERTICAL
+WA_ITALIC := A_ITALIC
+
+COLOR_BLACK   :: 0
+COLOR_RED     :: 1
+COLOR_GREEN   :: 2
+COLOR_YELLOW  :: 3
+COLOR_BLUE    :: 4
+COLOR_MAGENTA :: 5
+COLOR_CYAN    :: 6
+COLOR_WHITE   :: 7
+
+foreign ncurses {
+       // Maximum number of colors.
+       COLORS: c.int
+       // Maximum number of color-pairs.
+       COLORS_PAIRS: c.int
+
+       attron :: proc(attr: c.int) -> c.int ---
+       attroff :: proc(attr: c.int) -> c.int ---
+       attrset :: proc(attr: c.int) -> c.int ---
+       wattron :: proc(win: ^Window, attr: c.int) -> c.int ---
+       wattroff :: proc(win: ^Window, attr: c.int) -> c.int ---
+       wattrset :: proc(win: ^Window, attr: c.int) -> c.int ---
+
+       attr_on :: proc() --- // TODO
+       attr_off :: proc() --- // TODO
+       attr_set :: proc() --- // TODO
+       wattr_on :: proc() --- // TODO
+       wattr_off :: proc() --- // TODO
+       wattr_set :: proc() --- // TODO
+
+       // changes the definition of a color. It takes four arguments:
+       //  the number of the color to be changed followed by three RGB values (for the amounts of red, green, and blue components).
+       //
+       // The value of the first argument must be between 0 and COLORS. (See the section Colors for the default color index.)
+       // Each of the last three arguments must be a value between 0 and 1000. 
+       // When init_color is used, all occurrences of that color on the screen immediately change to the new definition. 
+       init_color :: proc(color, r, g, b: c.short) -> c.int ---
+       // Changes the definition of a color-pair. 
+       // It takes three arguments: the number of the color-pair to be changed, the foreground color number, and the background color number. 
+       // For portable applications:
+       //
+       // - The value of the first argument must be between 1 and COLOR_PAIRS-1, except that if default colors are used (see use_default_colors) 
+       // the upper limit is adjusted to allow for extra pairs which use a default color in foreground and/or background.
+       //
+       // - The value of the second and third arguments must be between 0 and COLORS. Color pair 0 is assumed to be white on black,
+       // but is actually whatever the terminal implements before color is initialized. It cannot be modified by the application.
+       // If the color-pair was previously initialized, the screen is refreshed and all occurrences of that color-pair are changed to the new definition.
+       //
+       // As an extension, ncurses allows you to set color pair 0 via the assume_default_colors routine, 
+       // or to specify the use of default colors (color number -1) if you first invoke the use_default_colors routine. 
+       init_pair :: proc(pair_id, fg, bg: c.short) -> c.int ---
+       has_colors :: proc() -> c.bool ---
+       // Must be called if the programmer wants to use colors, and before any other color manipulation routine is called. 
+       // It is good practice to call this routine right after initscr.
+       //
+       // start_color initializes eight basic colors (black, red, green, yellow, blue, magenta, cyan, and white), 
+       // and two global variables, COLORS and COLOR_PAIRS (respectively defining the maximum number of colors and color-pairs the terminal can support).
+       // It also restores the colors on the terminal to the values they had when the terminal was just turned on. 
+       start_color :: proc() -> c.int ---
+       use_default_colors :: proc() -> c.int ---
+       assume_default_colors :: proc(fg, bg: c.int) -> c.int ---
+       COLOR_PAIR :: proc(pair_id: c.int) -> c.int ---
+}
diff --git a/ncurses/io.odin b/ncurses/io.odin
new file mode 100644 (file)
index 0000000..b50394a
--- /dev/null
@@ -0,0 +1,264 @@
+package ncurses
+
+import "core:c"
+foreign import ncurses "system:ncurses"
+
+foreign ncurses {
+
+       // Enables the terminal's keypad. 
+       // If enabled, the user can press a function key (such as an arrow key) and wgetch returns a single value representing the function key, as in KEY_LEFT.
+       //
+       // If disabled, curses does not treat function keys specially and the program has to interpret the escape sequences itself.
+       //
+       // If the keypad in the terminal can be turned on (made to transmit) and off (made to work locally), turning on this option causes the terminal keypad to be turned on when wgetch is called.
+       // The default value for keypad is false. 
+       keypad :: proc(win: ^Window, enable: bool) -> c.int ---
+
+       // --- INPUT
+
+       getch :: proc() -> c.int ---
+       wgetch :: proc(win: ^Window) -> c.int ---
+       mvgetch :: proc(y, x: c.int) -> c.int ---
+       mvwgetch :: proc(win: ^Window, y, x: c.int) -> c.int ---
+
+       getstr :: proc(str: [^]byte) -> c.int ---
+       wgetstr :: proc(win: ^Window, str: [^]byte) -> c.int ---
+       getnstr :: proc(str: [^]byte, n: c.int) -> c.int ---
+       mvgetstr :: proc(y, x: c.int, str: [^]byte) -> c.int ---
+       mvwgetstr :: proc(win: ^Window, y, x: c.int, str: [^]byte) -> c.int ---
+       mvgetnstr :: proc(y, x: c.int, str: [^]byte, n: c.int) -> c.int ---
+       mvwgetnstr :: proc(win: ^Window, y, x: c.int, str: [^]byte, n: c.int) -> c.int ---
+       wgetnstr :: proc(win: ^Window, str: [^]byte, n: c.int) -> c.int ---
+
+       ungetch :: proc(char: c.int) -> c.int ---
+
+       scanw :: proc(fmt: cstring, #c_vararg args: ..any) -> c.int ---
+       wscanw :: proc(win: ^Window, fmt: cstring, #c_vararg args: ..any) -> c.int ---
+       mvscanw :: proc(y, x: c.int, fmt: cstring, #c_vararg args: ..any) -> c.int ---
+       mvwscanw :: proc(win: ^Window, y, x: c.int, fmt: cstring, #c_vararg args: ..any) -> c.int ---
+
+
+       // --- OUTPUT 
+
+       printw :: proc(fmt: cstring, #c_vararg args: ..any) -> c.int ---
+       wprintw :: proc(win: ^Window, fmt: cstring, #c_vararg args: ..any) -> c.int ---
+       mvprintw :: proc(y, x: c.int, fmt: cstring, #c_vararg args: ..any) -> c.int ---
+       mvwprintw :: proc(win: ^Window, y, x: c.int, fmt: cstring, #c_vararg args: ..any) -> c.int ---
+
+       addch :: proc(char: c.uint) -> c.int ---
+       waddch :: proc(win: ^Window, char: c.uint) -> c.int ---
+       mvaddch :: proc(y, x: c.int, char: c.uint) -> c.int ---
+       mvwaddch :: proc(win: ^Window, y, x: c.int, char: c.uint) -> c.int ---
+
+       addstr :: proc(str: cstring) -> c.int ---
+       waddstr :: proc(win: ^Window, str: cstring) -> c.int ---
+       mvaddstr :: proc(y, x: c.int, str: cstring) -> c.int ---
+       mvwaddstr :: proc(win: ^Window, y, x: c.int, str: cstring) -> c.int ---
+
+       move :: proc(y, x: c.int) -> c.int ---
+       wmove :: proc(win: ^Window, y, x: c.int) ---
+
+       // Returns a character string corresponding to the key c:
+       //
+    // - Printable characters are displayed as themselves, e.g., a one-character string containing the key.
+    // - Control characters are displayed in the ^X notation.
+    // - DEL (character 127) is displayed as ^?.
+    // - Values above 128 are either meta characters (if the screen has not been initialized, or if meta has been called with a TRUE parameter), shown in the M-X notation, or are displayed as themselves. In the latter case, the values may not be printable; this follows the X/Open specification.
+    // - Values above 256 may be the names of the names of function keys.
+    // - Otherwise (if there is no corresponding name) the function returns null, to denote an error. X/Open also lists an "UNKNOWN KEY" return value, which some implementations return rather than null. 
+       keyname :: proc(c: c.int) -> cstring ---
+}
+
+// down-arrow key 
+KEY_DOWN      :: 0o402
+// up-arrow key 
+KEY_UP        :: 0o403
+// left-arrow key 
+KEY_LEFT      :: 0o404
+// right-arrow key 
+KEY_RIGHT     :: 0o405
+// home key 
+KEY_HOME      :: 0o406
+// backspace key 
+KEY_BACKSPACE :: 0o407
+// Function keys.  Space for 64 
+KEY_F0        :: 0o410
+KEY_F1        :: 0o411
+KEY_F2        :: 0o412
+KEY_F3        :: 0o413
+KEY_F4        :: 0o414
+KEY_F5        :: 0o415
+KEY_F6        :: 0o416
+KEY_F7        :: 0o417
+KEY_F8        :: 0o420
+KEY_F9        :: 0o421
+KEY_F10       :: 0o422
+KEY_F11       :: 0o423
+KEY_F12       :: 0o424
+KEY_F :: #force_inline proc(n: c.int) -> c.int {
+       return c.int(KEY_F0) + n
+}
+// delete-line key 
+KEY_DL        :: 0o510
+// insert-line key 
+KEY_IL        :: 0o511
+// delete-character key 
+KEY_DC        :: 0o512
+// insert-character key 
+KEY_IC        :: 0o513
+// sent by rmir or smir in insert mode 
+KEY_EIC       :: 0o514
+// clear-screen or erase key 
+KEY_CLEAR     :: 0o515
+// clear-to-end-of-screen key 
+KEY_EOS       :: 0o516
+// clear-to-end-of-line key 
+KEY_EOL       :: 0o517
+// scroll-forward key 
+KEY_SF        :: 0o520
+// scroll-backward key 
+KEY_SR        :: 0o521
+// next-page key 
+KEY_NPAGE     :: 0o522
+// previous-page key 
+KEY_PPAGE     :: 0o523
+// set-tab key 
+KEY_STAB      :: 0o524
+// clear-tab key 
+KEY_CTAB      :: 0o525
+// clear-all-tabs key 
+KEY_CATAB     :: 0o526
+// enter/send key 
+KEY_ENTER     :: 0o527
+// print key 
+KEY_PRINT     :: 0o532
+// lower-left key (home down) 
+KEY_LL        :: 0o533
+// upper left of keypad 
+KEY_A1        :: 0o534
+// upper right of keypad 
+KEY_A3        :: 0o535
+// center of keypad 
+KEY_B2        :: 0o536
+// lower left of keypad 
+KEY_C1        :: 0o537
+// lower right of keypad 
+KEY_C3        :: 0o540
+// back-tab key 
+KEY_BTAB      :: 0o541
+// begin key 
+KEY_BEG       :: 0o542
+// cancel key 
+KEY_CANCEL    :: 0o543
+// close key 
+KEY_CLOSE     :: 0o544
+// command key 
+KEY_COMMAND   :: 0o545
+// copy key 
+KEY_COPY      :: 0o546
+// create key 
+KEY_CREATE    :: 0o547
+// end key 
+KEY_END       :: 0o550
+// exit key 
+KEY_EXIT      :: 0o551
+// find key 
+KEY_FIND      :: 0o552
+// help key 
+KEY_HELP      :: 0o553
+// mark key 
+KEY_MARK      :: 0o554
+// message key 
+KEY_MESSAGE   :: 0o555
+// move key 
+KEY_MOVE      :: 0o556
+// next key 
+KEY_NEXT      :: 0o557
+// open key 
+KEY_OPEN      :: 0o560
+// options key 
+KEY_OPTIONS   :: 0o561
+// previous key 
+KEY_PREVIOUS  :: 0o562
+// redo key 
+KEY_REDO      :: 0o563
+// reference key 
+KEY_REFERENCE :: 0o564
+// refresh key 
+KEY_REFRESH   :: 0o565
+// replace key 
+KEY_REPLACE   :: 0o566
+// restart key 
+KEY_RESTART   :: 0o567
+// resume key 
+KEY_RESUME    :: 0o570
+// save key 
+KEY_SAVE      :: 0o571
+// shifted begin key 
+KEY_SBEG      :: 0o572
+// shifted cancel key 
+KEY_SCANCEL   :: 0o573
+// shifted command key 
+KEY_SCOMMAND  :: 0o574
+// shifted copy key 
+KEY_SCOPY     :: 0o575
+// shifted create key 
+KEY_SCREATE   :: 0o576
+// shifted delete-character key 
+KEY_SDC       :: 0o577
+// shifted delete-line key 
+KEY_SDL       :: 0o600
+// select key 
+KEY_SELECT    :: 0o601
+// shifted end key 
+KEY_SEND      :: 0o602
+// shifted clear-to-end-of-line key 
+KEY_SEOL      :: 0o603
+// shifted exit key 
+KEY_SEXIT     :: 0o604
+// shifted find key 
+KEY_SFIND     :: 0o605
+// shifted help key 
+KEY_SHELP     :: 0o606
+// shifted home key 
+KEY_SHOME     :: 0o607
+// shifted insert-character key 
+KEY_SIC       :: 0o610
+// shifted left-arrow key 
+KEY_SLEFT     :: 0o611
+// shifted message key 
+KEY_SMESSAGE  :: 0o612
+// shifted move key 
+KEY_SMOVE     :: 0o613
+// shifted next key 
+KEY_SNEXT     :: 0o614
+// shifted options key 
+KEY_SOPTIONS  :: 0o615
+// shifted previous key 
+KEY_SPREVIOUS :: 0o616
+// shifted print key 
+KEY_SPRINT    :: 0o617
+// shifted redo key 
+KEY_SREDO     :: 0o620
+// shifted replace key 
+KEY_SREPLACE  :: 0o621
+// shifted right-arrow key 
+KEY_SRIGHT    :: 0o622
+// shifted resume key 
+KEY_SRESUME   :: 0o623
+// shifted save key 
+KEY_SSAVE     :: 0o624
+// shifted suspend key 
+KEY_SSUSPEND  :: 0o625
+// shifted undo key 
+KEY_SUNDO     :: 0o626
+// suspend key 
+KEY_SUSPEND   :: 0o627
+// undo key 
+KEY_UNDO      :: 0o630
+// Mouse event has occurred 
+KEY_MOUSE     :: 0o631
+// Terminal resize event 
+KEY_RESIZE    :: 0o632
+// Maximum key value is 0632 
+KEY_MAX       :: 0o777
diff --git a/ncurses/ncurses.odin b/ncurses/ncurses.odin
new file mode 100644 (file)
index 0000000..4125108
--- /dev/null
@@ -0,0 +1,68 @@
+package ncurses
+
+import "core:c"
+
+foreign import ncurses "system:ncurses"
+
+OK: c.int : 0
+ERR: c.int : -1
+
+foreign ncurses {
+       // Usually the first routine called when initializing a program.
+       //
+       // It determines the terminal type and initializes all ncurses data structures.
+       // It internally calls the first refresh().
+       //
+       // Error: writes a message to stderr and exit
+       // Success: returns stdscr
+       initscr :: proc() -> ^Window ---
+
+       // Use halfdelay mode.
+       //
+       // Similar to cbreak(), however after tenths of seconds
+       // have passed an ERR is returned if nothing was typed.
+       halfdelay :: proc(tenths: c.int) -> c.int ---
+
+       // Copies the stdscr to the physical terminal screen.
+       refresh :: proc() -> c.int ---
+       // Copies the specified window to the physical terminal screen.
+       wrefresh :: proc(win: ^Window) -> c.int ---
+
+       // All allocated resources from ncurses are cleaned up and the tty modes are restored to the status they had before calling initscr().
+       endwin :: proc() -> c.int ---
+
+       // Place the terminal into raw mode.
+       //
+       // Raw mode is similar to cbreak mode, in that characters typed are immediately passed through to the user program.
+       // The differences are that in raw mode, the interrupt, quit, suspend, and flow control characters are all passed through uninterpreted, instead of generating a signal.
+       // The behavior of the BREAK key depends on other bits in the tty driver that are not set by curses. 
+       raw :: proc() -> c.int ---
+       // Place the terminal out of raw mode.
+       //
+       // Raw mode is similar to cbreak mode, in that characters typed are immediately passed through to the user program.
+       // The differences are that in raw mode, the interrupt, quit, suspend, and flow control characters are all passed through uninterpreted, instead of generating a signal.
+       // The behavior of the BREAK key depends on other bits in the tty driver that are not set by curses. 
+       noraw :: proc() -> c.int ---
+
+       // Disables line buffering and erase/kill character-processing (interrupt and flow control characters are unaffected),
+       // making characters typed by the user immediately available to the program.
+       cbreak :: proc() -> c.int ---
+       // Returns the terminal to normal (cooked) mode.
+       nocbreak :: proc() -> c.int ---
+
+       // Show typed characters when user when using getch.
+       echo :: proc() -> c.int ---
+       // Hide typed characters when user when using getch.
+       noecho :: proc() -> c.int ---
+
+       // Set the cursor's visibility.
+       //
+       // Visibility modes:
+       //       0: invisible
+       //       1: normal
+       //       2: very visible
+       //
+       // Success: Returns previous cursor state
+       // Error: Returns ERR
+       curs_set :: proc(visibility: c.int) -> c.int ---
+}
diff --git a/ncurses/window.odin b/ncurses/window.odin
new file mode 100644 (file)
index 0000000..287fcab
--- /dev/null
@@ -0,0 +1,192 @@
+package ncurses
+
+import "core:c"
+foreign import ncurses "system:ncurses"
+
+@(private = "file")
+_win_st :: struct {}
+
+Window :: distinct _win_st
+
+@(private)
+foreign ncurses {
+       acs_map: [^]c.uint
+
+       getattrs :: proc(win: ^Window) -> c.int ---
+       getcurx :: proc(win: ^Window) -> c.int ---
+       getcury :: proc(win: ^Window) -> c.int ---
+       getbegx :: proc(win: ^Window) -> c.int ---
+       getbegy :: proc(win: ^Window) -> c.int ---
+       getmaxx :: proc(win: ^Window) -> c.int ---
+       getmaxy :: proc(win: ^Window) -> c.int ---
+       getparx :: proc(win: ^Window) -> c.int ---
+       getpary :: proc(win: ^Window) -> c.int ---
+}
+
+foreign ncurses {
+       stdscr: ^Window
+       LINES: c.int
+       COLS: c.int
+
+       // The upper left-hand corner of the window is at line y, column x. 
+       // If either h or w is zero, they default to LINES - y and COLS - x.
+       //
+       // A new full-screen window is created by calling newwin(0, 0, 0, 0). 
+       newwin :: proc(h, w, y, x: c.int) -> ^Window ---
+       // Deletes the named window, freeing all memory associated with it (it does not actually erase the window's screen image).
+       //
+       // Subwindows must be deleted before the main window can be deleted. 
+       //
+       // Returns an error if the window pointer is null, or if the window is the parent of another window. 
+       delwin :: proc(win: ^Window) -> c.int ---
+
+       // moves the window so that the upper left-hand corner is at position (x, y).
+       // If the move would cause the window to be off the screen, it is an error and the window is not moved.
+       // Moving subwindows is allowed, but should be avoided. 
+       mvwin :: proc(win: ^Window, y, x: c.int) ---
+
+       // Draw a box around the edges of a window.
+       //
+       // verch: vertical character
+       // horch: horizontal character
+       box :: proc(win: ^Window, verch, horch: c.int) ---
+       // Draw a box around the edges of stdscr.
+       //
+       // ls: left side,
+       // rs: right side,
+       // ts: top side,
+       // bs: bottom side,
+       // tl: top left-hand corner,
+       // tr: top right-hand corner,
+       // bl: bottom left-hand corner, and
+       // br: bottom right-hand corner. 
+       //
+       // If any of these arguments is zero, then the corresponding default values are used instead:
+       // ACS_VLINE,
+       // ACS_VLINE,
+       // ACS_HLINE,
+       // ACS_HLINE,
+       // ACS_ULCORNER,
+       // ACS_URCORNER,
+       // ACS_LLCORNER,
+       // ACS_LRCORNER.
+       border :: proc(ls, rs, ts, bs, tl, tr, bl, br: c.uint) ---
+       // Draw a box around the edges of a window.
+       //
+       // ls: left side,
+       // rs: right side,
+       // ts: top side,
+       // bs: bottom side,
+       // tl: top left-hand corner,
+       // tr: top right-hand corner,
+       // bl: bottom left-hand corner, and
+       // br: bottom right-hand corner. 
+       //
+       // If any of these arguments is zero, then the corresponding default values are used instead:
+       // ACS_VLINE,
+       // ACS_VLINE,
+       // ACS_HLINE,
+       // ACS_HLINE,
+       // ACS_ULCORNER,
+       // ACS_URCORNER,
+       // ACS_LLCORNER,
+       // ACS_LRCORNER.
+       wborder :: proc(win: ^Window, ls, rs, ts, bs, tl, tr, bl, br: c.uint) ---
+
+       // The clear and wclear routines are like erase and werase, but they also call clearok,
+       // so that the screen is cleared completely on the next call to wrefresh for that window and repainted from scratch. 
+       clear :: proc() -> c.int ---
+       // The clear and wclear routines are like erase and werase, but they also call clearok,
+       // so that the screen is cleared completely on the next call to wrefresh for that window and repainted from scratch. 
+       wclear :: proc(win: ^Window) -> c.int ---
+
+       // Erase from the cursor to the end of screen.
+       clrtoeol :: proc() -> c.int ---
+       // Erase from the cursor to the end of screen.
+       wclrtoeol :: proc(win: ^Window) -> c.int ---
+
+       // Copy blanks to every position in stdscr, clearing the screen.
+       erase :: proc() -> c.int ---
+       // Copy blanks to every position in the window, clearing the screen.
+       werase :: proc(win: ^Window) -> c.int ---
+
+       // 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 ---
+}
+
+// Get the current coordinates of the cursor.
+getyx :: proc(win: ^Window) -> (y, x: c.int) {return getcury(win), getcurx(win)}
+// Get absolute screen coordinates of the specified window. 
+getbegyx :: proc(win: ^Window) -> (y, x: c.int) {return getbegy(win), getbegx(win)}
+// Get the size of the current window.
+getmaxyx :: proc(win: ^Window) -> (y, x: c.int) {return getmaxy(win), getmaxx(win)}
+// Get the beginning coordinates of the subwindow relative to the parent window.
+getparyx :: proc(win: ^Window) -> (y, x: c.int) {return getpary(win), getparx(win)}
+
+// -- VT100 symbols begin here 
+
+// upper left corner 
+ACS_ULCORNER := acs_map['l']
+// lower left corner 
+ACS_LLCORNER := acs_map['m']
+// upper right corner 
+ACS_URCORNER := acs_map['k']
+// lower right corner 
+ACS_LRCORNER := acs_map['j']
+// tee pointing right 
+ACS_LTEE := acs_map['t']
+// tee pointing left 
+ACS_RTEE := acs_map['u']
+// tee pointing up 
+ACS_BTEE := acs_map['v']
+// tee pointing down 
+ACS_TTEE := acs_map['w']
+// horizontal line 
+ACS_HLINE := acs_map['q']
+// vertical line 
+ACS_VLINE := acs_map['x']
+// large plus or crossover 
+ACS_PLUS := acs_map['n']
+// scan line 1 
+ACS_S1 := acs_map['o']
+// scan line 9 
+ACS_S9 := acs_map['s']
+// diamon] 
+ACS_DIAMOND := acs_map['`']
+// checker board (stipple) 
+ACS_CKBOARD := acs_map['a']
+// degree symbol 
+ACS_DEGREE := acs_map['f']
+// plus/minus 
+ACS_PLMINUS := acs_map['g']
+// bulle] 
+ACS_BULLET := acs_map['~']
+
+// -- Teletype 5410v1 symbols begin here 
+
+ACS_LARROW := acs_map[','] /* arro] pointing left */
+ACS_RARROW := acs_map['+'] /* arro] pointing right */
+ACS_DARROW := acs_map['.'] /* arro] pointing down */
+ACS_UARROW := acs_map['-'] /* arrow pointing up */
+ACS_BOARD := acs_map['h'] /* board of squares */
+ACS_LANTERN := acs_map['i'] /* lantern symbol */
+ACS_BLOCK := acs_map['0'] /* solid square block */
+
+// -- These aren't documented, but a lot of System Vs have them anyway
+// -- (you can spot pprryyzz{{||}} in a lot of AT&T terminfo strings).
+// -- The ACS_names may not match AT&T's, our source didn't know them.
+
+// scan line 3 
+ACS_S3 := acs_map['p']
+// scan line 7 
+ACS_S7 := acs_map['r']
+// less/equal 
+ACS_LEQUAL := acs_map['y']
+// greater/equal 
+ACS_GEQUAL := acs_map['z']
+// Pi 
+ACS_PI := acs_map['{']
+// not equal 
+ACS_NEQUAL := acs_map['|']
+// UK pound sign 
+ACS_STERLING := acs_map['}']