GajuDesk/src/gd_v_devman.erl
2025-03-31 15:36:31 +09:00

1287 lines
50 KiB
Erlang

-module(gd_v_devman).
-vsn("0.5.4").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-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, 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").
-define(editorMode, sophia).
% Widgets
-record(w,
{name = none :: atom() | {FunName :: binary(), call | dryr},
id = 0 :: integer(),
wx = none :: none | wx:wx_object()}).
% Contract functions in an ACI
-record(f,
{name = <<"">> :: binary(),
call = #w{} :: #w{},
dryrun = #w{} :: none | #w{},
args = [] :: [{wx:wx_object(), wx:wx_object(), 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{}]}}).
-type argt() :: int | string | address | list(argt()).
%%% 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 :: string().
open_contract(Address) ->
wx_object:cast(?MODULE, {open_contract, Address}).
-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_us),
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({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) ->
ok = handle_troubling(State, Info),
{noreply, State};
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(#s{frame = Frame}, Info) ->
zxw:show_message(Frame, Info).
code_change(_, State, _) ->
{ok, State}.
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}}, Name) ->
case wxNotebook:getSelection(Consbook) of
?wxNOT_FOUND ->
ok = tell(warning, "Inconcievable! No notebook page is selected!"),
State;
Index ->
Contract = lists:nth(Index + 1, Contracts),
clicked2(State, Contract, Name)
end.
clicked2(State, Contract, Name) ->
case gd_con:list_keys() of
{ok, 0, []} ->
handle_troubling(State, "No keys exist in the current wallet.");
{ok, Selected, Keys} ->
clicked3(State, Contract, Name, Selected, Keys);
error ->
handle_troubling(State, "No wallet is selected!")
end.
clicked3(State = #s{frame = Frame, j = J}, Contract, Name, Selected, Keys) ->
Label =
case element(2, Name) of
call -> "Contract Call";
dryr -> "Dry Run"
end,
Dialog = wxDialog:new(Frame, ?wxID_ANY, J(Label)),
Sizer = wxBoxSizer:new(?wxVERTICAL),
KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Key")}]),
KeyPicker = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Keys}]),
_ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)),
ok = wxChoice:setSelection(KeyPicker, Selected - 1),
{ParamSz, TTL_Tx, GasP_Tx, Gas_Tx, Amount_Tx} = call_param_sizer(Dialog, J),
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, KeySz, zxw:flags(wide)),
_ = wxSizer:add(Sizer, ParamSz, zxw:flags(wide)),
_ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxDialog:setSize(Dialog, {500, 300}),
ok = wxDialog:center(Dialog),
Outcome =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)),
PK = unicode:characters_to_binary(ID),
Controls =
[{"TTL", TTL_Tx},
{"Gas Price", GasP_Tx},
{"Gas", Gas_Tx},
{"Amount", Amount_Tx}],
case call_params(Controls) of
{ok, [TTL, GasP, Gas, Amount]} ->
{ok, Nonce} = hz:next_nonce(PK),
{ok, {PK, Nonce, TTL, GasP, Gas, Amount}};
E ->
E
end;
?wxID_CANCEL ->
cancel
end,
ok = wxDialog:destroy(Dialog),
case Outcome of
{ok, Params} -> clicked4(State, Contract, Name, Params);
cancel -> State;
Error -> handle_troubling(State, Error)
end.
call_param_sizer(Dialog, J) ->
{ok, Height} = hz:top_height(),
DefTTL = Height + 10000,
DefGasP = hz:min_gas_price(),
DefGas = 5000000,
DefAmount = 0,
ParamSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("TX Parameters")}]),
GridSz = wxFlexGridSizer:new(2, 4, 4),
ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL),
ok = wxFlexGridSizer:addGrowableCol(GridSz, 1),
TTL_L = wxStaticText:new(Dialog, ?wxID_ANY, "TTL"),
TTL_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY),
ok = wxTextCtrl:setValue(TTL_Tx, integer_to_list(DefTTL)),
GasP_L = wxStaticText:new(Dialog, ?wxID_ANY, J("Gas Price")),
GasP_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY),
ok = wxTextCtrl:setValue(GasP_Tx, integer_to_list(DefGasP)),
Gas_L = wxStaticText:new(Dialog, ?wxID_ANY, J("Gas")),
Gas_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY),
ok = wxTextCtrl:setValue(Gas_Tx, integer_to_list(DefGas)),
Amount_L = wxStaticText:new(Dialog, ?wxID_ANY, J("TX Amount")),
Amount_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY),
ok = wxTextCtrl:setValue(Amount_Tx, integer_to_list(DefAmount)),
_ = wxFlexGridSizer:add(GridSz, TTL_L, zxw:flags(base)),
_ = wxFlexGridSizer:add(GridSz, TTL_Tx, zxw:flags(wide)),
_ = wxFlexGridSizer:add(GridSz, GasP_L, zxw:flags(base)),
_ = wxFlexGridSizer:add(GridSz, GasP_Tx, zxw:flags(wide)),
_ = wxFlexGridSizer:add(GridSz, Gas_L, zxw:flags(base)),
_ = wxFlexGridSizer:add(GridSz, Gas_Tx, zxw:flags(wide)),
_ = wxFlexGridSizer:add(GridSz, Amount_L, zxw:flags(base)),
_ = wxFlexGridSizer:add(GridSz, Amount_Tx, zxw:flags(wide)),
_ = wxSizer:add(ParamSz, GridSz, zxw:flags(wide)),
{ParamSz, TTL_Tx, GasP_Tx, Gas_Tx, Amount_Tx}.
call_params(Controls) ->
call_params(Controls, []).
call_params([], A) ->
{ok, lists:reverse(A)};
call_params([{L, C} | T], A) ->
O =
try
{ok, list_to_integer(wxTextCtrl:getValue(C))}
catch
error:badarg -> {error, {L, not_an_integer}}
end,
case O of
{ok, N} -> call_params(T, [N | A]);
Error -> Error
end.
clicked4(State,
#c{id = ConID, build = #{aci := ACI}, funs = {_, Funs}},
{Name, Type},
{PK, Nonce, TTL, GasP, Gas, Amount}) ->
AACI = hz:prepare_aaci(ACI),
#f{args = ArgFields} = lists:keyfind(Name, #f.name, Funs),
Args = lists:map(fun get_arg/1, ArgFields),
FunName = binary_to_list(Name),
case hz:contract_call(PK, Nonce, Gas, GasP, Amount, TTL, AACI, ConID, FunName, Args) of
{ok, UnsignedTX} ->
case Type of
call -> do_call(State, ConID, PK, UnsignedTX);
dryr -> do_dry_run(State, ConID, UnsignedTX)
end;
Error ->
handle_troubling(State, Error),
State
end.
do_call(State, ConID, CallerID, UnsignedTX) ->
ok = gd_con:sign_call(ConID, CallerID, UnsignedTX),
State.
do_dry_run(State, ConID, TX) ->
ok = gd_con:dry_run(ConID, TX),
State.
do_call_result(#s{tabs = TopBook, cons = {Consbook, Contracts}}, ConID, CallInfo) ->
case lookup_contract(ConID, Contracts) of
{#c{cons = Console}, ZeroIndex} ->
_ = wxNotebook:changeSelection(TopBook, 1),
_ = wxNotebook:changeSelection(Consbook, ZeroIndex),
Out = io_lib:format("Call Result:~n~p~n~n", [CallInfo]),
wxTextCtrl:appendText(Console, Out);
error ->
tell(info, "Received result for ~p:~n~p ", [ConID, CallInfo])
end.
do_dryrun_result(#s{tabs = TopBook, cons = {Consbook, Contracts}}, ConID, CallInfo) ->
case lookup_contract(ConID, Contracts) of
{#c{cons = Console}, ZeroIndex} ->
_ = wxNotebook:changeSelection(TopBook, 1),
_ = wxNotebook:changeSelection(Consbook, ZeroIndex),
Out = io_lib:format("Call Result:~n~p~n~n", [CallInfo]),
wxTextCtrl:appendText(Console, Out);
error ->
tell(info, "Received result for ~p:~n~p ", [ConID, CallInfo])
end.
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]),
ok = handle_troubling(State, Message),
State
end;
{error, Reason} ->
Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Reason]),
ok = handle_troubling(State, Message),
State
end;
add_code_page2(State, {hash, Address}) ->
open_hash2(State, Address).
add_code_page(State = #s{tabs = TopBook, code = {Codebook, Pages}}, Location, Code) ->
Color = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW),
tell("Color: ~p", [Color]),
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 = wxTextCtrl:getValue(CodeTx),
deploy2(State, Source)
end.
deploy2(State, Source) ->
case compile(Source) of
% Options = sophia_options(),
% case so_compiler:from_string(Source, Options) of
{ok, Build} ->
deploy3(State, Build);
Other ->
ok = tell(info, "Compilation Failed!~n~tp", [Other]),
State
end.
deploy3(State, Build) ->
case gd_con:list_keys() of
{ok, 0, []} ->
handle_troubling(State, "No keys exist in the current wallet.");
{ok, Selected, Keys} ->
deploy4(State, Build, Selected, Keys);
error ->
handle_troubling(State, "No wallet is selected!")
end.
deploy4(State = #s{frame = Frame, j = J}, Build = #{aci := ACI}, Selected, Keys) ->
{#{functions := Funs}, _} = find_main(ACI),
#{arguments := As} = lom:find(name, <<"init">>, Funs),
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")),
Sizer = wxBoxSizer:new(?wxVERTICAL),
ScrollWin = wxScrolledWindow:new(Dialog),
ScrollSz = wxBoxSizer:new(?wxVERTICAL),
ok = wxScrolledWindow:setSizerAndFit(ScrollWin, ScrollSz),
ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5),
FunName = unicode:characters_to_list(["init/", integer_to_list(length(As))]),
FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]),
KeySz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Signature Key")}]),
KeyPicker = wxChoice:new(ScrollWin, ?wxID_ANY, [{choices, Keys}]),
_ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)),
ok = wxChoice:setSelection(KeyPicker, Selected - 1),
{ParamSz, TTL_Tx, GasP_Tx, Gas_Tx, Amount_Tx} = call_param_sizer(ScrollWin, J),
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)),
GridSz = wxFlexGridSizer:new(2, 4, 4),
ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL),
ok = wxFlexGridSizer:addGrowableCol(GridSz, 1),
MakeArgField =
fun(#{name := AN, type := T}) ->
Type =
case T of
<<"address">> -> address;
<<"int">> -> integer;
<<"bool">> -> boolean;
L when is_list(L) -> list; % FIXME
% I when is_binary(I) -> iface % FIXME
I when is_binary(I) -> address % FIXME
end,
ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN),
TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY),
_ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)),
_ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)),
{ANT, TCT, Type}
end,
ArgFields = lists:map(MakeArgField, As),
_ = wxStaticBoxSizer:add(FunSz, GridSz, zxw:flags(wide)),
_ = wxStaticBoxSizer:add(ScrollSz, FunSz, [{proportion, 1}, {flag, ?wxEXPAND}]),
_ = wxStaticBoxSizer:add(ScrollSz, KeySz, [{proportion, 0}, {flag, ?wxEXPAND}]),
_ = wxStaticBoxSizer:add(ScrollSz, ParamSz, [{proportion, 0}, {flag, ?wxEXPAND}]),
_ = wxSizer:add(Sizer, ScrollWin, [{proportion, 5}, {flag, ?wxEXPAND}]),
_ = wxSizer:add(Sizer, ButtSz, [{proportion, 1}, {flag, ?wxEXPAND}]),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxDialog:setSize(Dialog, {500, 500}),
ok = wxDialog:center(Dialog),
Outcome =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)),
PK = unicode:characters_to_binary(ID),
InitArgs = lists:map(fun get_arg/1, ArgFields),
Controls =
[{"TTL", TTL_Tx},
{"Gas Price", GasP_Tx},
{"Gas", Gas_Tx},
{"Amount", Amount_Tx}],
case call_params(Controls) of
{ok, [TTL, GasP, Gas, Amount]} ->
{ok, Nonce} = hz:next_nonce(PK),
DeployParams = {PK, Nonce, TTL, GasP, Gas, Amount},
{ok, DeployParams, InitArgs};
E ->
E
end;
?wxID_CANCEL ->
cancel
end,
ok = wxDialog:destroy(Dialog),
case Outcome of
{ok, Params, Args} -> deploy5(State, Build, Params, Args);
cancel -> State;
Error -> handle_troubling(State, Error)
end.
deploy5(State, Build, Params, Args) ->
tell(info, "Build: ~p", [Build]),
ok = gd_con:deploy(Build, Params, Args),
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}) ->
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")),
Sizer = wxBoxSizer:new(?wxVERTICAL),
AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]),
AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
_ = wxSizer:add(AddressSz, AddressTx, 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)),
_ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)),
_ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxDialog:setSize(Dialog, {500, 200}),
ok = wxDialog:center(Dialog),
ok = wxTextCtrl:setFocus(AddressTx),
Choice =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
case wxTextCtrl:getValue(AddressTx) of
"" -> cancel;
A -> {ok, A}
end;
?wxID_CANCEL ->
cancel
end,
ok = wxDialog:destroy(Dialog),
case Choice of
{ok, Address = "ct_" ++ _} -> open_hash2(State, Address);
{ok, Address = "th_" ++ _} -> get_contract_from_tx(State, 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
{ok, Source} ->
open_hash3(State, Address, Source);
Error ->
ok = handle_troubling(State, Error),
State
end.
open_hash3(State, Address, Source) ->
% TODO: Compile on load and verify the deployed hash for validity.
Options = sophia_options(),
case so_compiler:from_string(Source, Options) of
{ok, Build = #{aci := ACI}} ->
{Defs = #{functions := Funs}, ConIfaces} = find_main(ACI),
Callable = lom:delete(name, <<"init">>, Funs),
FunDefs = {maps:put(functions, Callable, Defs), ConIfaces},
ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]),
add_code_page(State, {hash, Address}, Source);
Other ->
ok = tell(info, "Compilation Failed!~n~tp", [Other]),
State
end.
% TODO: Break this down -- tons of things in here recur.
save(State = #s{frame = Frame, j = J, 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 = wxTextCtrl:getValue(Widget),
case filelib:ensure_dir(Path) of
ok ->
case file:write_file(Path, Source) of
ok ->
State;
Error ->
ok = handle_troubling(State, Error),
State
end;
Error ->
ok = handle_troubling(State, Error),
State
end;
Page = #p{path = {hash, Hash}, code = Widget} ->
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, unicode:characters_to_list([Hash, ".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),
Source = get_source(Widget),
case filelib:ensure_dir(Path) of
ok ->
case file:write_file(Path, Source) of
ok ->
true = wxNotebook:setPageText(Codebook, Index, File),
NewPrefs = maps:put(dir, Dir, Prefs),
NewPage = Page#p{path = {file, Path}},
NewPages = store_nth(Index + 1, NewPage, Pages),
NewCode = {Codebook, NewPages},
State#s{prefs = NewPrefs, code = NewCode};
Error ->
ok = handle_troubling(State, Error),
State
end;
Error ->
ok = handle_troubling(State, Error),
State
end
end;
?wxID_CANCEL ->
State
end,
ok = wxFileDialog:destroy(Dialog),
NewState
end
end.
get_source(Widget) ->
case ?editorMode of
plain -> wxTextCtrl:getValue(Widget);
sophia -> gd_sophia_editor:get_text(Widget)
end.
set_source(Widget, Src) ->
case ?editorMode of
plain -> wxTextCtrl:setValue(Widget, Src);
sophia -> gd_sophia_editor:set_text(Widget, Src)
end.
% TODO: Break this down -- tons of things in here recur.
rename(State = #s{frame = Frame, j = J, prefs = Prefs, 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} ->
DefaultDir = filename:dirname(Path),
Options =
[{message, J("Save Location")},
{defaultDir, DefaultDir},
{defaultFile, filename:basename(Path)},
{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 = get_source(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 ->
ok = handle_troubling(State, Error),
State
end;
Error ->
ok = handle_troubling(State, Error),
State
end
end;
?wxID_CANCEL ->
State
end,
ok = wxFileDialog:destroy(Dialog),
NewState;
#p{path = {hash, _}} ->
save(State)
end
end.
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.
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")),
Sizer = wxBoxSizer:new(?wxVERTICAL),
AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]),
AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
_ = wxSizer:add(AddressSz, AddressTx, 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)),
_ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)),
_ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxDialog:setSize(Dialog, {500, 200}),
ok = wxDialog:center(Dialog),
ok = wxTextCtrl:setFocus(AddressTx),
Choice =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
case wxTextCtrl:getValue(AddressTx) of
"" -> cancel;
A -> {ok, A}
end;
?wxID_CANCEL ->
cancel
end,
ok = wxDialog:destroy(Dialog),
case Choice 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, Address) ->
case hz:contract_source(Address) of
{ok, Source} ->
load3(State, Address, Source);
Error ->
ok = handle_troubling(State, Error),
State
end.
load3(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")}]),
CodeTxStyle = {style, ?wxTE_MULTILINE
bor ?wxTE_PROCESS_TAB
bor ?wxTE_DONTWRAP
bor ?wxTE_READONLY},
CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]),
Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]),
TextAt = wxTextAttr:new(),
ok = wxTextAttr:setFont(TextAt, Mono),
true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt),
ok = set_source(CodeTx, Source),
_ = 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)),
_ = wxSizer:add(ProgSz, CodeSz, [{proportion, 2}, {flag, ?wxEXPAND}]),
_ = wxSizer:add(ProgSz, ScrollWin, [{proportion, 1}, {flag, ?wxEXPAND}]),
_ = wxSizer:add(PageSz, ProgSz, [{proportion, 3}, {flag, ?wxEXPAND}]),
_ = wxSizer:add(PageSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND}]),
{Out, IFaces, Build, NewButtons} =
case compile(Source) of
{ok, B = #{aci := ACI}} ->
{#{functions := Fs}, _} = find_main(ACI),
Callable = lom:delete(name, <<"init">>, Fs),
{NB, IFs} = fun_interfaces(ScrollWin, FunSz, Buttons, Callable, J),
O = io_lib:format("Compilation Succeeded!~n~tp~n~nDone!~n", [B]),
{O, IFs, B, 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}.
get_arg({_, TextCtrl, _}) ->
wxTextCtrl:getValue(TextCtrl).
find_main(ACI) ->
find_main(ACI, none, []).
find_main([#{contract := I = #{kind := contract_interface}} | T], M, Is) ->
find_main(T, M, [I | Is]);
find_main([#{contract := M = #{kind := contract_main}} | T], _, Is) ->
find_main(T, M, Is);
find_main([#{namespace := _} | T], M, Is) ->
find_main(T, M, Is);
find_main([C | T], M, Is) ->
ok = tell("Surprising ACI element: ~p", [C]),
find_main(T, M, Is);
find_main([], M, Is) ->
{M, Is}.
fun_interfaces(ScrollWin, FunSz, Buttons, Funs, J) ->
MakeIface =
fun(#{name := N, arguments := As}) ->
FunName = unicode:characters_to_list([N, "/", integer_to_list(length(As))]),
FN = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]),
GridSz = wxFlexGridSizer:new(2, 4, 4),
ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL),
ok = wxFlexGridSizer:addGrowableCol(GridSz, 1),
MakeArgField =
fun(#{name := AN, type := T}) ->
Type =
case T of
<<"address">> -> address;
<<"int">> -> integer;
<<"bool">> -> boolean;
L when is_list(L) -> list; % FIXME
% I when is_binary(I) -> iface % FIXME
I when is_binary(I) -> address % FIXME
end,
ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN),
TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY),
_ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)),
_ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)),
{ANT, TCT, Type}
end,
ArgFields = lists:map(MakeArgField, As),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
{CallButton, DryRunButton} =
case N =:= <<"init">> of
false ->
CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]),
DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]),
_ = wxBoxSizer:add(ButtSz, CallBn, zxw:flags(wide)),
_ = wxBoxSizer:add(ButtSz, DryRBn, zxw:flags(wide)),
{#w{name = {N, call}, id = wxButton:getId(CallBn), wx = CallBn},
#w{name = {N, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}};
true ->
Deploy = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Deploy")}]),
_ = wxBoxSizer:add(ButtSz, Deploy, zxw:flags(wide)),
{#w{name = {N, call}, id = wxButton:getId(Deploy), wx = Deploy},
none}
end,
_ = wxStaticBoxSizer:add(FN, GridSz, zxw:flags(wide)),
_ = wxStaticBoxSizer:add(FN, ButtSz, zxw:flags(base)),
_ = wxSizer:add(FunSz, FN, zxw:flags(base)),
#f{name = N, call = CallButton, dryrun = DryRunButton, args = ArgFields}
end,
IFaces = lists:map(MakeIface, Funs),
NewButtons = lists:foldl(fun map_iface_buttons/2, Buttons, IFaces),
{NewButtons, IFaces}.
map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) ->
maps:put(DID, D, maps:put(CID, C, 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 = wxTextCtrl:getValue(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),
IDs = list_iface_buttons(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(),
so_compiler:from_string(Source, Options).
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.