-module(gd_v_devman). -vsn("0.9.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). -behavior(wx_object). %-behavior(gd_v). -include_lib("wx/include/wx.hrl"). -export([to_front/1]). -export([set_manifest/1, open_contract/1, write_console/2, call_result/2, dryrun_result/2, trouble/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"). -include("gd.hrl"). -include("gdl.hrl"). % Contract functions in an ACI -record(f, {name = <<"">> :: binary(), call = #w{} :: #w{}, dryrun = #w{} :: none | #w{}, args = [] :: [argt()]}). % Code book pages -record(p, {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, win = none :: none | wx:wx_object(), code = none :: none | wxTextCtrl:wxTextCtrl()}). % Contract pages -record(c, {id = <<"">> :: binary(), win = none :: none | wx:wx_object(), code = none :: none | wxTextCtrl:wxTextCtrl(), cons = none :: none | wxTextCtrl:wxTextCtrl(), build = none :: none | map(), funs = {#w{}, []} :: {#w{}, [#f{}]}}). % State -record(s, {wx = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(), lang = en :: en | jp, j = none :: none | fun(), prefs = #{} :: map(), buttons = #{} :: #{WX_ID :: integer() := #w{}}, tabs = none :: none | wx:wx_object(), code = {none, []} :: {Codebook :: none | wx:wx_object(), Pages :: [#p{}]}, cons = {none, []} :: {Consbook :: none | wx:wx_object(), Pages :: [#c{}]}}). % TODO: Spec HZ AACIs. -type argt() :: term(). % FIXME: Whatever HZ returns in the AACI as an arg type. %%% Interface -spec to_front(Win) -> ok when Win :: wx:wx_object(). to_front(Win) -> wx_object:cast(Win, to_front). % TODO: Probably kill this -spec set_manifest(Entries) -> ok when Entries :: list(). set_manifest(Entries) -> case is_pid(whereis(?MODULE)) of true -> wx_object:cast(?MODULE, {set_manifest, Entries}); false -> ok end. -spec open_contract(Address) -> ok when Address :: binary() | string(). open_contract(Address) when is_binary(Address) -> wx_object:cast(?MODULE, {open_contract, Address}); open_contract(Address) when is_list(Address) -> open_contract(list_to_binary(Address)). -spec write_console(ConID, Message) -> ok when ConID :: gajudesk:id(), Message :: unicode:chardata(). write_console(ConID, Message) -> wx_object:cast(?MODULE, {write_console, ConID, Message}). -spec call_result(ConID, CallInfo) -> ok when ConID :: gajudesk:id(), CallInfo :: map(). call_result(ConID, CallInfo) -> wx_object:cast(?MODULE, {call_result, ConID, CallInfo}). -spec dryrun_result(ConID, CallInfo) -> ok when ConID :: gajudesk:id(), CallInfo :: map(). dryrun_result(ConID, CallInfo) -> wx_object:cast(?MODULE, {dryrun_result, ConID, CallInfo}). -spec trouble(Info) -> ok when Info :: term(). trouble(Info) -> wx_object:cast(?MODULE, {trouble, Info}). %%% Startup Functions start_link(Args) -> wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []). init({Prefs, Manifest}) -> Lang = maps:get(lang, Prefs, en), Trans = gd_jt:read_translations(?MODULE), J = gd_jt:j(Lang, Trans), Wx = wx:new(), Frame = wxFrame:new(Wx, ?wxID_ANY, J("Contracts")), MainSz = wxBoxSizer:new(?wxVERTICAL), TopBook = wxNotebook:new(Frame, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), {LWin, LButtons, Codebook} = make_l_win(TopBook, J), {RWin, RButtons, Consbook} = make_r_win(TopBook, J), ButtonMap = maps:merge(LButtons, RButtons), true = wxNotebook:addPage(TopBook, LWin, J("Contract Editor"), []), true = wxNotebook:addPage(TopBook, RWin, J("Deployed Contracts"), []), State = #s{wx = Wx, frame = Frame, j = J, prefs = Prefs, buttons = ButtonMap, tabs = TopBook, code = {Codebook, []}, cons = {Consbook, []}}, _ = wxSizer:add(MainSz, TopBook, zxw:flags(wide)), _ = wxFrame:setSizer(Frame, MainSz), _ = wxSizer:layout(MainSz), ok = gd_v:safe_size(Frame, Prefs), ok = wxFrame:connect(Frame, close_window), ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:center(Frame), true = wxFrame:show(Frame), NewState = add_code_pages(State, Manifest), {Frame, NewState}. make_l_win(TopBook, J) -> Win = wxWindow:new(TopBook, ?wxID_ANY), MainSz = wxBoxSizer:new(?wxVERTICAL), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), ButtonTemplates = [{new, J("New")}, {open, J("Open")}, {save, J("Save")}, {rename, J("Save (rename)")}, {deploy, J("Deploy")}, {close_source, J("Close")}], MakeButton = fun({Name, Label}) -> B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), #w{name = Name, id = wxButton:getId(B), wx = B} end, Buttons = lists:map(MakeButton, ButtonTemplates), AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), ok = lists:foreach(AddButton, Buttons), _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), _ = wxWindow:setSizer(Win, MainSz), MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, ButtonMap = lists:foldl(MapButton, #{}, Buttons), {Win, ButtonMap, Codebook}. make_r_win(TopBook, J) -> Win = wxWindow:new(TopBook, ?wxID_ANY), MainSz = wxBoxSizer:new(?wxVERTICAL), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), ButtonTemplates = [{load, J("Load from Chain")}, {edit, J("Copy to Editor")}, {close_instance, J("Close")}], MakeButton = fun({Name, Label}) -> B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), #w{name = Name, id = wxButton:getId(B), wx = B} end, Buttons = lists:map(MakeButton, ButtonTemplates), AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), ok = lists:foreach(AddButton, Buttons), _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), _ = wxWindow:setSizer(Win, MainSz), MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, ButtonMap = lists:foldl(MapButton, #{}, Buttons), {Win, ButtonMap, Codebook}. add_code_pages(State, Files) -> lists:foldl(fun add_code_page/2, State, Files). %%% OTP callbacks handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. handle_cast(to_front, State = #s{frame = Frame}) -> ok = wxFrame:raise(Frame), {noreply, State}; handle_cast({open_contract, Address}, State) -> NewState = load2(State, Address), {noreply, NewState}; handle_cast({write_console, ConID, Message}, State) -> ok = do_write_console(State, ConID, Message), {noreply, State}; handle_cast({call_result, ConID, CallInfo}, State) -> ok = do_call_result(State, ConID, CallInfo), {noreply, State}; handle_cast({dryrun_result, ConID, CallInfo}, State) -> ok = do_dryrun_result(State, ConID, CallInfo), {noreply, State}; handle_cast({trouble, Info}, State) -> NewState = handle_troubling(State, Info), {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(E = #wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{buttons = Buttons}) -> NewState = case maps:get(ID, Buttons, undefined) of #w{name = new} -> new_file(State); #w{name = open} -> open(State); #w{name = save} -> save(State); #w{name = rename} -> rename(State); #w{name = deploy} -> deploy(State); #w{name = close_source} -> close_source(State); #w{name = load} -> load(State); #w{name = edit} -> edit(State); #w{name = close_instance} -> close_instance(State); #w{name = Name} -> clicked(State, Name); undefined -> tell("Received message: ~w", [E]), State end, {noreply, NewState}; handle_event(#wx{event = Event = #wxStyledText{type = stc_styleneeded}, obj = Win}, State) -> ok = style(State, Win, Event), {noreply, State}; handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) -> Geometry = case wxTopLevelWindow:isMaximized(Frame) of true -> max; false -> {X, Y} = wxWindow:getPosition(Frame), {W, H} = wxWindow:getSize(Frame), {X, Y, W, H} end, NewPrefs = maps:put(geometry, Geometry, Prefs), ok = gd_con:save(?MODULE, NewPrefs), ok = wxWindow:destroy(Frame), {noreply, State}; handle_event(Event, State) -> ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), {noreply, State}. handle_troubling(State = #s{frame = Frame}, Info) -> ok = zxw:show_message(Frame, Info), State. code_change(_, State, _) -> {ok, State}. terminate(wx_deleted, _) -> wx:destroy(); terminate(Reason, State) -> ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), wx:destroy(). %%% Doers style(#s{code = {_, Pages}}, Win, Event) -> case lists:keyfind(Win, #p.win, Pages) of #p{code = STC} -> gd_sophia_editor:update(Event, STC); false -> tell("Received bogus style event.~nWin: ~p~nEvent: ~p", [Win, Event]) end. clicked(State = #s{cons = {Consbook, Contracts}}, FunDef) -> ok = case wxNotebook:getSelection(Consbook) of ?wxNOT_FOUND -> tell(warning, "Inconcievable! No deployed contract page is selected!"); Index -> #c{id = ConID, build = Build} = lists:nth(Index + 1, Contracts), gd_con:prompt_call(FunDef, ConID, Build) end, State. do_write_console(#s{tabs = TopBook, cons = {Consbook, Contracts}}, ConID, Message) -> case lookup_contract(ConID, Contracts) of {#c{cons = Console}, ZeroIndex} -> _ = wxNotebook:changeSelection(TopBook, 1), _ = wxNotebook:changeSelection(Consbook, ZeroIndex), Out = [Message, "\n\n"], wxTextCtrl:appendText(Console, Out); error -> tell(info, "Received result for ~p:~n~p ", [ConID, Message]) end. do_call_result(State, ConID, CallInfo) -> Message = io_lib:format("Call Result:~n~p", [CallInfo]), do_write_console(State, ConID, Message). do_dryrun_result(State, ConID, CallInfo) -> Message = io_lib:format("Call Result:~n~p", [CallInfo]), do_write_console(State, ConID, Message). lookup_contract(ConID, Contracts) -> lookup_contract(ConID, Contracts, 0). lookup_contract(ConID, [Contract = #c{id = ConID} | _], I) -> {Contract, I}; lookup_contract(ConID, [#c{} | T], I) -> lookup_contract(ConID, T, I + 1); lookup_contract(_, [], _) -> error. add_code_page(State = #s{code = {Codebook, Pages}}, File) -> case keyfind_index({file, File}, #p.path, Pages) of error -> add_code_page2(State, File); {ok, Index} -> _ = wxNotebook:setSelection(Codebook, Index - 1), State end. add_code_page2(State = #s{j = J}, {file, File}) -> case file:read_file(File) of {ok, Bin} -> case unicode:characters_to_list(Bin) of Code when is_list(Code) -> add_code_page(State, {file, File}, Code); Error -> Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Error]), handle_troubling(State, Message) end; {error, Reason} -> Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Reason]), handle_troubling(State, Message) end; add_code_page2(State, {hash, Address}) -> open_hash2(State, Address). add_code_page(State = #s{tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> Window = wxWindow:new(Codebook, ?wxID_ANY), PageSz = wxBoxSizer:new(?wxHORIZONTAL), CodeTx = gd_sophia_editor:new(Window), ok = gd_sophia_editor:set_text(CodeTx, Code), _ = wxSizer:add(PageSz, CodeTx, zxw:flags(wide)), ok = wxWindow:setSizer(Window, PageSz), ok = wxSizer:layout(PageSz), _ = wxNotebook:changeSelection(TopBook, 0), FileName = case Location of {file, Path} -> filename:basename(Path); {hash, Addr} -> Addr end, ok = wxStyledTextCtrl:connect(Window, stc_styleneeded), true = wxNotebook:addPage(Codebook, Window, FileName, [{bSelect, true}]), Page = #p{path = Location, win = Window, code = CodeTx}, NewPages = Pages ++ [Page], State#s{code = {Codebook, NewPages}}. new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = case maps:find(dir, Prefs) of {ok, PrefDir} -> PrefDir; error -> case os:getenv("ZOMP_DIR") of "" -> file:get_pwd(); D -> filename:basename(D) end end, Options = [{message, J("Save Location")}, {defaultDir, DefaultDir}, {defaultFile, "my_contract.aes"}, {wildCard, "*.aes"}, {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], Dialog = wxFileDialog:new(Frame, Options), NewState = case wxFileDialog:showModal(Dialog) of ?wxID_OK -> Dir = wxFileDialog:getDirectory(Dialog), case wxFileDialog:getFilename(Dialog) of "" -> State; Name -> File = case filename:extension(Name) of ".aes" -> Name; _ -> Name ++ ".aes" end, Path = filename:join(Dir, File), NewPrefs = maps:put(dir, Dir, Prefs), NextState = State#s{prefs = NewPrefs}, add_code_page(NextState, {file, Path}, "") end; ?wxID_CANCEL -> State end, ok = wxFileDialog:destroy(Dialog), NewState. deploy(State = #s{code = {Codebook, Pages}}) -> case wxNotebook:getSelection(Codebook) of ?wxNOT_FOUND -> State; Index -> #p{code = CodeTx} = lists:nth(Index + 1, Pages), Source = gd_sophia_editor:get_text(CodeTx), deploy2(State, Source) end. deploy2(State, Source) -> ok = case compile(Source) of {ok, Build} -> gd_con:prompt_call({"init", init}, init, Build); Other -> tell(info, "Compilation Failed!~n~tp", [Other]) end, State. open(State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Open Contract Source")), Sizer = wxBoxSizer:new(?wxVERTICAL), Choices = wxRadioBox:new(Dialog, ?wxID_ANY, J("Select Origin"), ?wxDefaultPosition, ?wxDefaultSize, [J("From File"), J("From Hash")], [{majorDim, 1}]), 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)), _ = wxSizer:add(Sizer, Choices, zxw:flags(wide)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxDialog:setSize(Dialog, {250, 170}), ok = wxBoxSizer:layout(Sizer), Choice = case wxDialog:showModal(Dialog) of ?wxID_OK -> case wxRadioBox:getSelection(Choices) of 0 -> file; 1 -> hash; ?wxNOT_FOUND -> none end; ?wxID_CANCEL -> cancel end, ok = wxDialog:destroy(Dialog), case Choice of file -> open_file(State); hash -> open_hash(State); none -> ok = tell(info, "No selection."), State; cancel -> ok = tell(info, "Cancelled."), State end. open_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = case maps:find(dir, Prefs) of {ok, PrefDir} -> PrefDir; error -> case os:getenv("ZOMP_DIR") of "" -> file:get_pwd(); D -> filename:basename(D) end end, Options = [{message, J("Load Contract Source")}, {defaultDir, DefaultDir}, {wildCard, "*.aes"}, {style, ?wxFD_OPEN}], Dialog = wxFileDialog:new(Frame, Options), NewState = case wxFileDialog:showModal(Dialog) of ?wxID_OK -> Dir = wxFileDialog:getDirectory(Dialog), case wxFileDialog:getFilename(Dialog) of "" -> State; Name -> File = case filename:extension(Name) of ".aes" -> Name; _ -> Name ++ ".aes" end, Path = filename:join(Dir, File), NewPrefs = maps:put(dir, Dir, Prefs), NextState = State#s{prefs = NewPrefs}, add_code_page(NextState, {file, Path}) end; ?wxID_CANCEL -> State end, ok = wxFileDialog:destroy(Dialog), NewState. open_hash(State = #s{frame = Frame, j = J}) -> Title = J("Retrieve Contract Source"), Label = J("Address Hash"), case zxw_modal_text:show(Frame, Title, [{label, Label}]) of {ok, Address = "ct_" ++ _} -> open_hash2(State, list_to_binary(Address)); {ok, Address = "th_" ++ _} -> get_contract_from_tx(State, list_to_binary(Address)); {ok, Turd} -> handle_troubling(State, {bad_address, Turd}); cancel -> State end. get_contract_from_tx(State, Address) -> case hz:tx_info(Address) of {ok, #{"call_info" := #{"contract_id" := Contract}}} -> open_hash2(State, Contract); {ok, Other} -> handle_troubling(State, {bad_address, Other}); Error -> handle_troubling(State, Error) end. open_hash2(State, Address) -> case hz:contract_source(Address) of {project, [{Name, Source}]} -> ok = tell("Retrieved ~p from ~p", [Name, Address]), open_hash3(State, Address, Source); {ok, Source} -> ok = tell("Retrieved uncompressed source of ~p", [Address]), open_hash3(State, Address, Source); Error -> handle_troubling(State, Error) end. open_hash3(State, Address, Source) -> % TODO: Compile on load and verify the deployed hash for validity. case compile(Source) of {ok, _Build} -> add_code_page(State, {hash, Address}, Source); Other -> ok = tell(info, "Compilation Failed!~n~tp", [Other]), State end. save(State = #s{prefs = Prefs, code = {Codebook, Pages}}) -> case wxNotebook:getSelection(Codebook) of ?wxNOT_FOUND -> State; Index -> case lists:nth(Index + 1, Pages) of #p{path = {file, Path}, code = Widget} -> Source = gd_sophia_editor:get_text(Widget), case filelib:ensure_dir(Path) of ok -> case file:write_file(Path, Source) of ok -> State; Error -> handle_troubling(State, Error) end; Error -> handle_troubling(State, Error) end; Page = #p{path = {hash, Hash}, code = Widget} -> DefDir = case maps:find(dir, Prefs) of {ok, PrefDir} -> PrefDir; error -> case os:getenv("ZOMP_DIR") of "" -> file:get_pwd(); D -> filename:basename(D) end end, DefName = unicode:characters_to_list([Hash, ".aes"]), save_dialog(State, DefDir, DefName, Widget, Index, Page) end end. rename(State = #s{code = {Codebook, Pages}}) -> case wxNotebook:getSelection(Codebook) of ?wxNOT_FOUND -> State; Index -> case lists:nth(Index + 1, Pages) of Page = #p{path = {file, Path}, code = Widget} -> DefDir = filename:dirname(Path), DefName = filename:basename(Path), save_dialog(State, DefDir, DefName, Widget, Index, Page); #p{path = {hash, _}} -> save(State) end end. save_dialog(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}, DefDir, DefName, Widget, Index, Page) -> Options = [{message, J("Save Location")}, {defaultDir, DefDir}, {defaultFile, DefName}, {wildCard, "*.aes"}, {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], Dialog = wxFileDialog:new(Frame, Options), NewState = case wxFileDialog:showModal(Dialog) of ?wxID_OK -> Dir = wxFileDialog:getDirectory(Dialog), case wxFileDialog:getFilename(Dialog) of "" -> State; Name -> File = case filename:extension(Name) of ".aes" -> Name; _ -> Name ++ ".aes" end, NewPath = filename:join(Dir, File), Source = gd_sophia_editor:get_text(Widget), case filelib:ensure_dir(NewPath) of ok -> case file:write_file(NewPath, Source) of ok -> true = wxNotebook:setPageText(Codebook, Index, File), NewPrefs = maps:put(dir, Dir, Prefs), NewPage = Page#p{path = {file, NewPath}}, NewPages = store_nth(Index + 1, NewPage, Pages), NewCode = {Codebook, NewPages}, State#s{prefs = NewPrefs, code = NewCode}; Error -> handle_troubling(State, Error) end; Error -> handle_troubling(State, Error) end end; ?wxID_CANCEL -> State end, ok = wxFileDialog:destroy(Dialog), NewState. close_source(State = #s{code = {Codebook, Pages}}) -> case wxNotebook:getSelection(Codebook) of ?wxNOT_FOUND -> State; Index -> NewPages = drop_nth(Index + 1, Pages), true = wxNotebook:deletePage(Codebook, Index), State#s{code = {Codebook, NewPages}} end. load(State = #s{frame = Frame, j = J}) -> % TODO: Extract the exact compiler version, load it, and use only that or fail if % the specific version is unavailable. % TODO: Compile on load and verify the deployed hash for validity. Title = J("Retrieve Contract Source"), Label = J("Address Hash"), case zxw_modal_text:show(Frame, Title, [{label, Label}]) of {ok, Address = "ct_" ++ _} -> load2(State, Address); {ok, Address = "th_" ++ _} -> load_from_tx(State, Address); {ok, Turd} -> handle_troubling(State, {bad_address, Turd}); cancel -> State end. load_from_tx(State, Address) -> case hz:tx_info(Address) of {ok, #{"call_info" := #{"contract_id" := Contract}}} -> load2(State, Contract); {ok, Other} -> handle_troubling(State, {bad_address, Other}); Error -> handle_troubling(State, Error) end. load2(State = #s{cons = {_, Pages}}, Address) when is_binary(Address) -> case lists:keyfind(Address, #c.id, Pages) of false -> load3(State, Address); #c{} -> State end; load2(State, Address) when is_list(Address) -> load2(State, list_to_binary(Address)). load3(State, Address) -> case hz:contract_source(Address) of {project, [{Name, Source}]} -> ok = tell("Retrieved ~p from ~p", [Name, Address]), load4(State, Address, Source); {ok, Source} -> ok = tell("Retrieved uncompressed source of ~p", [Address]), load4(State, Address, Source); Error -> handle_troubling(State, Error) end. load4(State = #s{tabs = TopBook, cons = {Consbook, Pages}, buttons = Buttons, j = J}, Address, Source) -> Window = wxWindow:new(Consbook, ?wxID_ANY), PageSz = wxBoxSizer:new(?wxVERTICAL), ProgSz = wxBoxSizer:new(?wxHORIZONTAL), CodeSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Contract Source")}]), CodeTx = gd_sophia_editor:new(Window), ok = gd_sophia_editor:set_text(CodeTx, Source), ok = gd_sophia_editor:update(CodeTx), _ = wxSizer:add(CodeSz, CodeTx, zxw:flags(wide)), ScrollWin = wxScrolledWindow:new(Window), FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Function Interfaces")}]), ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSz), ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), ConsSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Console")}]), ConsTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}, ConsTx = wxTextCtrl:new(Window, ?wxID_ANY, [ConsTxStyle]), _ = wxSizer:add(ConsSz, ConsTx, zxw:flags({wide, 5})), _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 3}, {flag, ?wxEXPAND bor ?wxALL}, {border, 5}]), _ = wxSizer:add(ProgSz, ScrollWin, [{proportion, 1}, {flag, ?wxEXPAND bor ?wxALL}, {border, 5}]), _ = wxSizer:add(PageSz, ProgSz, [{proportion, 3}, {flag, ?wxEXPAND bor ?wxALL}, {border, 5}]), _ = wxSizer:add(PageSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND bor ?wxALL}, {border, 5}]), {Out, IFaces, Build, NewButtons} = case compile(Source) of {ok, Output} -> {aaci, _, Funs, _} = maps:get(aaci, Output), Callable = maps:remove("init", Funs), {NB, IFs} = fun_interfaces(ScrollWin, FunSz, Buttons, Callable, J), O = io_lib:format("Compilation Succeeded!~n~tp~n~nDone!~n", [Output]), {O, IFs, Output, NB}; Other -> O = io_lib:format("Compilation Failed!~n~tp~n", [Other]), {O, [], none, Buttons} end, ok = wxWindow:setSizer(Window, PageSz), ok = wxSizer:layout(PageSz), true = wxNotebook:addPage(Consbook, Window, Address, [{bSelect, true}]), Page = #c{id = Address, win = Window, code = CodeTx, cons = ConsTx, build = Build, funs = {ScrollWin, IFaces}}, NewPages = Pages ++ [Page], ok = wxTextCtrl:appendText(ConsTx, Out), _ = wxNotebook:changeSelection(TopBook, 1), % TODO: Verify the deployed hash for validity. State#s{cons = {Consbook, NewPages}, buttons = NewButtons}. fun_interfaces(ScrollWin, FunSz, Buttons, Funs, J) -> MakeIface = fun(Name, {Args, _}) -> FunName = unicode:characters_to_list([Name, "/", integer_to_list(length(Args))]), FS = wxBoxSizer:new(?wxHORIZONTAL), FLabel = wxStaticText:new(ScrollWin, ?wxID_ANY, FunName), CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]), DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]), _ = wxBoxSizer:add(FS, FLabel, zxw:flags(wide)), _ = wxBoxSizer:add(FS, CallBn, zxw:flags(base)), _ = wxBoxSizer:add(FS, DryRBn, zxw:flags(base)), CallButton = #w{name = {Name, call}, id = wxButton:getId(CallBn), wx = CallBn}, DryRButton = #w{name = {Name, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}, _ = wxSizer:add(FunSz, FS, zxw:flags({base, 5})), #f{name = Name, call = CallButton, dryrun = DryRButton, args = Args} end, Iterator = maps:iterator(Funs, ordered), IFaces = maps:map(MakeIface, Iterator), NewButtons = maps:fold(fun map_iface_buttons/3, Buttons, IFaces), {NewButtons, IFaces}. map_iface_buttons(_, #f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) -> maps:merge(#{CID => C, DID => D}, A); map_iface_buttons(_, #f{call = C = #w{id = CID}, dryrun = none}, A) -> maps:put(CID, C, A). edit(State = #s{cons = {Consbook, Pages}}) -> case wxNotebook:getSelection(Consbook) of ?wxNOT_FOUND -> State; Index -> #c{code = CodeTx} = lists:nth(Index + 1, Pages), Address = wxNotebook:getPageText(Consbook, Index), Source = gd_sophia_editor:get_text(CodeTx), add_code_page(State, {hash, Address}, Source) end. close_instance(State = #s{cons = {Consbook, Pages}, buttons = Buttons}) -> case wxNotebook:getSelection(Consbook) of ?wxNOT_FOUND -> State; Index -> {#c{funs = {_, IFaces}}, NewPages} = take_nth(Index + 1, Pages), log(info, "IFaces: ~tp", [IFaces]), IDs = list_iface_buttons(maps:values(IFaces)), NewButtons = maps:without(IDs, Buttons), true = wxNotebook:deletePage(Consbook, Index), State#s{cons = {Consbook, NewPages}, buttons = NewButtons} end. list_iface_buttons(IFaces) -> lists:foldl(fun list_iface_buttons/2, [], IFaces). list_iface_buttons(#f{call = #w{id = CID}, dryrun = #w{id = DID}}, A) -> [CID, DID | A]. %% Incomplete compiler wrangling compile(Source) -> Options = sophia_options(), case so_compiler:from_string(Source, Options) of {ok, Build} -> ACI = maps:get(aci, Build), AACI = hz_aaci:prepare(ACI), Complete = maps:put(aaci, AACI, Build), {ok, Complete}; Other -> Other end. sophia_options() -> [{aci, json}]. %% (Somewhat silly) Data operations store_nth(1, E, [_ | T]) -> [E | T]; store_nth(N, E, [H | T]) -> [H | store_nth(N - 1, E, T)]. drop_nth(1, [_ | T]) -> T; drop_nth(N, [H | T]) -> [H | drop_nth(N - 1, T)]. take_nth(N, L) -> take_nth(N, L, []). take_nth(1, [E | T], A) -> {E, lists:reverse(A) ++ T}; take_nth(N, [H | T], A) -> take_nth(N - 1, T, [H | A]). keyfind_index(K, E, L) -> keyfind_index(K, E, 1, L). keyfind_index(K, E, I, [H | T]) -> case element(E, H) =:= K of false -> keyfind_index(K, E, I + 1, T); true -> {ok, I} end; keyfind_index(_, _, _, []) -> error.