diff --git a/test_prep b/test_prep new file mode 100755 index 0000000..103e18e --- /dev/null +++ b/test_prep @@ -0,0 +1,10 @@ +#!/bin/bash + +pushd $(dirname $BASH_SOURCE) > /dev/null +ZX_DEV_ROOT=$PWD +popd > /dev/null +ZOMP_DIR="$ZX_DEV_ROOT/tester" +rm -rf "$ZOMP_DIR" +cp -r "$ZX_DEV_ROOT/zomp" "$ZOMP_DIR" + +echo "Done. Make sure to export \"export ZOMP_DIR=$ZOMP_DIR\" before running zx" diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx.erl b/zomp/lib/otpr/zx/0.1.0/src/zx.erl index 9031436..5455d84 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx.erl @@ -27,7 +27,7 @@ -export_type([serial/0, package_id/0, package/0, realm/0, name/0, version/0, identifier/0, host/0, - key_id/0, key_name/0, key_data/0, + key_id/0, key_name/0, user_id/0, user_name/0, contact_info/0, user_data/0, lower0_9/0, label/0, package_meta/0, @@ -50,16 +50,13 @@ -type host() :: {string() | inet:ip_address(), inet:port_number()}. -type key_id() :: {realm(), key_name()}. -type key_name() :: lower0_9(). --type key_data() :: {ID :: key_id(), - Public :: binary() | <<>>, - Private :: binary() | <<>>}. -type user_id() :: {realm(), user_name()}. -type user_name() :: label(). -type contact_info() :: {Type :: string(), Data :: string()}. -type user_data() :: {ID :: user_id(), RealName :: string(), - Contact :: contact_info(), - KeyData :: [key_data()]}. + Contact :: [contact_info()], + Keys :: [key_name()]}. -type lower0_9() :: [$a..$z | $0..$9 | $_]. -type label() :: [$a..$z | $0..$9 | $_ | $- | $.]. -type package_meta() :: #{package_id := package_id(), @@ -136,8 +133,8 @@ do(["package", TargetDir]) -> end; do(["dialyze"]) -> done(zx_local:dialyze()); -do(["create", "user", Realm, Name]) -> - done(zx_local:create_user(Realm, Name)); +do(["create", "user"]) -> + done(zx_local:create_user()); do(["create", "keypair"]) -> done(zx_local:grow_a_pair()); do(["drop", "key", Realm, KeyName]) -> @@ -646,7 +643,7 @@ usage() -> " zx add key Realm KeyName~n" " zx get key Realm KeyName~n" " zx rem key Realm KeyName~n" -" zx create user Realm~n" +" zx create user~n" " zx create userfiles Realm UserName~n" " zx create keypair Realm~n" " zx export user UserID~n" diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl index abf3b86..fbff670 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl @@ -17,11 +17,26 @@ takeover/1, abdicate/1, create_plt/0, dialyze/0, grow_a_pair/0, drop_key/1, - create_user/2, create_realm/0, create_realmfile/2]). + create_user/0, create_realm/0, create_realmfile/2]). -include("zx_logger.hrl"). +-record(user_data, + {realm = none :: none | zx:realm(), + username = none :: none | zx:user_name(), + realname = none :: none | string(), + contact_info = none :: none | [zx:contact_info()], + keys = none :: none | [zx:key_name()]}). + +-record(realm_init, + {realm = none :: none | zx:realm(), + addr = none :: none | string() | inet:ip_address(), + port = none :: none | inet:port_number(), + sysop = none :: none | #user_data{}, + url = none :: none | string()}). + + %%% Functions @@ -57,7 +72,7 @@ initialize2(Type, RawPackageString) -> initialize3(Type, PackageID) -> case package_exists(PackageID) of false -> - Prefix = solicit_prefix(), + Prefix = ask_prefix(), initialize(Type, PackageID, Prefix); true -> Message = "Package already exists. Try another.", @@ -118,9 +133,9 @@ initialize(Type, PackageID, Prefix, AppStart) -> ok = log(info, "Project ~tp initialized.", [PackageString]), Message = "~nNOTICE:~n" - " This project is currently listed as having no dependencies.~n" - " If this is not true then run `zx set dep DepID` for each current dependency.~n" - " (run `zx help` for more information on usage)~n", + "This project is currently listed as having no dependencies.~n" + "If this is not true then run `zx set dep DepID` for each current dependency.~n" + "(run `zx help` for more information on usage)~n", io:format(Message). @@ -139,11 +154,11 @@ package_exists(PackageID) -> false. --spec solicit_prefix() -> string(). +-spec ask_prefix() -> string(). %% @private %% Get a valid module prefix to use as a namespace for new modules. -solicit_prefix() -> +ask_prefix() -> Instructions = "~nPICKING A PREFIX~n" "Most Erlang applications have a prefix on the front of their modules. This " @@ -169,7 +184,7 @@ solicit_prefix() -> false -> Message = "The string \"~tp\" is problematic. Try \"[a-z]*_\".~n", ok = io:format(Message, [Prefix]), - solicit_prefix() + ask_prefix() end end. @@ -805,18 +820,6 @@ drop_key({Realm, KeyName}) -> end. --spec create_user(zx:realm(), zx:user_name()) -> ok. -%% @private -%% Validate the realm and username provided, prompt the user to either select a keypair -%% to use or generate a new one, and bundle a .zuser file for conveyance of the user -%% data and his relevant keys (for import into an existing zomp server via `add' -%% command like "add packager", "add maintainer" and "add sysop". - -create_user(Realm, Username) -> - Message = "Would be generating a user file for {~160tp, ~160to}.", - log(info, Message, [Realm, Username]). - - -spec create_realm() -> ok. %% @private %% Prompt the user to input the information necessary to create a new zomp realm, @@ -825,211 +828,58 @@ create_user(Realm, Username) -> create_realm() -> ok = log(info, "WOOHOO! Making a new realm!"), + create_realm(#realm_init{}). + + +create_realm(R = #realm_init{realm = none}) -> + create_realm(R#realm_init{realm = ask_realm()}); +create_realm(R = #realm_init{addr = none}) -> + create_realm(R#realm_init{addr = ask_addr()}); +create_realm(R = #realm_init{port = none}) -> + create_realm(R#realm_init{port = ask_port()}); +create_realm(R = #realm_init{url = none}) -> + create_realm(R#realm_init{url = ask_url()}); +create_realm(R = #realm_init{realm = Realm, sysop = none}) -> + create_realm(R#realm_init{sysop = create_sysop(#user_data{realm = Realm})}); +create_realm(R = #realm_init{realm = Realm, addr = Addr, port = Port, url = URL}) -> Instructions = - "~nNAMING~n" - "Enter a name for your new realm.~n" - "Names can contain only lower-case letters, numbers and the underscore.~n" - "Names must begin with a lower-case letter.~n", - ok = io:format(Instructions), - Realm = zx_tty:get_input(), - case zx_lib:valid_lower0_9(Realm) of - true -> - case realm_exists(Realm) of - false -> - create_realm(Realm); - true -> - ok = io:format("That realm already exists. Be more original.~n"), - create_realm() - end; - false -> - ok = io:format("Bad realm name \"~ts\". Try again.~n", [Realm]), - create_realm() - end. - - --spec create_realm(Realm) -> ok - when Realm :: zx:realm(). - -create_realm(Realm) -> - Address = prompt_address(), - create_realm(Realm, Address). - - --spec prompt_address() -> Result - when Result :: inet:hostname() | inet:ip_address(). - -prompt_address() -> - Message = - "~nHOST ADDRESS~n" - "Enter a static, valid hostname or IPv4 or IPv6 address at which this host " - "can be reached from the public internet (or internal network if it will never " - "need to be reached from the internet).~n" - "DO NOT INCLUDE A PORT NUMBER IN THIS STEP~n", - ok = io:format(Message), + "~nREALM DATA CONFIRMATION~n" + "That's everything! Please confirm these settings:~n" + "[1] Realm Name : ~ts ~n" + "[2] Host Address: ~ts~n" + "[3] Port Number : ~w~n" + "[4] URL : ~ts~n" + "Press a number to select something to change, or [ENTER] to continue.~n", + ok = io:format(Instructions, [Realm, Addr, Port, URL]), case zx_tty:get_input() of - "" -> - ok = io:format("You need to enter an address.~n"), - prompt_address(); - String -> - parse_address(String) + "1" -> create_realm(R#realm_init{realm = none}); + "2" -> create_realm(R#realm_init{addr = none}); + "3" -> create_realm(R#realm_init{port = none}); + "4" -> create_realm(R#realm_init{url = none}); + "" -> store_realm(R); + _ -> + ok = io:format("~nArglebargle, glop-glyf!?!~n~n"), + create_realm(R) end. --spec parse_address(string()) -> inet:hostname() | inet:ip_address(). - -parse_address(String) -> - case inet:parse_address(String) of - {ok, Address} -> Address; - {error, einval} -> String - end. - - --spec create_realm(Realm, Address) -> ok - when Realm :: zx:realm(), - Address :: inet:hostname() | inet:ip_address(). - -create_realm(Realm, Address) -> - Message = - "~nPUBLIC PORT NUMBER~n" - "Enter the public (external) port number at which this service should be " - "available. (This might be different from the local port number if you are " - "forwarding ports or have a complex network layout.)~n", - ok = io:format(Message), - Port = prompt_port_number(11311), - create_realm(Realm, Address, Port). - - --spec prompt_port_number(Current) -> Result - when Current :: inet:port_number(), - Result :: inet:port_number(). - -prompt_port_number(Current) -> - Instructions = - "A valid port is any number from 1 to 65535." - "[Press enter to accept the current setting: ~tw]~n", - ok = io:format(Instructions, [Current]), - case zx_tty:get_input() of - "" -> - Current; - S -> - try - case list_to_integer(S) of - Port when 16#ffff >= Port, Port > 0 -> - Port; - Illegal -> - Whoops = "Whoops! ~tw is out of bounds (1~65535). Try again.~n", - ok = io:format(Whoops, [Illegal]), - prompt_port_number(Current) - end - catch error:badarg -> - ok = io:format("~tp is not a port number. Try again...", [S]), - prompt_port_number(Current) - end - end. - - --spec create_realm(Realm, Address, Port) -> ok - when Realm :: zx:realm(), - Address :: inet:hostname() | inet:ip_address(), - Port :: inet:port_number(), - Port :: inet:port_number(). - -create_realm(Realm, Address, Port) -> - Instructions = - "~nSYSOP USERNAME~n" - "Enter a username for the realm sysop.~n" - "Names can contain only lower-case letters, numbers and the underscore.~n" - "Names must begin with a lower-case letter.~n", - ok = io:format(Instructions), - UserName = zx_tty:get_input(), - case zx_lib:valid_lower0_9(UserName) of - true -> - create_realm(Realm, Address, Port, UserName); - false -> - ok = io:format("Bad username ~tp. Try again.~n", [UserName]), - create_realm(Realm, Address, Port) - end. - - --spec create_realm(Realm, Address, Port, UserName) -> ok - when Realm :: zx:realm(), - Address :: inet:hostname() | inet:ip_address(), - Port :: inet:port_number(), - UserName :: string(). - -create_realm(Realm, Address, Port, UserName) -> - Instructions = - "~nSYSOP EMAIL~n" - "Enter an email address for the realm sysop.~n" - "Valid email address rules apply though the checking done here is quite " - "minimal. Check the address you enter carefully. The only people who will " - "suffer from an invalid address are your users.~n", - ok = io:format(Instructions), - Email = zx_tty:get_input(), - [User, Host] = string:lexemes(Email, "@"), - case {zx_lib:valid_lower0_9(User), zx_lib:valid_label(Host)} of - {true, true} -> - create_realm(Realm, Address, Port, UserName, Email); - {false, true} -> - Message = "The user part of the email address seems invalid. Try again.~n", - ok = io:format(Message), - create_realm(Realm, Address, Port, UserName); - {true, false} -> - Message = "The host part of the email address seems invalid. Try again.~n", - ok = io:format(Message), - create_realm(Realm, Address, Port, UserName); - {false, false} -> - Message = "This email address seems like its totally bonkers. Try again.~n", - ok = io:format(Message), - create_realm(Realm, Address, Port, UserName) - end. - - --spec create_realm(Realm, Address, Port, UserName, Email) -> ok - when Realm :: zx:realm(), - Address :: inet:hostname() | inet:ip_address(), - Port :: inet:port_number(), - UserName :: string(), - Email :: string(). - -create_realm(Realm, Address, Port, UserName, Email) -> - Instructions = - "~nSYSOP REAL NAME~n" - "Enter the real name (or whatever name people recognize) for the sysop.~n" - "There are no rules for this one. Any valid UTF-8 printables are legal.~n", - ok = io:format(Instructions), - RealName = zx_tty:get_input(), - create_realm(Realm, Address, Port, UserName, Email, RealName). - - --spec create_realm(Realm, Address, Port, UserName, Email, RealName) -> ok - when Realm :: zx:realm(), - Address :: inet:hostname() | inet:ip_address(), - Port :: inet:port_number(), - UserName :: string(), - Email :: string(), - RealName :: string(). - -create_realm(Realm, Address, Port, UserName, Email, RealName) -> - ok = io:format("~nGenerating keys. This might take a while, so settle in...~n"), - KeyName = UserName ++ "-root", +store_realm(#realm_init{realm = Realm, + addr = Addr, + port = Port, + url = URL, + sysop = Sysop = #user_data{username = UserName, + keys = [KeyName]}}) -> ok = make_realm_dirs(Realm), - ok = zx_key:generate_rsa({Realm, KeyName}), + Address = parse_maybe_address(Addr), RealmConf = [{realm, Realm}, {prime, {Address, Port}}, {sysop, UserName}, - {key, KeyName}], - UserConf = - [{realm, Realm}, - {username, UserName}, - {realmname, RealName}, - {contact_info, [{"email", Email}]}, - {keys, [KeyName]}], + {key, KeyName}, + {url, URL}], + ok = store_user(Sysop), RealmConfPath = filename:join(zx_lib:path(etc, Realm), "realm.conf"), ok = zx_lib:write_terms(RealmConfPath, RealmConf), - UserConfPath = filename:join(zx_lib:path(etc, Realm), UserName ++ ".user"), - ok = zx_lib:write_terms(UserConfPath, UserConf), ok = create_realmfile(Realm, "."), ZRF = Realm ++ ".zrf", Message = @@ -1054,6 +904,308 @@ create_realm(Realm, Address, Port, UserName, Email, RealName) -> io:format(Message, Substitutions). +-spec ask_realm() -> string(). + +ask_realm() -> + Instructions = + "~nNAMING~n" + "Enter a name for your new realm.~n" + "Names can contain only lower-case letters, numbers and the underscore.~n" + "Names must begin with a lower-case letter.~n", + ok = io:format(Instructions), + Realm = zx_tty:get_input(), + case zx_lib:valid_lower0_9(Realm) of + true -> + case realm_exists(Realm) of + false -> + Realm; + true -> + ok = io:format("That realm already exists. Be more original.~n"), + ask_realm() + end; + false -> + ok = io:format("Bad realm name \"~ts\". Try again.~n", [Realm]), + ask_realm() + end. + + +-spec ask_addr() -> string(). + +ask_addr() -> + Message = + "~nHOST ADDRESS~n" + "Enter a static, valid hostname or IPv4 or IPv6 address at which this host " + "can be reached from the public internet (or internal network if it will never " + "need to be reached from the internet).~n" + "DO NOT INCLUDE A PORT NUMBER IN THIS STEP~n", + ok = io:format(Message), + case zx_tty:get_input() of + "" -> + ok = io:format("You need to enter an address.~n"), + ask_addr(); + String -> + String + end. + + +-spec parse_maybe_address(string()) -> inet:hostname() | inet:ip_address(). + +parse_maybe_address(String) -> + case inet:parse_address(String) of + {ok, Address} -> Address; + {error, einval} -> String + end. + + +-spec ask_port() -> inet:port_number(). + +ask_port() -> + Message = + "~nPUBLIC PORT NUMBER~n" + "Enter the publicly visible (external) port number at which this service " + "should be available.~n" + "(This might be different from the local port number if you are forwarding " + "ports or have a complex network layout.)~n", + ok = io:format(Message), + prompt_port_number(11311). + + +-spec prompt_port_number(Current) -> Result + when Current :: inet:port_number(), + Result :: inet:port_number(). + +prompt_port_number(Current) -> + ok = io:format("A valid port is any number from 1 to 65535.~n"), + case zx_tty:get_input("[~tw]", [Current]) of + "" -> + Current; + S -> + try + case list_to_integer(S) of + Port when 16#ffff >= Port, Port > 0 -> + Port; + Illegal -> + Whoops = "Whoops! ~tw is out of bounds (1~65535). Try again.~n", + ok = io:format(Whoops, [Illegal]), + prompt_port_number(Current) + end + catch error:badarg -> + ok = io:format("~tp is not a port number. Try again...", [S]), + prompt_port_number(Current) + end + end. + + +-spec ask_url() -> string(). + +ask_url() -> + Message = + "~nURL~n" + "Most public realms have a website, IRC channel, or similar location where its " + "community (or customers) communicate. If you have such a URL enter it here.~n" + "NOTE: No checking is performed on the input here. Confuse your users at " + "your own peril!~n", + ok = io:format(Message), + zx_tty:get_input("[ENTER] to leave blank"). + + +-spec create_sysop(InitUser) -> FullUser + when InitUser :: #user_data{}, + FullUser :: #user_data{}. + +create_sysop(U = #user_data{username = none}) -> + UserName = ask_username(), + KeyName = UserName ++ ".root", + create_sysop(U#user_data{username = UserName, keys = [KeyName]}); +create_sysop(U = #user_data{realname = none}) -> + create_sysop(U#user_data{realname = ask_realname()}); +create_sysop(U = #user_data{contact_info = none}) -> + create_sysop(U#user_data{contact_info = [ask_email()]}); +create_sysop(U = #user_data{username = UserName, + realname = RealName, + contact_info = [{"email", Email}]}) -> + Instructions = + "~nSYSOP DATA CONFIRMATION~n" + "Please correct or confirm the sysop's data:~n" + "[1] Username : ~ts~n" + "[2] Real Name: ~ts~n" + "[3] Email : ~ts~n" + "Press a number to select something to change, or [ENTER] to accept.~n", + ok = io:format(Instructions, [UserName, RealName, Email]), + case zx_tty:get_input() of + "1" -> create_sysop(U#user_data{username = none}); + "2" -> create_sysop(U#user_data{realname = none}); + "3" -> create_sysop(U#user_data{contact_info = none}); + "" -> U; + _ -> + ok = io:format("~nArglebargle, glop-glyf!?!~n~n"), + create_sysop(U) + end. + + +-spec create_user() -> zx:outcome(). + +create_user() -> + create_user(#user_data{}). + + +-spec create_user(#user_data{}) -> zx:outcome(). + +create_user(U = #user_data{realm = none}) -> + case pick_realm() of + {ok, Realm} -> create_user(U#user_data{realm = Realm}); + {error, no_realms} -> {error, "No realms configured.", 1} + end; +create_user(U = #user_data{username = none}) -> + UserName = ask_username(), + KeyName = UserName ++ ".1", + create_user(U#user_data{username = UserName, keys = [KeyName]}); +create_user(U = #user_data{realname = none}) -> + create_user(U#user_data{realname = ask_realname()}); +create_user(U = #user_data{contact_info = none}) -> + create_user(U#user_data{contact_info = [ask_email()]}); +create_user(U = #user_data{realm = Realm, + username = UserName, + realname = RealName, + contact_info = [{"email", Email}]}) -> + Instructions = + "~nUSER DATA CONFIRMATION~n" + "Please correct or confirm the user's data:~n" + "[1] Realm : ~ts~n" + "[2] Username : ~ts~n" + "[3] Real Name: ~ts~n" + "[4] Email : ~ts~n" + "Press a number to select something to change, or [ENTER] to accept.~n", + ok = io:format(Instructions, [Realm, UserName, RealName, Email]), + case zx_tty:get_input() of + "1" -> create_user(U#user_data{realm = none}); + "2" -> create_user(U#user_data{username = none}); + "3" -> create_user(U#user_data{realname = none}); + "4" -> create_user(U#user_data{contact_info = none}); + "" -> store_user(U); + _ -> + ok = io:format("~nArglebargle, glop-glyf!?!~n~n"), + create_user(U) + end. + + +-spec store_user(#user_data{}) -> ok. + +store_user(#user_data{realm = Realm, + username = UserName, + realname = RealName, + contact_info = ContactInfo, + keys = KeyNames}) -> + UserConf = + [{realm, Realm}, + {username, UserName}, + {realname, RealName}, + {contact_info, ContactInfo}, + {keys, KeyNames}], + ok = gen_keys(Realm, KeyNames), + UserConfPath = filename:join(zx_lib:path(etc, Realm), UserName ++ ".user"), + ok = zx_lib:write_terms(UserConfPath, UserConf), + log(info, "User ~tp created.", [{Realm, UserName}]). + + +-spec gen_keys(Realm, KeyNames) -> ok + when Realm :: zx:realm(), + KeyNames :: [zx:key_names()]. + +gen_keys(Realm, KeyNames) -> + ok = io:format("Generating keys. This might take a while, so settle in...~n"), + GenRSA = fun(KeyName) -> zx_key:generate_rsa({Realm, KeyName}) end, + lists:foreach(GenRSA, KeyNames). + + +-spec pick_realm() -> Result + when Result :: {ok, zx:realm()} + | {error, no_realms}. + +pick_realm() -> + case zx_lib:list_realms() of + [] -> + ok = log(warning, "No realms configured! Exiting..."), + {error, no_realms}; + Realms -> + ok = io:format("Select a realm:~n"), + Options = lists:zip(Realms, Realms), + Realm = zx_tty:select(Options), + {ok, Realm} + end. + + +-spec ask_username() -> zx:user_name(). + +ask_username() -> + Instructions = + "~nUSERNAME~n" + "Enter a username.~n" + "Names can contain only lower-case letters, numbers and the underscore.~n" + "Names must begin with a lower-case letter.~n", + ok = io:format(Instructions), + case zx_tty:get_input() of + "" -> + Message = "You have to be called *something*. Let's try that again.~n", + ok = io:format(Message), + ask_username(); + UserName -> + case zx_lib:valid_lower0_9(UserName) of + true -> + UserName; + false -> + ok = io:format("Bad username ~tp. Try again.~n", [UserName]), + ask_username() + end + end. + + +-spec ask_realname() -> string(). + +ask_realname() -> + Instructions = + "~nREAL NAME~n" + "Enter the user's real name (or whatever name people recognize).~n" + "There are no rules for this one. Any valid UTF-8 printables are legal.~n", + ok = io:format(Instructions), + zx_tty:get_input(). + + +-spec ask_email() -> {Type :: string(), Email :: string()}. + +ask_email() -> + Instructions = + "~nEMAIL~n" + "Enter an email address.~n" + "Valid email address rules apply though the checking done here is quite " + "minimal. Check the address you enter carefully. The only people who will " + "suffer from an invalid address are other realm users.~n", + ok = io:format(Instructions), + Email = zx_tty:get_input(), + case string:lexemes(Email, "@") of + [User, Host] -> + case {zx_lib:valid_lower0_9(User), zx_lib:valid_label(Host)} of + {true, true} -> + {"email", Email}; + {false, true} -> + Message = "The user part of the email address seems invalid.~n", + ok = io:format(Message), + ask_email(); + {true, false} -> + Message = "The host part of the email address seems invalid.~n", + ok = io:format(Message), + ask_email(); + {false, false} -> + Message = "This email address is totally bonkers. Try again.~n", + ok = io:format(Message), + ask_email() + end; + _ -> + ok = io:format("Don't get fresh with me. Try again. For real this time.~n"), + ask_email() + end. + + -spec realm_exists(zx:realm()) -> boolean(). %% @private %% Checks for remnants of a realm. @@ -1111,9 +1263,8 @@ drop_realm(Realm) -> case realm_exists(Realm) of true -> Message = - "~n" - " WARNING: Are you SURE you want to remove realm ~ts?~n" - " (Only \"Y\" will confirm this action.)~n", + "~nWARNING: Are you SURE you want to remove realm ~ts?~n" + "(Only \"Y\" will confirm this action.)~n", ok = io:format(Message, [Realm]), case zx_tty:get_input() of "Y" -> diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl index 46e91e7..24bbd64 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl @@ -10,7 +10,7 @@ -copyright("Craig Everett "). -license("GPL-3.0"). --export([get_input/0, select/1, select_string/1]). +-export([get_input/0, get_input/1, get_input/2, select/1, select_string/1]). %%% Type Definitions @@ -20,12 +20,38 @@ %%% User menu interface (terminal) + -spec get_input() -> string(). %% @private %% Provide a standard input prompt and newline sanitized return value. get_input() -> - string:trim(io:get_line("(^C to quit): ")). + case string:trim(io:get_line("(or \"QUIT\"): ")) of + "QUIT" -> what_a_quitter(); + String -> String + end. + + +-spec get_input(Prompt :: string()) -> string(). +%% @private +%% Introduce + +get_input(Prompt) -> + get_input(Prompt, []). + + +-spec get_input(Format, Args) -> string() + when Format :: string(), + Args :: [term()]. +%% @private +%% Allow the caller to use io format strings and args to create a prompt. + +get_input(Format, Args) -> + Prompt = io_lib:format(Format, Args), + case string:trim(io:get_line(["(or \"QUIT\") ", Prompt, ": "])) of + "QUIT" -> what_a_quitter(); + String -> String + end. -spec select(Options) -> Selected @@ -37,13 +63,18 @@ get_input() -> select(Options) -> Max = show(Options), - case pick(string:to_integer(io:get_line("(or ^C to quit)~n ? ")), Max) of - error -> - ok = hurr(), - select(Options); - I -> - {_, Value} = lists:nth(I, Options), - Value + case string:trim(io:get_line("(or \"QUIT\"): ")) of + "QUIT" -> + what_a_quitter(); + String -> + case pick(string:to_integer(String), Max) of + error -> + ok = hurr(), + select(Options); + Index -> + {_, Value} = lists:nth(Index, Options), + Value + end end. @@ -78,7 +109,7 @@ show([], I) -> I; show([{Label, _} | Rest], I) -> Z = I + 1, - ok = io:format(" ~2w - ~ts~n", [Z, Label]), + ok = io:format("[~2w] ~ts~n", [Z, Label]), show(Rest, Z). @@ -99,3 +130,12 @@ pick(_, _) -> error. %% Present an appropriate response when the user derps on selection. hurr() -> io:format("That isn't an option.~n"). + + +-spec what_a_quitter() -> no_return(). +%% @private +%% Halt the runtime if the user decides to quit. + +what_a_quitter() -> + ok = io:format("User abort: \"QUIT\".~nHalting.~n"), + halt(0). diff --git a/zomp/zx b/zomp/zx index c2ebddd..154149f 100755 --- a/zomp/zx +++ b/zomp/zx @@ -1,10 +1,11 @@ #!/bin/sh -ZOMP_DIR="$HOME/.zomp" -VERSION=$(cat "$ZOMP_DIR/etc/version.txt") -ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION" +export ZOMP_DIR="${ZOMP_DIR:-$HOME/.zomp}" +version=$(cat "$ZOMP_DIR/etc/version.txt") +export ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$version" -pushd "$ZX_DIR" > /dev/null +start_dir="$PWD" +cd "$ZX_DIR" ./make_zx -popd > /dev/null -erl -pa "$ZX_DIR/ebin" -run zx run $@ +cd "$start_dir" +erl -noshell -pa "$ZX_DIR/ebin" -run zx do $@ diff --git a/zomp/zxh b/zomp/zxh new file mode 100755 index 0000000..5b262bf --- /dev/null +++ b/zomp/zxh @@ -0,0 +1,11 @@ +#!/bin/sh + +export ZOMP_DIR="${ZOMP_DIR:-$HOME/.zomp}" +version=$(cat "$ZOMP_DIR/etc/version.txt") +export ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$version" + +start_dir="$PWD" +cd "$ZX_DIR" +./make_zx +cd "$start_dir" +erl -pa "$ZX_DIR/ebin" -run zx do $@ diff --git a/zomp/zxh.cmd b/zomp/zxh.cmd new file mode 100644 index 0000000..12e5aec --- /dev/null +++ b/zomp/zxh.cmd @@ -0,0 +1,9 @@ +REM Prepare the environment for ZX and launch it + +set ZOMP_DIR="%LOCALAPPDATA%\zomp" +set VERSION=<"%ZOMP_DIR%\etc\version.txt" +set ZX_DIR="%ZOMP_DIR%\lib\otpr\zx\%VERSION%" +pushd "%ZX_DIR%" +escript.exe make_zx +popd +erl.exe -pa "%ZX_DIR%/ebin" -run zx run "%*" diff --git a/zx_dev b/zx_dev deleted file mode 100755 index 8e30d1c..0000000 --- a/zx_dev +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -pushd $(dirname $BASH_SOURCE) > /dev/null -ZX_DEV_ROOT=$PWD -popd > /dev/null -export ZOMP_DIR="$ZX_DEV_ROOT/tester" -rm -rf "$ZOMP_DIR" -cp -r "$ZX_DEV_ROOT/zomp" "$ZOMP_DIR" -VERSION=$(cat "$ZOMP_DIR/etc/version.txt") -export ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION" - -pushd "$ZX_DIR" > /dev/null -./make_zx -popd > /dev/null -erl -noshell -pa "$ZX_DIR/ebin" -run zx do $@ diff --git a/zxh_dev b/zxh_dev deleted file mode 100755 index b65f80f..0000000 --- a/zxh_dev +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -pushd $(dirname $BASH_SOURCE) > /dev/null -ZX_DEV_ROOT=$PWD -popd > /dev/null -export ZOMP_DIR="$ZX_DEV_ROOT/tester" -rm -rf "$ZOMP_DIR" -cp -r "$ZX_DEV_ROOT/zomp" "$ZOMP_DIR" -VERSION=$(cat "$ZOMP_DIR/etc/version.txt") -export ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION" - -pushd "$ZX_DIR" > /dev/null -./make_zx -popd > /dev/null -erl -pa "$ZX_DIR/ebin" -run zx do $@