%%% @doc %%% GajuMine GUI %%% @end -module(gmc_gui). -vsn("0.1.4"). -author("Craig Everett "). -copyright("Craig Everett "). -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).