--- /dev/null
+package main
+
+import "core:crypto/ed25519"
+import "core:encoding/csv"
+import "core:encoding/json"
+import "core:fmt"
+import "core:log"
+import "core:net"
+import "core:os"
+import "core:strings"
+
+import fpath "core:path/filepath"
+
+import nc "ncurses"
+
+import "../common"
+
+Config :: struct {
+ selected_profile: Maybe(string),
+}
+
+config_deinit :: proc(config: Config) {
+ if selected_profiles, ok := config.selected_profile.?; ok {
+ delete(selected_profiles)
+ }
+}
+
+config_update :: proc(config: Config) {
+ home_path := os.get_env("HOME")
+
+ storage_path := fpath.join({ home_path, "mesange-cli" }, 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)
+
+ if err1 != nil {
+ fmt.eprintln("Failed to open config file with error: .", err1)
+ os.exit(1)
+ }
+
+ 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)
+
+ if err2 != nil {
+ fmt.eprintln("Failed to marshal config.")
+ os.exit(1)
+ }
+}
+
+Profile :: struct {
+ private_key: ed25519.Private_Key,
+}
+
+App :: struct {
+ screen: ^nc.Window,
+ storage_path: string,
+ state: State,
+
+ box_message: Maybe(^nc.Window),
+ info_bar: ^nc.Window,
+
+ config: Config,
+ profile_password: Maybe(string),
+ profile: Profile,
+}
+
+app_init :: proc() -> App {
+ screen := nc.initscr()
+
+ nc.keypad(screen, true)
+ nc.curs_set(0)
+
+ screen_height, screen_width := nc.getmaxyx(screen)
+
+ info_bar := nc.newwin(0, 0, 3, screen_width)
+
+ nc.box(info_bar, 0, 0)
+ nc.wrefresh(info_bar)
+
+ home_path := os.get_env("HOME")
+ storage_path := fpath.join({ home_path, "mesange" })
+
+ if !os.exists(storage_path) {
+ err := os.make_directory(storage_path)
+
+ if err != nil {
+ fmt.eprintln("Failed to create storage directory.")
+ os.exit(1)
+ }
+ }
+
+ return {
+ screen = screen,
+ storage_path = storage_path,
+ }
+}
+
+app_deinit :: proc(app: App) {
+ config_deinit(app.config)
+
+ nc.delwin(app.info_bar)
+
+ if box_message, ok := app.box_message.?; ok {
+ nc.delwin(box_message)
+ }
+
+ nc.endwin()
+}
+
+app_set_state :: proc(app: ^App, state: State) {
+ app_clear_box_message(app)
+
+ nc.clear()
+ nc.refresh()
+
+ app.state = state
+}
+
+app_set_info_bar :: proc(app: ^App, content: string) {
+
+}
+
+app_set_box_message :: proc(app: ^App, lines: []string) {
+ app_clear_box_message(app)
+
+ screen_height, screen_width := nc.getmaxyx(app.screen)
+
+ max_line_len: int
+
+ for line in lines {
+ if len(line) > max_line_len {
+ max_line_len = len(line)
+ }
+ }
+
+ height := i32(len(lines)) + 2
+ width := i32(max_line_len) + 2
+
+ box_message := nc.newwin(
+ height, width,
+ (screen_height - height) / 2,
+ (screen_width - width) / 2,
+ )
+
+ 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.wrefresh(box_message)
+
+ app.box_message = box_message
+}
+
+app_clear_info_bar :: proc(app: App) {
+
+}
+
+app_clear_box_message :: proc(app: ^App) {
+ if box_message, ok := app.box_message.?; ok {
+ nc.wclear(box_message)
+ nc.wrefresh(box_message)
+ nc.delwin(box_message)
+
+ app.box_message = nil
+ }
+}
+
+app_get_input :: proc(app: App, allocator := context.allocator) -> string {
+ context.allocator = allocator
+
+ 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.curs_set(0)
+
+ return strings.clone_from_cstring(cstring(raw_data(&buffer)))
+}
+
+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)
+
+ 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)
+ }
+ }
+}
--- /dev/null
+Subproject commit 8712a2b7a368771baf35ce0e860b577c3f87052d
--- /dev/null
+package main
+
+import "core:encoding/json"
+import "core:fmt"
+import "core:os"
+import "core:strings"
+
+import fpath "core:path/filepath"
+
+State :: enum {
+ Load_Config,
+ Ask_Config,
+
+ Load_Profile,
+ Ask_Profile,
+ Ask_Profile_Password,
+ Ask_Profile_Seed_Phrase,
+}
+
+state_load_config :: proc(app: ^App) {
+ config_path := fpath.join({ app.storage_path, "config.json" }, context.temp_allocator)
+
+ raw_config, config_read := os.read_entire_file_from_filename(config_path)
+ config: Config
+
+ if config_read {
+ err := json.unmarshal(raw_config, &config)
+
+ if err != nil {
+ app_set_state(app, .Ask_Config)
+ return
+ }
+ } else {
+ app_set_info_bar(app, "No config, using default.")
+ }
+
+ app.config = config
+
+ app_set_state(app, .Load_Profile)
+}
+
+state_ask_config :: proc(app: ^App) {
+ app_set_box_message(
+ app,
+ {
+ "Invalid config.",
+ "Type :delete or :retry.",
+ },
+ )
+
+ 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:
+ return
+ }
+
+ app_set_state(app, .Load_Config)
+}
+
+state_load_profile :: proc(app: ^App) {
+ selected_profile, ok := app.config.selected_profile.?
+
+ if !ok {
+ app_set_state(app, .Ask_Profile)
+ return
+ }
+
+ encrypted_profile_path := fpath.join({ app.storage_path, selected_profile }, context.temp_allocator)
+
+ if os.exists(encrypted_profile_path) && app.profile_password == nil {
+ app_set_state(app, .Ask_Profile_Password)
+ return
+ }
+
+ profile_content: string
+
+ load_profile_content: {
+ encrypted_profile_content, profile_read := os.read_entire_file_from_filename(encrypted_profile_path)
+
+ if profile_read {
+
+ }
+ }
+}
+
+state_ask_profile :: proc(app: ^App) {
+ app_set_box_message(
+ app,
+ {
+ "No profile selected.",
+ "Please enter a profile name.",
+ },
+ )
+
+ input := app_get_input(app^)
+
+ if len(input) == 0 {
+ return
+ }
+
+ app.config.selected_profile = app_get_input(app^)
+
+ config_update(app.config)
+ app_set_state(app, .Load_Profile)
+}
+
+state_ask_profile_password :: proc(app: ^App) {
+ app_set_box_message(
+ app,
+ {
+ "Profile is encrypted.",
+ "Please enter profile password."
+ },
+ )
+
+ app.profile_password = app_get_input(app^)
+
+ app_set_state(app, .Load_Profile)
+}
+
+state_ask_profile_seed_phrase :: proc(app: ^App) {
+
+}
+++ /dev/null
-package main
-
-import "core:encoding/csv"
-import "core:encoding/json"
-import "core:fmt"
-import "core:log"
-import "core:net"
-import "core:os"
-
-import fpath "core:path/filepath"
-
-import "../common"
-
-Config :: struct {
- selected_profile: Maybe(string),
-}
-
-App :: struct {
- config: Config,
- servers: map[string]net.TCP_Socket,
-}
-
-config_update :: proc(app: App, config: Config) {
- home_path := os.get_env("HOME")
- storage_path := fpath.join({ home_path, "mesange-cli" }, context.temp_allocator)
-
- config_path := fpath.join({ storage_path, "config.json" }, context.temp_allocator)
-
- config_file, err1 := os.open(config_path, O_WRONLY)
-
- if err1 != nil {
- fmt.eprintln("Failed to open config file.")
- 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, app.config, &marshal_options)
-
- if err2 != nil {
- fmt.eprintln("Failed to marshal config.")
- return
- }
-}
-
-init :: proc(app: ^App) {
- home_path := os.get_env("HOME")
- storage_path := fpath.join({ home_path, "mesange-cli" }, context.temp_allocator)
-
- err1 := os.make_directory(storage_path)
-
- if err1 != nil {
- fmt.eprintln("Failed to create storage directory.")
- return
- }
-
- { // Load config.
- fmt.println("Loading config...")
-
- config_path := fpath.join({ storage_path, "config.json" }, context.temp_allocator)
-
- if !os.exists(config_path) {
- // Init default config.
- ok := os.write_entire_file(
- config_path,
- `
-{
- "current_profile": null
-}
- `,
- )
-
- if !ok {
- fmt.eprintln("Failed to write to config file.")
- return
- }
- }
-
- raw_config, ok := os.read_entire_file_from_filename(config_path)
-
- if !ok {
- fmt.eprintln("Failed to read config file.")
- return
- }
-
- config: Config
-
- err2 := json.unmarshal(raw_config, &config)
-
- if err2 != nil {
- fmt.eprintln("Invalid config file.")
- return
- }
-
- app.config = config
- }
-}
-
-main :: proc() {
- context.logger = log.create_console_logger()
- defer log.destroy_console_logger(context.logger)
-
- when ODIN_DEBUG {
- track: mem.Tracking_Allocator
- mem.tracking_allocator_init(&track, context.allocator)
-
- context.allocator = mem.tracking_allocator(&track)
-
- defer {
- if len(track.allocation_map) > 0 {
- fmt.eprintf("=== %v allocations not freed: ===\n", len(track.allocation_map))
-
- for _, entry in track.allocation_map {
- fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
- }
- }
-
- if len(track.bad_free_array) > 0 {
- fmt.eprintf("=== %v incorrect frees: ===\n", len(track.bad_free_array))
-
- for entry in track.bad_free_array {
- fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
- }
- }
-
- mem.tracking_allocator_destroy(&track)
- }
- }
-
- app := App{
- servers = make(map[string]net.TCP_Socket)
- }
-
- init()
-}