GajuMine/src/gmc_gui.erl
2025-05-26 16:25:39 +09:00

420 lines
14 KiB
Erlang

%%% @doc
%%% GajuMine GUI
%%% @end
-module(gmc_gui).
-vsn("0.1.4").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("Craig Everett <craigeverett@qpq.swiss>").
-license("GPL-3.0-or-later").
-behavior(wx_object).
-include_lib("wx/include/wx.hrl").
-export([ask_passphrase/0, set_account/1, difficulty/1, speed/1, candidate/1, message/1]).
-export([start_link/1]).
-export([init/1, terminate/2, code_change/3,
handle_call/3, handle_cast/2, handle_info/2, handle_event/2]).
-include("$zx_include/zx_logger.hrl").
-record(w,
{name = none :: atom(),
id = 0 :: integer(),
wx = none :: none | wx:wx_object()}).
-record(s,
{wx = none :: none | wx:wx_object(),
frame = none :: none | wx:wx_object(),
lang = en :: en | jp,
j = none :: none | fun(),
id = none :: none | wx:wx_object(),
diff = none :: none | wx:wx_object(),
perf = none :: none | wx:wx_object(),
hist = {ts(), 0} :: {LastTS :: integer(), Average :: integer()},
candy = none :: none | wx:wx_object(),
height = none :: none | wx:wx_object(),
block = none :: none | wx:wx_object(),
% solved = 0 :: non_neg_integer(), % Add a widget to show this. Maybe
mess = none :: none | wx:wx_object(),
buff = new_buff() :: buff(),
buttons = [] :: [#w{}]}).
ts() ->
erlang:system_time(microsecond).
new_buff() ->
{9, 0, 10, 0, []}.
-type buff() :: {OMax :: non_neg_integer(),
OCur :: non_neg_integer(),
IMax :: non_neg_integer(),
ICur :: non_neg_integer(),
Buff :: [[string()]]}.
%%% Interface functions
ask_passphrase() ->
wx_object:cast(?MODULE, ask_passphrase).
set_account(ID) ->
wx_object:cast(?MODULE, {set_account, ID}).
difficulty(N) ->
wx_object:cast(?MODULE, {difficulty, N}).
speed(N) ->
wx_object:cast(?MODULE, {speed, N}).
candidate(Block) ->
wx_object:cast(?MODULE, {candidate, Block}).
message(Terms) ->
wx_object:cast(?MODULE, {message, Terms}).
%%% Startup Functions
start_link(Title) ->
wx_object:start_link({local, ?MODULE}, ?MODULE, Title, []).
init(Prefs) ->
ok = log(info, "GUI starting..."),
Lang = maps:get(lang, Prefs, en_US),
Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans),
WX = wx:new(),
Frame = wxFrame:new(WX, ?wxID_ANY, J("GajuMine")),
MainSz = wxBoxSizer:new(?wxVERTICAL),
LeftRight = wxBoxSizer:new(?wxHORIZONTAL),
Left = wxBoxSizer:new(?wxVERTICAL),
Right = wxBoxSizer:new(?wxVERTICAL),
Labels = [J("ID"), J("Target"), J("Maps/s"), J("Candidate"), J("Height"), J("BlockHash")],
{Grid, [ID_C, DiffC, PerfC, CandyC, HeightC, BlockC]} = display_box(Frame, Labels),
Style = ?wxTE_MULTILINE bor ?wxTE_READONLY,
MessSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Messages")}]),
MessC = wxTextCtrl:new(Frame, ?wxID_ANY, [{style, Style}]),
_ = wxStaticBoxSizer:add(MessSz, MessC, zxw:flags(wide)),
ButtonTemplates =
[{start_stop, J("Start")},
{gajudesk, J("Open Wallet")},
{eureka, J("GajuMining")},
{explorer, J("ChainExplorer")}],
MakeButton =
fun({Name, Label}) ->
B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]),
#w{name = Name, id = wxButton:getId(B), wx = B}
end,
Buttons = lists:map(MakeButton, ButtonTemplates),
_ = wxBoxSizer:add(Left, Grid, zxw:flags(base)),
_ = wxBoxSizer:add(Left, MessSz, zxw:flags(wide)),
Add = fun(#w{wx = Button}) -> wxBoxSizer:add(Right, Button, zxw:flags(wide)) end,
ok = lists:foreach(Add, Buttons),
_ = wxBoxSizer:add(LeftRight, Left, zxw:flags(wide)),
_ = wxBoxSizer:add(LeftRight, Right, zxw:flags(base)),
_ = wxBoxSizer:add(MainSz, LeftRight, zxw:flags(wide)),
ok = wxFrame:setSizer(Frame, MainSz),
ok = wxSizer:layout(MainSz),
ok = wxFrame:connect(Frame, command_button_clicked),
ok = wxFrame:connect(Frame, close_window),
ok = wxFrame:setSize(Frame, {650, 300}),
ok = wxFrame:center(Frame),
true = wxFrame:show(Frame),
State =
#s{wx = WX, frame = Frame,
lang = Lang, j = J,
id = ID_C, diff = DiffC, perf = PerfC, candy = CandyC, height = HeightC, block = BlockC,
mess = MessC,
buttons = Buttons},
{Frame, State}.
display_box(Parent, Labels) ->
Grid = wxFlexGridSizer:new(2, 4, 4),
ok = wxFlexGridSizer:setFlexibleDirection(Grid, ?wxHORIZONTAL),
ok = wxFlexGridSizer:addGrowableCol(Grid, 1),
Make =
fun(S) ->
L = [S, ":"],
T_L = wxStaticText:new(Parent, ?wxID_ANY, L),
T_C = wxStaticText:new(Parent, ?wxID_ANY, "---", [{style, ?wxALIGN_LEFT}]),
_ = wxFlexGridSizer:add(Grid, T_L, zxw:flags(base)),
_ = wxFlexGridSizer:add(Grid, T_C, zxw:flags(wide)),
T_C
end,
Controls = lists:map(Make, Labels),
{Grid, Controls}.
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
{noreply, State}.
handle_cast(ask_passphrase, State) ->
ok = ask_passphrase(State),
{noreply, State};
handle_cast({set_account, ID}, State) ->
ok = set_account(ID, State),
{noreply, State};
handle_cast({difficulty, N}, State) ->
ok = difficulty(N, State),
{noreply, State};
handle_cast({speed, N}, State) ->
ok = speed(N, State),
{noreply, State};
handle_cast({candidate, Block}, State) ->
ok = candidate(Block, State),
{noreply, State};
handle_cast({message, Terms}, State) ->
NewState = do_message(Terms, State),
{noreply, NewState};
handle_cast(Unexpected, State) ->
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
{noreply, State}.
handle_info(Unexpected, State) ->
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
{noreply, State}.
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID},
State = #s{buttons = Buttons}) ->
NewState =
case lists:keyfind(ID, #w.id, Buttons) of
#w{name = start_stop} -> start_stop(State);
#w{name = gajudesk} -> gajudesk(State);
#w{name = eureka} -> eureka(State);
#w{name = explorer} -> explorer(State);
false -> State
end,
{noreply, NewState};
handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame}) ->
ok = retire(Frame),
{noreply, State};
handle_event(Event, State) ->
ok = tell(info, "Unexpected event ~tp", [Event]),
{noreply, State}.
code_change(_, State, _) ->
{ok, State}.
terminate(Reason, State) ->
ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]),
wx:destroy().
ask_passphrase(#s{frame = Frame, j = J}) ->
Label = J("Unlock Account"),
Dialog = wxDialog:new(Frame, ?wxID_ANY, Label, [{size, {500, 115}}]),
Sizer = wxBoxSizer:new(?wxVERTICAL),
PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Password")}]),
PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]),
_ = wxStaticBoxSizer:add(PassSz, PassTx, zxw:flags(wide)),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
Affirm = wxButton:new(Dialog, ?wxID_OK),
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
_ = wxBoxSizer:add(Sizer, PassSz, zxw:flags(base)),
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(wide)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxFrame:center(Dialog),
ok =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
Phrase = wxTextCtrl:getValue(PassTx),
gmc_con:unlock(Phrase);
?wxID_CANCEL ->
retire(Frame)
end,
wxDialog:destroy(Dialog).
set_account(ID, #s{id = Widget}) ->
wxStaticText:setLabel(Widget, ID).
difficulty(N, #s{diff = Widget}) ->
wxStaticText:setLabel(Widget, integer_to_list(N)).
speed(N, #s{perf = Widget}) ->
wxStaticText:setLabel(Widget, float_to_list(N)).
candidate(Block, #s{candy = Widget}) ->
wxStaticText:setLabel(Widget, Block).
do_message({notice, Notice}, State) ->
Entry = io_lib:format("~n~ts", [Notice]),
ok = log(info, Entry),
do_message2(Entry, State);
do_message({puzzle, #{info := {_, Diff, Nonce, _}}}, State = #s{diff = DiffT}) ->
DiffS = integer_to_list(Diff),
ok = wxStaticText:setLabel(DiffT, DiffS),
Entry = io_lib:format("~nTrying Nonce: ~p", [Nonce]),
do_message2(Entry, State);
do_message({result, #{info := Info}}, State = #s{perf = PerfT, hist = {LastTS, AvgDiff}}) ->
Now = ts(),
NewAvgDiff = avg20(Now, LastTS, AvgDiff),
PerfS = io_lib:format("~w", [1_000_000 / NewAvgDiff]),
ok = wxStaticText:setLabel(PerfT, PerfS),
NewState = State#s{hist = {Now, NewAvgDiff}},
case Info of
{error, no_solution} ->
NewState;
{ok, Cycles} ->
Entry = io_lib:format("~nFound! Reporting ~w cycles to leader.", [length(Cycles)]),
do_message2(Entry, NewState);
Other ->
Entry = io_lib:format("~nUnexpected 'result': ~tp", [Other]),
do_message2(Entry, NewState)
end;
do_message({pool_notification, #{info := #{msg := MSG}}}, State = #s{height = HeightT, block = BlockT, candy = CandT}) ->
case MSG of
#{new_generation := #{height := Height, block_hash := BlockHash}} ->
ok = wxStaticText:setLabel(HeightT, integer_to_list(Height)),
ok = wxStaticText:setLabel(BlockT, BlockHash),
State;
#{candidate := #{candidate := Candidate}} ->
ok = wxStaticText:setLabel(CandT, Candidate),
State;
#{solution_accepted := #{seq := Seq}} ->
Entry = io_lib:format("~nSolution Accepted! You solved one! Sequence: ~w", [Seq]),
do_message2(Entry, State);
Other ->
Entry = io_lib:format("~nUnexpected 'pool_notification': ~tp", [Other]),
do_message2(Entry, State)
end;
do_message({connected, _}, State) ->
Entry = "\nConnected!",
do_message2(Entry, State);
do_message({disconnected, _}, State) ->
Entry = "\nDisconnected!",
do_message2(Entry, State);
do_message(Terms, State) ->
tell(info, "~p", [Terms]),
Entry = io_lib:format("~n~tp", [Terms]),
do_message2(Entry, State).
avg20(Now, LastTS, AvgDiff) ->
Diff = Now - LastTS,
((AvgDiff * 19) + Diff) div 20.
do_message2(Entry, State = #s{mess = Mess, buff = Buff}) ->
NewBuff =
case os:type() of
{unix, _} ->
add_message(Entry, Mess, Buff);
{win32, nt} ->
ok = wxTextCtrl:freeze(Mess),
Updated = add_message(Entry, Mess, Buff),
Last = wxTextCtrl:getLastPosition(Mess),
ok = wxTextCtrl:showPosition(Mess, Last),
ok = wxTextCtrl:thaw(Mess),
Updated
end,
State#s{buff = NewBuff}.
add_message(Entry, Mess, Buff) ->
case add_message2(Entry, Buff) of
{flush, Updated} ->
String = unicode:characters_to_list(lists:reverse(element(5, Updated))),
ok = wxTextCtrl:changeValue(Mess, String),
Last = wxTextCtrl:getLastPosition(Mess),
ok = wxTextCtrl:showPosition(Mess, Last),
ok = wxTextCtrl:thaw(Mess),
Updated;
{append, Updated} ->
ok = wxTextCtrl:appendText(Mess, Entry),
Updated
end.
add_message2(Entry, {OMax, OMax, IMax, IMax, [H | T]}) ->
{flush, {OMax, OMax, IMax, 1, [[Entry] , lists:reverse(H) | lists:droplast(T)]}};
add_message2(Entry, {OMax, OMax, IMax, ICur, [H | Buff]}) ->
{append, {OMax, OMax, IMax, ICur + 1, [[Entry | H] | Buff]}};
add_message2(Entry, {OMax, OCur, IMax, IMax, [H | T]}) ->
{append, {OMax, OCur + 1, IMax, 1, [[Entry], lists:reverse(H) | T]}};
add_message2(Entry, {OMax, OCur, IMax, ICur, [H | Buff]}) ->
{append, {OMax, OCur, IMax, ICur + 1, [[Entry | H] | Buff]}};
add_message2(Entry, {OMax, 0, IMax, 0, []}) ->
{append, {OMax, 1, IMax, 1, [[Entry]]}}.
start_stop(State) ->
ok = gmc_con:start_stop(),
State.
gajudesk(State) ->
ok = gmc_con:gajudesk(),
State.
eureka(State = #s{frame = Frame, j = J}) ->
ok = tell(info, "Opening Eureka"),
ok = open_browser(Frame, J, eureka_url()),
State.
eureka_url() ->
case gmc_con:network() of
<<"testnet">> -> "https://test.gajumining.com";
_ -> "https://gajumining.com"
end.
explorer(State = #s{frame = Frame, j = J, id = ID}) ->
ok = tell(info, "Opening Explorer"),
URL =
case wxStaticText:getLabel(ID) of
"" -> explorer_url();
AccountID -> unicode:characters_to_list([explorer_url(), "/account/", AccountID])
end,
ok = open_browser(Frame, J, URL),
State.
explorer_url() ->
case gmc_con:network() of
none -> "https://groot.mainnet.gajumaru.io";
Network -> unicode:characters_to_list(["https://groot.", Network, ".gajumaru.io"])
end.
open_browser(Frame, J, URL) ->
case wx_misc:launchDefaultBrowser(URL) of
true ->
ok;
false ->
Format = J("Trouble launching browser.\nOpen URL here: ~s"),
Message = io_lib:format(Format, [URL]),
zxw:show_message(Frame, Message)
end.
retire(Frame) ->
ok = gmc_con:stop(),
wxWindow:destroy(Frame).