import "core:sync"
import "core:thread"
import "core:time"
+import "core:unicode/utf8"
import fpath "core:path/filepath"
COLOR_THIRD :: 18
COLOR_FOURTH :: 19
+DEFAULT_PORT :: 42069
+
json_marshal_options := json.Marshal_Options{
spec = .JSON,
pretty = true,
seed_phrase_entropy: u128,
seed_phrase_checksum: u8,
+
+ host: net.TCP_Socket,
}
app_init :: proc(storage_path: string) -> App {
screen := nc.initscr()
nc.cbreak()
- nc.keypad(screen, true)
- nc.curs_set(0)
+ nc.curs_set(0)
+
+ nc.use_default_colors()
+ nc.start_color()
- 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_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)
- nc.init_pair(1, COLOR_FIRST, COLOR_THIRD)
- nc.init_pair(2, COLOR_FIRST, COLOR_FOURTH)
+ screen_height, screen_width := nc.getmaxyx(screen)
- screen_height, screen_width := nc.getmaxyx(screen)
+ info_bar := nc.newwin(1, screen_width, screen_height - 2, 0)
+ input_window := nc.newwin(1, screen_width, screen_height - 1, 0)
- info_bar := nc.newwin(1, screen_width, screen_height - 2, 0)
- input_window := nc.newwin(1, screen_width, screen_height - 1, 0)
+ nc.keypad(input_window, true)
+
+ nc.refresh()
- nc.refresh()
+ profiles_path := fpath.join({ storage_path, "profiles" })
+ if !os.exists(profiles_path) {
+ _ = os.make_directory(profiles_path)
+ }
+
app := App{
running = true,
storage_path = storage_path,
- profiles_path = fpath.join({ storage_path, "profiles" }),
+ profiles_path = profiles_path,
screen = screen,
}
app_deinit :: proc(app: ^App) {
+ sync.mutex_lock(&app.mutex)
+ defer sync.mutex_unlock(&app.mutex)
+
+ net.close(app.host)
+
profile_deinit(app.profile)
- delete(app.password)
+ delete(app.profile_password)
config_deinit(app.config)
nc.delwin(app.input_window)
nc.mvwprintw(app.info_bar, 0, 0, c_content)
nc.wattroff(app.info_bar, color)
- nc.wrefresh(app.info_bar)
+ nc.wrefresh(app.info_bar)
}
app_clear_box_message :: proc(app: ^App) {
}
}
-app_get_input :: proc(app: ^App, allocator := context.allocator) -> string {
+app_get_input :: proc(app: ^App, hidden := false, allow_empty := false, allocator := context.allocator) -> string {
context.allocator = allocator
for {
buffer: [256]u8
+ buffer_len: u32
+ cursor_pos: u32
+
screen_height, screen_width := nc.getmaxyx(app.screen)
- nc.curs_set(1)
- nc.wgetnstr(app.input_window, raw_data(buffer[:]), len(buffer))
- nc.curs_set(0)
+ nc.curs_set(1)
+
+ input_loop: for {
+ char := nc.wgetch(app.input_window)
+
+ switch char {
+ case '\n':
+ break input_loop
+
+ case 27:
+ buffer_len = 0
+ cursor_pos = 0
+
+ case nc.KEY_BACKSPACE:
+ if cursor_pos != 0 {
+ buffer_len -= 1
+ cursor_pos -= 1
+ }
+
+ case nc.KEY_LEFT:
+ if cursor_pos != 0 {
+ cursor_pos -= 1
+ }
+
+ case nc.KEY_RIGHT:
+ if cursor_pos != buffer_len {
+ cursor_pos += 1
+ }
+
+ case 0o400..=0o777:
- nc.wclear(app.input_window)
+ case:
+ buffer_len += 1
- c_input := cstring(raw_data(buffer[:]))
+ 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 len(c_input) == 0 {
- continue
- }
+ buffer[cursor_pos] = u8(char)
- app_set_info_bar(app, "")
+ cursor_pos += 1
+ }
+
+ buffer[buffer_len] = 0
+ output := string(buffer[:buffer_len + 1])
+
+ if !hidden {
+ nc.wclear(app.input_window)
+ nc.mvwprintw(app.input_window, 0, 0, strings.unsafe_string_to_cstring(output))
+ nc.wrefresh(app.input_window)
+ nc.wmove(app.input_window, 0, i32(cursor_pos))
+ }
+ }
+
+ 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[:])))
}
}
app_reset_seed_phrase :: proc(app: ^App) {
- sync.mutex_lock(&app.mutex)
- defer sync.mutex_unlock(&app.mutex)
-
app.seed_phrase_entropy = rand.uint128()
sha_context: sha3.Context
case .Ask_Profile_Confirm_Password: state_ask_profile_confirm_password(app)
case .Ask_Profile_Password: state_ask_profile_password(app)
+ case .Connect_To_Host: state_connect_to_host(app)
+ case .Invalid_Host: state_invalid_host(app)
+
case .Main: state_main(app)
}
}
context.random_generator = rand.default_random_generator(&rng_state)
when ODIN_DEBUG {
- track: mem.Tracking_Allocator
- mem.tracking_allocator_init(&track, context.allocator)
+ track: mem.Tracking_Allocator
+ mem.tracking_allocator_init(&track, context.allocator)
- context.allocator = mem.tracking_allocator(&track)
+ context.allocator = mem.tracking_allocator(&track)
- defer {
- if len(track.allocation_map) > 0 {
- log.errorf("%v allocations not freed:", len(track.allocation_map))
+ defer {
+ if len(track.allocation_map) > 0 {
+ log.errorf("%v allocations not freed:", len(track.allocation_map))
- for _, entry in track.allocation_map {
- log.errorf("- %v bytes at %v\n", entry.size, entry.location)
- }
- }
+ for _, entry in track.allocation_map {
+ log.errorf("- %v bytes at %v\n", entry.size, entry.location)
+ }
+ }
- if len(track.bad_free_array) > 0 {
- log.errof("%v incorrect frees:\n", len(track.bad_free_array))
+ if len(track.bad_free_array) > 0 {
+ log.errof("%v incorrect frees:\n", len(track.bad_free_array))
- for entry in track.bad_free_array {
- log.errorf("- %p at %v\n", entry.memory, entry.location)
- }
- }
+ for entry in track.bad_free_array {
+ log.errorf("- %p at %v\n", entry.memory, entry.location)
+ }
+ }
- mem.tracking_allocator_destroy(&track)
- }
- }
-
+ mem.tracking_allocator_destroy(&track)
+ }
+ }
+
app := app_init(storage_path)
defer app_deinit(&app)
tag := buffer[len(iv):][:chacha.TAG_SIZE]
ciphertext := buffer[len(iv) + len(tag):]
- if !chacha.open(&chacha_context, buffer[:], iv, {}, ciphertext, tag) {
+ if !chacha.open(&chacha_context, buffer[:len(ciphertext)], iv, {}, ciphertext, tag) {
return {}, .Invalid_Password
}
return profile_from_parsed(parsed_profile), nil
}
-profile_deinit :: proc(profile: ^Profile) {
+profile_deinit :: proc(profile: Profile) {
delete(profile.host)
}
-profile_update :: proc(profile: ^Profile, app: ^App) {
+profile_update :: proc(profile: Profile, app: ^App) {
profile_filename := strings.concatenate({ app.config.selected_profile.?, ".json" }, context.temp_allocator)
profile_path := fpath.join({ app.profiles_path, profile_filename }, context.temp_allocator)
- profile_file, err1 := os.open(profile_path, os.O_CREATE | os.O_TRUNC, 0o664)
+ profile_file, err1 := os.open(profile_path, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0o664)
- parsed_profile := profile_to_parsed(profile^)
+ parsed_profile := profile_to_parsed(profile)
if err1 != nil {
+ log.errorf("Failed to open profile file with error: %v", err1)
app_set_info_bar(app, "Failed to save profile.")
return
}
err2 := json.marshal_to_builder(&json_string_builder, parsed_profile, &json_marshal_options)
if err2 != nil {
+ log.errorf("Failed to marshal profile with error: %v", err2)
app_set_info_bar(app, "Failed to save profile.")
return
}
tag := buffer[len(iv):][:chacha.TAG_SIZE]
if rand.read(iv[:]) < len(iv) {
+ log.error("Failed to generate iv.")
app_set_info_bar(app, "Failed to save profile.")
return
}
- chacha.seal(&chacha_context, buffer[len(iv) + len(tag):], tag, iv, {}, transmute([]u8)json_string)
+ chacha.seal(&chacha_context, buffer[len(iv) + len(tag):][:len(json_string)], 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 {
+ log.errorf("Failed to write profile to file with error: %v", err3)
app_set_info_bar(app, "Failed to save profile.")
return
}
Ask_Profile_Confirm_Password,
Ask_Profile_Password,
+ Connect_To_Host,
+ Invalid_Host,
+
Main,
}
},
)
- input := app_get_input(app, context.temp_allocator)
+ input := app_get_input(app, allocator = context.temp_allocator)
if err, ok := handle_command(app, input).?; ok {
if err != .Not_A_Command {
return
}
- profile_filename := strings.concatenate({ name, ".json" }, context.temp_allocator)
+ profile_filename := strings.concatenate({ selected_profile, ".json" }, context.temp_allocator)
profile_path := fpath.join({ app.storage_path, profile_filename }, context.temp_allocator)
profile, profile_error := profile_load_from_name(selected_profile, app)
},
)
- input := app_get_input(app, context.temp_allocator)
+ input := app_get_input(app, allocator = context.temp_allocator)
if err, ok := handle_command(app, input).?; ok {
if err != .Not_A_Command {
},
)
- app.profile_password = app_get_input(app)
+ app.profile_password = app_get_input(app, true)
app_set_state(app, .Load_Profile)
}
)
}
- input := app_get_input(app)
+ input := app_get_input(app, allocator = context.temp_allocator)
if err, ok := handle_command(app, input).?; ok {
if err != .Not_A_Command {
},
)
- input := app_get_input(app)
+ app.profile_password = app_get_input(app, true, true)
app_set_state(app, .Ask_Profile_Confirm_Password)
}
state_ask_profile_confirm_password :: proc(app: ^App) {
-
+ app_set_box_message(
+ app,
+ {
+ "Please enter your password again.",
+ },
+ )
+
+ input := app_get_input(app, true, true, context.temp_allocator)
+
+ if input != app.profile_password {
+ app_set_info_bar(app, "Passwords don't match.")
+ return
+ }
+
+ profile_update(app.profile, app)
+
+ app_set_state(app, .Connect_To_Host)
+}
+
+state_connect_to_host :: proc(app: ^App) {
+ app_set_box_message(
+ app,
+ {
+ "Connecting to host.",
+ }
+ )
+
+ host_endpoint, err1 := net.resolve_ip4(app.profile.host)
+
+ if err1 != nil {
+ log.errorf("Failed to resolve host with error: %v", err1)
+ app_set_state(app, .Invalid_Host)
+ return
+ }
+
+ host_endpoint.port = DEFAULT_PORT
+
+ host, err2 := net.dial_tcp_from_endpoint(host_endpoint)
+
+ if err2 != nil {
+ log.errorf("Failed to connect to host with error: %v", err2)
+ app_set_state(app, .Invalid_Host)
+ return
+ }
+
+ {
+ sync.mutex_lock(&app.mutex)
+ defer sync.mutex_unlock(&app.mutex)
+
+ app.host = host
+ }
+
+ app_set_state(app, .Main)
+}
+
+state_invalid_host :: proc(app: ^App) {
+ app_set_box_message(
+ app,
+ {
+ "Failed to connect to host.",
+ "Either enter a new host,",
+ "or :retry."
+ }
+ )
+
+ input := app_get_input(app)
+
+ if err, ok := handle_command(app, input).?; ok {
+ if err != .Not_A_Command {
+ return
+ }
+ } else {
+ delete(input)
+ return
+ }
+
+ {
+ sync.mutex_lock(&app.mutex)
+ defer sync.mutex_unlock(&app.mutex)
+
+ delete(app.profile.host)
+ app.profile.host = input
+ }
+
+ app_set_state(app, .Connect_To_Host)
}
state_main :: proc(app: ^App) {