diff --git a/dev/spacetraders_register.gleam b/dev/spacetraders_register.gleam new file mode 100644 index 0000000..07ee76b --- /dev/null +++ b/dev/spacetraders_register.gleam @@ -0,0 +1,53 @@ +import argv +import dot_env +import gleam/io +import gleam/list +import gleam_community/ansi +import spacetraders_api +import spacetraders_client/env +import spacetraders_client/pretty_print +import spacetraders_models/agent_symbol +import spacetraders_models/faction_symbol +import spacetraders_sdk + +const usage = "\nusage: gleam -m spacetraders_register AGENT_SYMBOL FACTION_SYMBOL" + +pub fn main() { + dot_env.load_default() + let assert Ok(account_token) = env.get_account_token() + as "no valid account token provided" + let args = argv.load().arguments + let assert [agent_symbol_str, ..args] = args + as { "missing agent symbol argument\n" <> usage } + let assert Ok(agent_symbol) = agent_symbol.parse(agent_symbol_str) + as { "invalid agent symbol\n" <> usage } + let assert [faction_symbol_str, ..] = args + as { "missing faction symbol argument\n" <> usage } + let assert Ok(faction_symbol) = faction_symbol.parse(faction_symbol_str) + as { "invalid faction symbol\n" <> usage } + let assert Ok(registered) = + spacetraders_api.register_new_agent( + account_token, + agent_symbol, + faction_symbol, + ) + as "Failed to register agent" + + io.println(ansi.bold(ansi.underline(ansi.green("Successfully Registered")))) + io.println( + ansi.bold("Agent Token:\t") + <> spacetraders_sdk.agent_token_to_string(registered.token), + ) + io.println("") + + pretty_print.agent(registered.agent) + io.println("") + + pretty_print.contract(registered.contract) + io.println("") + + list.each(registered.ships, fn(ship) { + pretty_print.ship(ship) + io.println("") + }) +} diff --git a/gleam.toml b/gleam.toml index f71ca52..891f279 100644 --- a/gleam.toml +++ b/gleam.toml @@ -13,11 +13,12 @@ gleam_http = ">= 4.0.0 and < 5.0.0" gleam_httpc = ">= 4.1.1 and < 5.0.0" birl = ">= 1.8.0 and < 2.0.0" dot_env = ">= 1.2.0 and < 2.0.0" +argv = ">= 1.0.2 and < 2.0.0" gleam_erlang = ">= 1.0.0 and < 2.0.0" gleam_otp = ">= 1.0.0 and < 2.0.0" gleam_community_ansi = ">= 1.4.3 and < 2.0.0" shore = ">= 1.1.0 and < 2.0.0" -spacetraders_sdk = ">= 1.5.3 and < 2.0.0" +spacetraders_sdk = ">= 1.5.5 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.5.1 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 0b8e58d..1775e26 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,6 +2,7 @@ # You typically do not need to edit this file packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, { name = "birl", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_regexp", "gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "2AC7BA26F998E3DFADDB657148BD5DDFE966958AD4D6D6957DD0D22E5B56C400" }, { name = "dot_env", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "simplifile"], otp_app = "dot_env", source = "hex", outer_checksum = "F2B4815F1B5AF8F20A6EADBB393E715C4C35203EBD5BE8200F766EA83A0B18DE" }, { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, @@ -19,10 +20,11 @@ packages = [ { name = "ranger", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "ranger", source = "hex", outer_checksum = "C8988E8F8CDBD3E7F4D8F2E663EF76490390899C2B2885A6432E942495B3E854" }, { name = "shore", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "shore", source = "hex", outer_checksum = "B5929F807459EAE243E4664D41F02696B5D3E9CE314971E8C2ECB57007CA9210" }, { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, - { name = "spacetraders_sdk", version = "1.5.3", build_tools = ["gleam"], requirements = ["birl", "gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib"], otp_app = "spacetraders_sdk", source = "hex", outer_checksum = "68D06C93333F646A516587644D6C91E5D07279D74E078FA7966D678E90EC431F" }, + { name = "spacetraders_sdk", version = "1.5.5", build_tools = ["gleam"], requirements = ["birl", "gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib"], otp_app = "spacetraders_sdk", source = "hex", outer_checksum = "62CE20A246D4D148A9CB89B3B11A2EFD7A7AE3863D6A24BAB1CE77FD82527535" }, ] [requirements] +argv = { version = ">= 1.0.2 and < 2.0.0" } birl = { version = ">= 1.8.0 and < 2.0.0" } dot_env = { version = ">= 1.2.0 and < 2.0.0" } gleam_community_ansi = { version = ">= 1.4.3 and < 2.0.0" } @@ -34,4 +36,4 @@ gleam_otp = { version = ">= 1.0.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.60.0 and < 1.0.0" } gleeunit = { version = ">= 1.5.1 and < 2.0.0" } shore = { version = ">= 1.1.0 and < 2.0.0" } -spacetraders_sdk = { version = ">= 1.5.3 and < 2.0.0" } +spacetraders_sdk = { version = ">= 1.5.5 and < 2.0.0" } diff --git a/src/spacetraders_client.gleam b/src/spacetraders_client.gleam index cd68875..9f578cd 100644 --- a/src/spacetraders_client.gleam +++ b/src/spacetraders_client.gleam @@ -1,100 +1,171 @@ -import birl -import env -import gleam/int +import birl.{type Time} +import gleam/erlang/process import gleam/io import gleam/list -import gleam/option -import gleam/uri -import gleam_community/ansi -import spacetraders_api -import spacetraders_models/account_id -import spacetraders_models/agent_symbol -import spacetraders_models/faction_symbol -import spacetraders_models/waypoint_symbol - -pub fn main() -> Nil { - let assert Ok(env.Env(agent_token:, ..)) = env.load_dotenv() - io.println("") +import gleam/option.{type Option, None, Some} +import shore +import shore/key +import shore/layout +import shore/style +import shore/ui +import spacetraders_api.{type ServerStatus} +import spacetraders_client/env.{type Env} +import spacetraders_client/pretty_print +import spacetraders_sdk.{type AgentToken, type ApiError, type ApiResponse} +pub fn print_general_info(agent_token: AgentToken) { let assert Ok(account) = spacetraders_api.get_account(agent_token) - io.println(ansi.bold(ansi.underline(ansi.blue("Account")))) - io.println(ansi.bold("Id:\t\t") <> account_id.to_string(account.id)) - case account.email { - option.Some(email) -> io.println(ansi.bold("Email:\t\t") <> email) - option.None -> Nil - } - io.println(ansi.bold("Created At:\t") <> birl.to_iso8601(account.created_at)) + pretty_print.account(account) io.println("") let assert Ok(agent) = spacetraders_api.get_agent(agent_token) - io.println(ansi.bold(ansi.underline(ansi.magenta("Agent")))) - io.println(ansi.bold("Symbol:\t\t") <> agent_symbol.to_string(agent.symbol)) - io.println( - ansi.bold("Headquarters:\t") - <> waypoint_symbol.to_string(agent.headquarters), - ) - io.println(ansi.bold("Credits:\t") <> int.to_string(agent.credits)) - io.println( - ansi.bold("Faction:\t") <> faction_symbol.to_string(agent.starting_faction), - ) - io.println(ansi.bold("Ship Count:\t") <> int.to_string(agent.ship_count)) + pretty_print.agent(agent) io.println("") let assert Ok(server_status) = spacetraders_api.get_server_status() - let now = birl.now() - io.println(ansi.bold(ansi.underline(ansi.green("Server")))) - io.println(ansi.bold("Version:\t") <> server_status.version) - io.println(ansi.bold("Status:\t\t") <> server_status.status) - io.println( - ansi.bold("Last Reset:\t") - <> birl.legible_difference(now, server_status.reset_date), - ) - io.println( - ansi.bold("Next Reset:\t") - <> birl.legible_difference(now, server_status.server_resets.next) - <> " (" - <> ansi.italic(server_status.server_resets.frequency) - <> ")", - ) - case server_status.health.last_market_update { - option.Some(last_market_update) -> - io.println( - ansi.bold("Market Updated:\t") - <> birl.legible_difference(now, last_market_update), - ) - option.None -> Nil - } - io.println("") - - io.println(ansi.bold(ansi.underline(ansi.blue("Links")))) - list.each(server_status.links, fn(link) { - io.println(ansi.bold(link.name) <> ": " <> uri.to_string(link.url)) - }) - io.println("") - - io.println( - ansi.bold(ansi.underline(ansi.yellow("Leaderboard - Most Credits"))), - ) - list.each(server_status.leaderboards.most_credits, fn(entry) { - io.println( - ansi.bold(agent_symbol.to_string(entry.agent_symbol)) - <> ": " - <> int.to_string(entry.credits), - ) - }) - io.println("") - - io.println( - ansi.bold( - ansi.underline(ansi.yellow("Leaderboard - Most Submitted Charts")), - ), - ) - list.each(server_status.leaderboards.most_submitted_charts, fn(entry) { - io.println( - ansi.bold(agent_symbol.to_string(entry.agent_symbol)) - <> ": " - <> int.to_string(entry.chart_count), - ) - }) + pretty_print.server_status(server_status, True, True) io.println("") } + +pub fn print_ships(agent_token: AgentToken) { + let assert Ok(ships) = + spacetraders_api.list_ships(agent_token, option.None, option.None) + list.each(ships.data, fn(ship) { + pretty_print.ship(ship) + io.println("") + }) +} + +pub fn print_contracts(agent_token: AgentToken) { + let assert Ok(contracts) = + spacetraders_api.list_contracts(agent_token, option.None, option.None) + list.each(contracts.data, fn(contract) { + pretty_print.contract(contract) + io.println("") + }) +} + +pub fn main() -> Nil { + let exit = process.new_subject() + let assert Ok(_actor) = + shore.spec( + init:, + update:, + view:, + exit:, + keybinds: shore.default_keybinds(), + redraw: shore.on_timer(16), + ) + |> shore.start + process.receive_forever(exit) +} + +pub opaque type LastServerStatus { + Loading + Loaded(response: ApiResponse(ServerStatus)) + Refreshing(prev_response: ApiResponse(ServerStatus)) + Refreshed( + prev_response: ApiResponse(ServerStatus), + response: ApiResponse(ServerStatus), + ) +} + +pub opaque type Model { + Model( + env: Env, + errors: List(ApiError), + last_request: Option(Time), + last_server_status: LastServerStatus, + ) +} + +pub opaque type Msg { + NoOp + DismissErrors + GetServerStatus + GotServerStatus(response: ApiResponse(ServerStatus)) +} + +fn init() -> #(Model, List(fn() -> Msg)) { + let assert Ok(env) = env.load_dotenv() + let model = + Model(env:, errors: [], last_request: None, last_server_status: Loading) + let cmds = [fn() { GetServerStatus }] + #(model, cmds) +} + +fn update(model: Model, msg: Msg) -> #(Model, List(fn() -> Msg)) { + case msg { + NoOp -> #(model, []) + DismissErrors -> #(Model(..model, errors: []), []) + GetServerStatus -> #( + Model(..model, last_server_status: case model.last_server_status { + Loading -> Loading + Loaded(prev_response) + | Refreshing(prev_response) + | Refreshed(response: prev_response, ..) -> Refreshing(prev_response:) + }), + [fn() { GotServerStatus(spacetraders_api.get_server_status()) }], + ) + GotServerStatus(response) -> { + #( + Model( + ..model, + last_request: Some(birl.now()), + last_server_status: case model.last_server_status { + Loading -> Loaded(response) + Loaded(prev_response) + | Refreshing(prev_response) + | Refreshed(response: prev_response, ..) -> + Refreshed(prev_response:, response:) + }, + ), + [], + ) + } + } +} + +fn view(model: Model) -> shore.Node(Msg) { + layout.grid(0, [style.Px(5), style.Fill], [style.Fill], [ + layout.cell( + ui.box( + [ + case model.last_server_status { + Loaded(response: Ok(server_status)) + | Refreshed(response: Ok(server_status), ..) -> + ui.text(server_status.status) + Loading -> ui.text("Loading server status...") + Refreshing(..) -> ui.text("Refreshing server status...") + Loaded(response: Error(..)) -> + ui.text_styled( + "Failed to load server status", + Some(style.Red), + None, + ) + Refreshed(response: Error(..), ..) -> + ui.text_styled( + "Failed to refresh server status", + Some(style.Red), + None, + ) + }, + ui.br(), + ui.row([ui.button("(R)efresh", key.Char("r"), GetServerStatus)]), + ], + Some("Server Status"), + ) + |> ui.align(style.Center, _), + #(0, 0), + #(0, 0), + ), + layout.cell( + ui.box( + [ui.row([ui.button("(D)ismiss Errors", key.Char("d"), DismissErrors)])], + Some("SpaceTraders"), + ), + #(1, 1), + #(0, 0), + ), + ]) +} diff --git a/src/env.gleam b/src/spacetraders_client/env.gleam similarity index 100% rename from src/env.gleam rename to src/spacetraders_client/env.gleam diff --git a/src/spacetraders_client/pretty_print.gleam b/src/spacetraders_client/pretty_print.gleam new file mode 100644 index 0000000..5106ffe --- /dev/null +++ b/src/spacetraders_client/pretty_print.gleam @@ -0,0 +1,305 @@ +import birl +import gleam/int +import gleam/io +import gleam/list +import gleam/option.{None, Some} +import gleam/order +import gleam/string +import gleam/uri +import gleam_community/ansi +import spacetraders_api.{type ServerStatus} +import spacetraders_models/account.{type Account} +import spacetraders_models/account_id +import spacetraders_models/agent.{type Agent} +import spacetraders_models/agent_symbol +import spacetraders_models/contract.{type Contract} +import spacetraders_models/contract_id +import spacetraders_models/contract_type +import spacetraders_models/crew_rotation +import spacetraders_models/faction_symbol +import spacetraders_models/ship.{type Ship} +import spacetraders_models/ship_fuel_consumed.{ShipFuelConsumed} +import spacetraders_models/ship_nav_flight_mode +import spacetraders_models/ship_nav_status +import spacetraders_models/ship_role +import spacetraders_models/ship_symbol +import spacetraders_models/trade_symbol +import spacetraders_models/waypoint_symbol +import spacetraders_models/waypoint_type + +pub fn account(account: Account) -> Nil { + io.println(ansi.bold(ansi.underline(ansi.blue("Account")))) + io.println(ansi.bold("Id:\t\t") <> account_id.to_string(account.id)) + case account.email { + Some(email) -> io.println(ansi.bold("Email:\t\t") <> email) + None -> Nil + } + io.println(ansi.bold("Created At:\t") <> birl.to_iso8601(account.created_at)) +} + +pub fn agent(agent: Agent) -> Nil { + io.println(ansi.bold(ansi.underline(ansi.magenta("Agent")))) + io.println(ansi.bold("Symbol:\t\t") <> agent_symbol.to_string(agent.symbol)) + io.println( + ansi.bold("Headquarters:\t") + <> waypoint_symbol.to_string(agent.headquarters), + ) + io.println(ansi.bold("Credits:\t") <> int.to_string(agent.credits)) + io.println( + ansi.bold("Faction:\t") <> faction_symbol.to_string(agent.starting_faction), + ) + io.println(ansi.bold("Ship Count:\t") <> int.to_string(agent.ship_count)) +} + +pub fn server_status( + server_status: ServerStatus, + print_links: Bool, + print_leaderboards: Bool, +) -> Nil { + let now = birl.now() + io.println(ansi.bold(ansi.underline(ansi.green("Server")))) + io.println(ansi.bold("Version:\t") <> server_status.version) + io.println(ansi.bold("Status:\t\t") <> server_status.status) + io.println( + ansi.bold("Last Reset:\t") + <> birl.legible_difference(now, server_status.reset_date), + ) + io.println( + ansi.bold("Next Reset:\t") + <> birl.legible_difference(now, server_status.server_resets.next) + <> " (" + <> ansi.italic(server_status.server_resets.frequency) + <> ")", + ) + case server_status.health.last_market_update { + Some(last_market_update) -> + io.println( + ansi.bold("Market Updated:\t") + <> birl.legible_difference(now, last_market_update), + ) + None -> Nil + } + io.println("") + + case print_links { + True -> { + io.println(ansi.bold(ansi.underline(ansi.blue("Links")))) + list.each(server_status.links, fn(link) { + io.println(ansi.bold(link.name) <> ": " <> uri.to_string(link.url)) + }) + io.println("") + } + False -> Nil + } + case print_leaderboards { + True -> { + io.println( + ansi.bold(ansi.underline(ansi.yellow("Leaderboard - Most Credits"))), + ) + list.each(server_status.leaderboards.most_credits, fn(entry) { + io.println( + ansi.bold(agent_symbol.to_string(entry.agent_symbol)) + <> ": " + <> int.to_string(entry.credits), + ) + }) + io.println("") + + io.println( + ansi.bold( + ansi.underline(ansi.yellow("Leaderboard - Most Submitted Charts")), + ), + ) + list.each(server_status.leaderboards.most_submitted_charts, fn(entry) { + io.println( + ansi.bold(agent_symbol.to_string(entry.agent_symbol)) + <> ": " + <> int.to_string(entry.chart_count), + ) + }) + } + False -> Nil + } +} + +pub fn contract(contract: Contract) -> Nil { + let now = birl.now() + io.println(ansi.bold(ansi.underline(ansi.yellow("Contract")))) + io.println(ansi.bold("Id:\t\t") <> contract_id.to_string(contract.id)) + io.println( + ansi.bold("Status:\t\t") + <> case contract.accepted { + True -> + case contract.fulfilled { + True -> "FULFILLED" + False -> "ACCEPTED" + } + False -> "NOT ACCEPTED" + }, + ) + io.println( + ansi.bold("Faction:\t") <> faction_symbol.to_string(contract.faction_symbol), + ) + io.println(ansi.bold("Type:\t\t") <> contract_type.to_string(contract.type_)) + io.println( + ansi.bold("Payment:\t") + <> int.to_string(contract.terms.payment.on_accepted) + <> " (on accept) + " + <> int.to_string(contract.terms.payment.on_fulfilled) + <> " (on fulfill) = " + <> int.to_string( + contract.terms.payment.on_accepted + contract.terms.payment.on_fulfilled, + ), + ) + io.println( + ansi.bold("Deadlines:\t") + <> case contract.deadline_to_accept { + Some(deadline) -> + "Accept by " + <> birl.to_iso8601(deadline) + <> " (" + <> ansi.italic(birl.legible_difference(now, deadline)) + <> ")" + None -> "No deadline to accept" + } + <> " / Fulfill by " + <> birl.to_iso8601(contract.terms.deadline) + <> " (" + <> ansi.italic(birl.legible_difference(now, contract.terms.deadline)) + <> ")", + ) + io.println( + ansi.bold("Deliver:\t") + <> case contract.terms.deliver { + Some(goods) -> + goods + |> list.map(fn(good) { + int.to_string(good.units_required) + <> "x " + <> trade_symbol.to_string(good.trade_symbol) + <> " to " + <> waypoint_symbol.to_string(good.destination_symbol) + }) + |> string.join(", ") + None -> "Nothing to deliver" + }, + ) +} + +pub fn ship(ship: Ship) -> Nil { + let now = birl.now() + io.println(ansi.bold(ansi.underline(ansi.blue("Ship")))) + io.println(ansi.bold("Symbol:\t\t") <> ship_symbol.to_string(ship.symbol)) + io.println(ansi.bold("Name:\t\t") <> ship.registration.name) + io.println( + ansi.bold("Role:\t\t") <> ship_role.to_string(ship.registration.role), + ) + io.println( + ansi.bold("Faction:\t") + <> faction_symbol.to_string(ship.registration.faction_symbol), + ) + io.println( + ansi.bold("Fuel:\t\t") + <> int.to_string(ship.fuel.current) + <> "/" + <> int.to_string(ship.fuel.capacity) + <> " (" + <> ansi.italic(case ship.fuel.consumed { + Some(ShipFuelConsumed(amount: 0, ..)) | None -> "none consumed" + Some(consumed) -> + "consumed " + <> int.to_string(consumed.amount) + <> " on " + <> birl.to_iso8601(consumed.timestamp) + }) + <> ")", + ) + io.println( + ansi.bold("Crew:\t\t") + <> int.to_string(ship.crew.current) + <> "/" + <> int.to_string(ship.crew.capacity) + <> " (" + <> ansi.italic(int.to_string(ship.crew.required) <> " required") + <> " / " + <> ansi.italic(int.to_string(ship.crew.morale) <> " morale") + <> " / " + <> ansi.italic(crew_rotation.to_string(ship.crew.rotation) <> " rotation") + <> ")", + ) + io.println( + ansi.bold("Cargo:\t\t") + <> int.to_string(ship.cargo.units) + <> "/" + <> int.to_string(ship.cargo.capacity), + ) + io.println( + ansi.bold("Cooldown:\t") + <> case ship.cooldown.expiration { + Some(expiration) -> + "Expires at " + <> birl.to_iso8601(expiration) + <> " (" + <> ansi.italic( + "in " <> int.to_string(ship.cooldown.remaining_seconds) <> " seconds", + ) + <> ")" + None -> ansi.italic("No active cooldown") + }, + ) + io.println( + ansi.bold("Location:\t") + <> ship_nav_status.to_string(ship.nav.status) + <> " (" + <> ansi.italic( + "in " <> ship_nav_flight_mode.to_string(ship.nav.flight_mode) <> " mode", + ) + <> ")" + <> " at " + <> waypoint_symbol.to_string(ship.nav.waypoint_symbol), + ) + io.println( + ansi.bold("Origin:\t\t") + <> case birl.compare(now, ship.nav.route.departure_time) { + order.Lt -> "Departing" + _ -> "Departed" + } + <> " " + <> waypoint_type.to_string(ship.nav.route.origin.type_) + <> " " + <> waypoint_symbol.to_string(ship.nav.route.origin.symbol) + <> " (" + <> ansi.italic( + int.to_string(ship.nav.route.origin.x) + <> ", " + <> int.to_string(ship.nav.route.origin.y), + ) + <> ") at " + <> birl.to_iso8601(ship.nav.route.departure_time) + <> " (" + <> ansi.italic(birl.legible_difference(now, ship.nav.route.departure_time)) + <> ")", + ) + io.println( + ansi.bold("Destination:\t") + <> case birl.compare(now, ship.nav.route.arrival) { + order.Lt -> "Arriving" + _ -> "Arrived" + } + <> " at " + <> waypoint_type.to_string(ship.nav.route.destination.type_) + <> " " + <> waypoint_symbol.to_string(ship.nav.route.destination.symbol) + <> " (" + <> ansi.italic( + int.to_string(ship.nav.route.destination.x) + <> ", " + <> int.to_string(ship.nav.route.destination.y), + ) + <> ") at " + <> birl.to_iso8601(ship.nav.route.arrival) + <> " (" + <> ansi.italic(birl.legible_difference(now, ship.nav.route.arrival)) + <> ")", + ) +}