sing-box-tray/dialog.jai

642 lines
24 KiB
Plaintext

#import "Basic";
#import "Windows";
#import "Windows_Utf8";
#import "File";
#import "String";
#import "Thread";
// Win32 declarations are provided by win32.jai loaded in the main workspace.
Dialog_State :: struct {
edit_hwnd: HWND;
ok_hwnd: HWND;
cancel_hwnd: HWND;
dialog_done: bool;
url_saved: bool;
is_test_mode: bool;
}
dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT #c_call {
ctx: #Context;
push_context ctx {
state := cast(*Dialog_State) GetWindowLongPtrW(hwnd, GWLP_USERDATA);
if msg == {
case WM_CREATE; {
create_struct := cast(*CREATESTRUCTW) lparam;
state = cast(*Dialog_State) create_struct.lpCreateParams;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, cast(LONG_PTR) state);
hFont := GetStockObject(DEFAULT_GUI_FONT);
static_label := CreateWindowExW(
0,
utf8_to_wide("STATIC"),
utf8_to_wide("Enter Sing-box Config URL:"),
WS_CHILD | WS_VISIBLE | SS_LEFT,
20, 20, 410, 20,
hwnd,
null,
null,
null
);
SendMessageW(static_label, WM_SETFONT, cast(WPARAM) hFont, TRUE);
state.edit_hwnd = CreateWindowExW(
WS_EX_CLIENTEDGE,
utf8_to_wide("EDIT"),
utf8_to_wide(""),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL,
20, 45, 410, 25,
hwnd,
null,
null,
null
);
SendMessageW(state.edit_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
// Populate with existing URL if present
config_filename := global_config_path;
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
existing_url := get_json_string_field(config_data, "\"_url\"");
if existing_url {
wide_url := utf8_to_wide(existing_url);
SetWindowTextW(state.edit_hwnd, wide_url);
}
free(config_data);
}
state.ok_hwnd = CreateWindowExW(
0,
utf8_to_wide("BUTTON"),
utf8_to_wide("OK"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON,
220, 85, 100, 30,
hwnd,
cast(HMENU) IDOK,
null,
null
);
SendMessageW(state.ok_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
state.cancel_hwnd = CreateWindowExW(
0,
utf8_to_wide("BUTTON"),
utf8_to_wide("Cancel"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
330, 85, 100, 30,
hwnd,
cast(HMENU) IDCANCEL,
null,
null
);
SendMessageW(state.cancel_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
SetFocus(state.edit_hwnd);
return 0;
}
case WM_COMMAND; {
control_id := LOWORD(wparam);
if control_id == IDOK {
length := GetWindowTextLengthW(state.edit_hwnd);
if length > 0 {
buffer := cast(*u16) alloc((length + 1) * size_of(u16));
defer free(buffer);
GetWindowTextW(state.edit_hwnd, buffer, length + 1);
url_utf8 := wide_to_utf8(buffer);
defer free(url_utf8);
trimmed_url := trim(url_utf8);
if trimmed_url {
mode: string;
port := ifx state.is_test_mode then 10899 else 10801;
update_interval := 3600;
config_filename := global_config_path;
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
extracted_mode := get_json_string_field(config_data, "\"_mode\"");
if extracted_mode {
mode = copy_string(extracted_mode);
} else {
mode = copy_string("system_proxy");
}
port_str := get_json_val_field(config_data, "\"_port\"");
if port_str {
val, ok := to_integer(port_str);
if ok port = xx val;
}
interval_str := get_json_val_field(config_data, "\"_update_interval\"");
if interval_str {
val, ok := to_integer(interval_str);
if ok update_interval = xx val;
}
free(config_data);
} else {
mode = copy_string("system_proxy");
}
defer free(mode);
minimal_json := tprint("{\n \"_url\": \"%\",\n \"_mode\": \"%\",\n \"_port\": %,\n \"_update_interval\": %,\n \"inbounds\": [],\n \"outbounds\": []\n}", trimmed_url, mode, port, update_interval);
write_entire_file(config_filename, minimal_json);
state.url_saved = true;
}
} else {
config_filename := global_config_path;
DeleteFileW(utf8_to_wide(config_filename));
state.url_saved = true;
}
DestroyWindow(hwnd);
return 0;
} else if control_id == IDCANCEL {
DestroyWindow(hwnd);
return 0;
}
}
case WM_CLOSE; {
DestroyWindow(hwnd);
return 0;
}
case WM_DESTROY; {
if state {
state.dialog_done = true;
}
PostThreadMessageW(GetCurrentThreadId(), WM_NULL, 0, 0);
return 0;
}
}
return DefWindowProcW(hwnd, msg, wparam, lparam);
}
}
show_config_url_dialog :: (parent_hwnd: HWND, is_test_mode := false) -> bool {
hInstance := GetModuleHandleW(null);
class_name := utf8_to_wide("Sing-boxConfigUrlDialogClass");
wclass: WNDCLASSEXW;
wclass.cbSize = size_of(WNDCLASSEXW);
wclass.style = CS_HREDRAW | CS_VREDRAW;
wclass.lpfnWndProc = dialog_proc;
wclass.hInstance = hInstance;
wclass.hCursor = LoadCursorW(null, IDC_ARROW);
wclass.hbrBackground = cast(HBRUSH) (COLOR_WINDOW + 1);
wclass.lpszClassName = class_name;
RegisterClassExW(*wclass);
defer UnregisterClassW(class_name, hInstance);
screen_w := GetSystemMetrics(SM_CXSCREEN);
screen_h := GetSystemMetrics(SM_CYSCREEN);
dialog_w: s32 = 460;
dialog_h: s32 = 170;
dialog_x := (screen_w - dialog_w) / 2;
dialog_y := (screen_h - dialog_h) / 2;
state: Dialog_State;
state.is_test_mode = is_test_mode;
dialog_hwnd := CreateWindowExW(
WS_EX_DLGMODALFRAME,
class_name,
utf8_to_wide("Configure Sing-box URL"),
WS_POPUP | WS_CAPTION | WS_SYSMENU,
dialog_x, dialog_y, dialog_w, dialog_h,
parent_hwnd,
null,
hInstance,
*state
);
if !dialog_hwnd return false;
if parent_hwnd {
EnableWindow(parent_hwnd, FALSE);
}
ShowWindow(dialog_hwnd, SW_SHOW);
UpdateWindow(dialog_hwnd);
while !state.dialog_done {
msg: MSG;
if GetMessageW(*msg, null, 0, 0) {
if !IsDialogMessageW(dialog_hwnd, *msg) {
TranslateMessage(*msg);
DispatchMessageW(*msg);
}
} else {
PostQuitMessage(cast(s32) msg.wParam);
break;
}
}
if parent_hwnd {
EnableWindow(parent_hwnd, TRUE);
SetFocus(parent_hwnd);
}
return state.url_saved;
}
show_config_port_dialog :: (parent_hwnd: HWND, is_test_mode := false) -> bool {
// Port Dialog Proc
port_dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT #c_call {
ctx: #Context;
push_context ctx {
state := cast(*Dialog_State) GetWindowLongPtrW(hwnd, GWLP_USERDATA);
if msg == {
case WM_CREATE; {
create_struct := cast(*CREATESTRUCTW) lparam;
state = cast(*Dialog_State) create_struct.lpCreateParams;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, cast(LONG_PTR) state);
hFont := GetStockObject(DEFAULT_GUI_FONT);
label_hwnd := CreateWindowExW(
0,
utf8_to_wide("STATIC"),
utf8_to_wide("Enter SOCKS/HTTP proxy port (default 10801):"),
WS_CHILD | WS_VISIBLE,
20, 20, 410, 20,
hwnd,
null,
null,
null
);
SendMessageW(label_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
state.edit_hwnd = CreateWindowExW(
0,
utf8_to_wide("EDIT"),
utf8_to_wide(""),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL | ES_NUMBER,
20, 45, 410, 25,
hwnd,
null,
null,
null
);
SendMessageW(state.edit_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
// Populate with existing port if present
config_filename := global_config_path;
config_data, read_ok := read_entire_file(config_filename);
port := ifx state.is_test_mode then 10899 else 10801;
if !state.is_test_mode && read_ok {
port_str := get_json_val_field(config_data, "\"_port\"");
if port_str {
val, ok := to_integer(port_str);
if ok {
port = xx val;
}
}
free(config_data);
} else if state.is_test_mode && read_ok {
port_str := get_json_val_field(config_data, "\"_port\"");
if port_str {
val, ok := to_integer(port_str);
if ok {
port = xx val;
}
}
free(config_data);
}
wide_port := utf8_to_wide(tprint("%", port));
SetWindowTextW(state.edit_hwnd, wide_port);
state.ok_hwnd = CreateWindowExW(
0,
utf8_to_wide("BUTTON"),
utf8_to_wide("OK"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON,
220, 85, 100, 30,
hwnd,
cast(HMENU) IDOK,
null,
null
);
SendMessageW(state.ok_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
state.cancel_hwnd = CreateWindowExW(
0,
utf8_to_wide("BUTTON"),
utf8_to_wide("Cancel"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
330, 85, 100, 30,
hwnd,
cast(HMENU) IDCANCEL,
null,
null
);
SendMessageW(state.cancel_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
SetFocus(state.edit_hwnd);
return 0;
}
case WM_COMMAND; {
control_id := LOWORD(wparam);
if control_id == IDOK {
length := GetWindowTextLengthW(state.edit_hwnd);
if length > 0 {
buffer := cast(*u16) alloc((length + 1) * size_of(u16));
defer free(buffer);
GetWindowTextW(state.edit_hwnd, buffer, length + 1);
port_utf8 := wide_to_utf8(buffer);
defer free(port_utf8);
val, ok := to_integer(trim(port_utf8));
if ok && val > 0 && val <= 65535 {
port := xx val;
config_filename := global_config_path;
config_data, read_ok := read_entire_file(config_filename);
url := "";
mode := "system_proxy";
update_interval := 3600;
if read_ok {
extracted_url := get_json_string_field(config_data, "\"_url\"");
if extracted_url url = copy_string(extracted_url);
extracted_mode := get_json_string_field(config_data, "\"_mode\"");
if extracted_mode {
mode = copy_string(extracted_mode);
} else {
mode = copy_string("system_proxy");
}
interval_str := get_json_val_field(config_data, "\"_update_interval\"");
if interval_str {
val, ok := to_integer(interval_str);
if ok update_interval = xx val;
}
updated := set_json_metadata(config_data, url, mode, port, update_interval);
write_entire_file(config_filename, updated);
free(config_data);
free(updated);
} else {
minimal_json := tprint("{\n \"_url\": \"\",\n \"_mode\": \"system_proxy\",\n \"_port\": %,\n \"_update_interval\": %,\n \"inbounds\": [],\n \"outbounds\": []\n}", port, update_interval);
write_entire_file(config_filename, minimal_json);
}
if url free(url);
free(mode);
state.url_saved = true;
}
}
DestroyWindow(hwnd);
return 0;
} else if control_id == IDCANCEL {
DestroyWindow(hwnd);
return 0;
}
}
case WM_CLOSE; {
DestroyWindow(hwnd);
return 0;
}
case WM_DESTROY; {
state.dialog_done = true;
// Wake up the dialog loop
PostThreadMessageW(GetCurrentThreadId(), WM_NULL, 0, 0);
return 0;
}
}
return DefWindowProcW(hwnd, msg, wparam, lparam);
}
}
hInstance := GetModuleHandleW(null);
class_name := utf8_to_wide("Sing-boxPortDialogClass");
wclass: WNDCLASSEXW;
wclass.cbSize = size_of(WNDCLASSEXW);
wclass.lpfnWndProc = port_dialog_proc;
wclass.hInstance = hInstance;
wclass.hCursor = LoadCursorW(null, IDC_ARROW);
wclass.lpszClassName = class_name;
RegisterClassExW(*wclass);
defer UnregisterClassW(class_name, hInstance);
screen_w := GetSystemMetrics(SM_CXSCREEN);
screen_h := GetSystemMetrics(SM_CYSCREEN);
dialog_w: s32 = 460;
dialog_h: s32 = 170;
dialog_x := (screen_w - dialog_w) / 2;
dialog_y := (screen_h - dialog_h) / 2;
state: Dialog_State;
state.is_test_mode = is_test_mode;
dialog_hwnd := CreateWindowExW(
WS_EX_DLGMODALFRAME,
class_name,
utf8_to_wide("Configure Sing-box Port"),
WS_POPUP | WS_CAPTION | WS_SYSMENU,
dialog_x, dialog_y, dialog_w, dialog_h,
parent_hwnd,
null,
hInstance,
*state
);
if !dialog_hwnd return false;
if parent_hwnd {
EnableWindow(parent_hwnd, FALSE);
}
ShowWindow(dialog_hwnd, SW_SHOW);
UpdateWindow(dialog_hwnd);
while !state.dialog_done {
msg: MSG;
if GetMessageW(*msg, null, 0, 0) {
if !IsDialogMessageW(dialog_hwnd, *msg) {
TranslateMessage(*msg);
DispatchMessageW(*msg);
}
} else {
PostQuitMessage(cast(s32) msg.wParam);
break;
}
}
if parent_hwnd {
EnableWindow(parent_hwnd, TRUE);
SetFocus(parent_hwnd);
}
return state.url_saved;
}
Download_Thread_Data :: struct {
url: string;
dest_path: string;
success: bool;
error_msg: string;
done: bool;
}
download_thread_proc :: (thread: *Thread) -> s64 {
data := cast(*Download_Thread_Data) thread.data;
data.success, data.error_msg = download_file(data.url, data.dest_path);
data.done = true;
return 0;
}
Download_Dialog_State :: struct {
dialog_done: bool;
thread: Thread;
thread_data: Download_Thread_Data;
static_hwnd: HWND;
}
download_dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT #c_call {
ctx: #Context;
push_context ctx {
state := cast(*Download_Dialog_State) GetWindowLongPtrW(hwnd, GWLP_USERDATA);
if msg == {
case WM_CREATE; {
create_struct := cast(*CREATESTRUCTW) lparam;
state = cast(*Download_Dialog_State) create_struct.lpCreateParams;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, cast(LONG_PTR) state);
hFont := GetStockObject(DEFAULT_GUI_FONT);
state.static_hwnd = CreateWindowExW(
0,
utf8_to_wide("STATIC"),
utf8_to_wide("Downloading Sing-box core executable...\nThis may take a moment, please wait."),
WS_CHILD | WS_VISIBLE | SS_LEFT,
30, 30, 400, 50,
hwnd,
null,
null,
null
);
SendMessageW(state.static_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
thread_init(*state.thread, download_thread_proc);
state.thread.data = *state.thread_data;
thread_start(*state.thread);
SetTimer(hwnd, 1, 100, null);
return 0;
}
case WM_TIMER; {
if wparam == 1 {
if state.thread_data.done {
KillTimer(hwnd, 1);
thread_is_done(*state.thread, -1);
// Capture error_msg (which may be temp-allocated inside download thread) BEFORE deinit frees the thread temp storage.
captured_success := state.thread_data.success;
captured_err := copy_string(state.thread_data.error_msg);
thread_deinit(*state.thread);
// Overwrite with safe copies (heap allocated) so that the post-loop return sees valid data.
state.thread_data.success = captured_success;
state.thread_data.error_msg = captured_err;
// Free the (copied) input paths now that thread is done with them.
if state.thread_data.url.count > 0 { free(state.thread_data.url); state.thread_data.url = ""; }
if state.thread_data.dest_path.count > 0 { free(state.thread_data.dest_path); state.thread_data.dest_path = ""; }
DestroyWindow(hwnd);
}
}
return 0;
}
case WM_CLOSE; {
if state.thread_data.done {
DestroyWindow(hwnd);
}
return 0;
}
case WM_DESTROY; {
if state {
state.dialog_done = true;
}
PostThreadMessageW(GetCurrentThreadId(), WM_NULL, 0, 0);
return 0;
}
}
return DefWindowProcW(hwnd, msg, wparam, lparam);
}
}
show_download_progress_dialog :: (parent_hwnd: HWND, url: string, dest_path: string) -> bool, string {
hInstance := GetModuleHandleW(null);
class_name := utf8_to_wide("Sing-boxDownloadDialogClass");
wclass: WNDCLASSEXW;
wclass.cbSize = size_of(WNDCLASSEXW);
wclass.style = CS_HREDRAW | CS_VREDRAW;
wclass.lpfnWndProc = download_dialog_proc;
wclass.hInstance = hInstance;
wclass.hCursor = LoadCursorW(null, IDC_ARROW);
wclass.hbrBackground = cast(HBRUSH) (COLOR_WINDOW + 1);
wclass.lpszClassName = class_name;
RegisterClassExW(*wclass);
defer UnregisterClassW(class_name, hInstance);
screen_w := GetSystemMetrics(SM_CXSCREEN);
screen_h := GetSystemMetrics(SM_CYSCREEN);
dialog_w: s32 = 460;
dialog_h: s32 = 150;
dialog_x := (screen_w - dialog_w) / 2;
dialog_y := (screen_h - dialog_h) / 2;
state: Download_Dialog_State;
state.thread_data.url = copy_string(url);
state.thread_data.dest_path = copy_string(dest_path);
dialog_hwnd := CreateWindowExW(
WS_EX_DLGMODALFRAME,
class_name,
utf8_to_wide("Downloading Sing-box Core"),
WS_POPUP | WS_CAPTION,
dialog_x, dialog_y, dialog_w, dialog_h,
parent_hwnd,
null,
hInstance,
*state
);
if !dialog_hwnd {
free(state.thread_data.url);
free(state.thread_data.dest_path);
return false, "Failed to create dialog window";
}
if parent_hwnd {
EnableWindow(parent_hwnd, FALSE);
}
ShowWindow(dialog_hwnd, SW_SHOW);
UpdateWindow(dialog_hwnd);
while !state.dialog_done {
msg: MSG;
if GetMessageW(*msg, null, 0, 0) {
if !IsDialogMessageW(dialog_hwnd, *msg) {
TranslateMessage(*msg);
DispatchMessageW(*msg);
}
} else {
PostQuitMessage(cast(s32) msg.wParam);
break;
}
}
if parent_hwnd {
EnableWindow(parent_hwnd, TRUE);
SetFocus(parent_hwnd);
}
return state.thread_data.success, state.thread_data.error_msg;
}