diff options
Diffstat (limited to 'lib/chessh/ssh/client/menus')
-rw-r--r-- | lib/chessh/ssh/client/menus/game_selector.ex | 24 | ||||
-rw-r--r-- | lib/chessh/ssh/client/menus/main_menu.ex | 45 | ||||
-rw-r--r-- | lib/chessh/ssh/client/menus/select_current_game.ex | 85 | ||||
-rw-r--r-- | lib/chessh/ssh/client/menus/select_joinable_game.ex | 87 | ||||
-rw-r--r-- | lib/chessh/ssh/client/menus/select_paginate_poller.ex | 263 |
5 files changed, 504 insertions, 0 deletions
diff --git a/lib/chessh/ssh/client/menus/game_selector.ex b/lib/chessh/ssh/client/menus/game_selector.ex new file mode 100644 index 0000000..7792082 --- /dev/null +++ b/lib/chessh/ssh/client/menus/game_selector.ex @@ -0,0 +1,24 @@ +defmodule Chessh.SSH.Client.GameSelector do + import Ecto.Query + alias Chessh.Repo + + def paginate_ish_query(query, current_id, direction) do + sorted_query = + if direction == :desc, + do: from(g in query, order_by: [desc: g.id]), + else: from(g in query, order_by: [asc: g.id]) + + results = + if !is_nil(current_id) do + if direction == :desc, + do: from(g in sorted_query, where: g.id < ^current_id), + else: from(g in sorted_query, where: g.id > ^current_id) + else + sorted_query + end + |> Repo.all() + |> Repo.preload([:light_player, :dark_player]) + + if direction == :desc, do: results, else: Enum.reverse(results) + end +end diff --git a/lib/chessh/ssh/client/menus/main_menu.ex b/lib/chessh/ssh/client/menus/main_menu.ex new file mode 100644 index 0000000..167a0ef --- /dev/null +++ b/lib/chessh/ssh/client/menus/main_menu.ex @@ -0,0 +1,45 @@ +defmodule Chessh.SSH.Client.MainMenu do + alias IO.ANSI + alias Chessh.PlayerSession + + require Logger + + @logo " Simponic's + dP MP\"\"\"\"\"\"`MM MP\"\"\"\"\"\"`MM M\"\"MMMMM\"\"MM + 88 M mmmmm..M M mmmmm..M M MMMMM MM +.d8888b. 88d888b. .d8888b. M. `YM M. `YM M `M +88' `\"\" 88' `88 88ooood8 MMMMMMM. M MMMMMMM. M M MMMMM MM +88. ... 88 88 88. ... M. .MMM' M M. .MMM' M M MMMMM MM +`88888P' dP dP `88888P' Mb. .dM Mb. .dM M MMMMM MM + MMMMMMMMMMM MMMMMMMMMMM MMMMMMMMMMMM" |> String.split("\n") + @logo_cols @logo |> Enum.map(&String.length/1) |> Enum.max() + + use Chessh.SSH.Client.SelectPaginatePoller + + def dynamic_options(), do: false + def tick_delay_ms(), do: 1000 + def max_displayed_options(), do: 4 + def max_box_cols(), do: @logo_cols + def title(), do: @logo + + def initial_options(%State{player_session: %PlayerSession{} = player_session}) do + [ + {"Start A Game (Light)", + {Chessh.SSH.Client.Game, + %Chessh.SSH.Client.Game.State{player_session: player_session, color: :light}}}, + {"Start A Game (Dark)", + {Chessh.SSH.Client.Game, + %Chessh.SSH.Client.Game.State{player_session: player_session, color: :dark}}}, + {"Current Games", + {Chessh.SSH.Client.SelectCurrentGame, + %Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}}, + {"Joinable Games (lobby)", + {Chessh.SSH.Client.SelectJoinableGame, + %Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}} + ] + end + + def make_process_tuple(selected, _state) do + selected + end +end diff --git a/lib/chessh/ssh/client/menus/select_current_game.ex b/lib/chessh/ssh/client/menus/select_current_game.ex new file mode 100644 index 0000000..ff1eb30 --- /dev/null +++ b/lib/chessh/ssh/client/menus/select_current_game.ex @@ -0,0 +1,85 @@ +defmodule Chessh.SSH.Client.SelectCurrentGame do + alias Chessh.{Utils, Repo, Game, PlayerSession} + alias Chessh.SSH.Client.GameSelector + import Ecto.Query + require Logger + + use Chessh.SSH.Client.SelectPaginatePoller + + def refresh_options_ms(), do: 4000 + def max_displayed_options(), do: 10 + def title(), do: ["-- Current Games --"] + def dynamic_options(), do: true + + def get_player_sorted_current_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do + GameSelector.paginate_ish_query( + Game + |> where([g], g.status == :continue) + |> where([g], g.light_player_id == ^player_id or g.dark_player_id == ^player_id) + |> limit(^max_displayed_options()), + current_id, + direction + ) + end + + def format_game_selection_tuple(%Game{id: game_id} = game) do + {Chessh.SSH.Client.Game.Renderer.make_status_line(game, false), game_id} + end + + def next_page_options(%State{ + options: options, + player_session: %PlayerSession{player_id: player_id} + }) do + {_label, previous_last_game_id} = List.last(options) + next_games = get_player_sorted_current_games_with_id(player_id, previous_last_game_id, :desc) + + if length(next_games) > 0, + do: + next_games + |> Enum.map(&format_game_selection_tuple/1), + else: options + end + + def previous_page_options(%State{ + options: options, + player_session: %PlayerSession{player_id: player_id} + }) do + {_label, previous_first_game_id} = List.first(options) + + previous_games = + get_player_sorted_current_games_with_id(player_id, previous_first_game_id, :asc) + + if length(previous_games) > 0, + do: + previous_games + |> Enum.map(&format_game_selection_tuple/1), + else: options + end + + def initial_options(%State{player_session: %PlayerSession{player_id: player_id}}) do + get_player_sorted_current_games_with_id(player_id) + |> Enum.map(&format_game_selection_tuple/1) + end + + def refresh_options(%State{options: options}) do + from(g in Game, + where: g.id in ^Enum.map(options, fn {_, id} -> id end), + order_by: [desc: g.id] + ) + |> Repo.all() + |> Repo.preload([:light_player, :dark_player]) + |> Enum.map(&format_game_selection_tuple/1) + end + + def make_process_tuple(selected_id, %State{ + player_session: player_session + }) do + game = Repo.get(Game, selected_id) + + {Chessh.SSH.Client.Game, + %Chessh.SSH.Client.Game.State{ + player_session: player_session, + game: game + }} + end +end diff --git a/lib/chessh/ssh/client/menus/select_joinable_game.ex b/lib/chessh/ssh/client/menus/select_joinable_game.ex new file mode 100644 index 0000000..3b3e249 --- /dev/null +++ b/lib/chessh/ssh/client/menus/select_joinable_game.ex @@ -0,0 +1,87 @@ +defmodule Chessh.SSH.Client.SelectJoinableGame do + alias Chessh.{Utils, Repo, Game, PlayerSession} + alias Chessh.SSH.Client.GameSelector + import Ecto.Query + + use Chessh.SSH.Client.SelectPaginatePoller + + def refresh_options_ms(), do: 4000 + def max_displayed_options(), do: 1 + def title(), do: ["-- Joinable Games --"] + def dynamic_options(), do: true + + def get_player_joinable_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do + GameSelector.paginate_ish_query( + Game + |> where([g], g.status == :continue) + |> where( + [g], + (is_nil(g.dark_player_id) or g.dark_player_id != ^player_id) and + (is_nil(g.light_player_id) or g.light_player_id != ^player_id) + ) + |> limit(^max_displayed_options()), + current_id, + direction + ) + end + + def format_game_selection_tuple(%Game{id: game_id} = game) do + {Chessh.SSH.Client.Game.Renderer.make_status_line(game, false), game_id} + end + + def next_page_options(%State{ + options: options, + player_session: %PlayerSession{player_id: player_id} + }) do + {_label, previous_last_game_id} = List.last(options) + next_games = get_player_joinable_games_with_id(player_id, previous_last_game_id, :desc) + + if length(next_games) > 0, + do: + next_games + |> Enum.map(&format_game_selection_tuple/1), + else: options + end + + def previous_page_options(%State{ + options: options, + player_session: %PlayerSession{player_id: player_id} + }) do + {_label, previous_first_game_id} = List.first(options) + + previous_games = get_player_joinable_games_with_id(player_id, previous_first_game_id, :asc) + + if length(previous_games) > 0, + do: + previous_games + |> Enum.map(&format_game_selection_tuple/1), + else: options + end + + def initial_options(%State{player_session: %PlayerSession{player_id: player_id}}) do + get_player_joinable_games_with_id(player_id) + |> Enum.map(&format_game_selection_tuple/1) + end + + def refresh_options(%State{options: options}) do + from(g in Game, + where: g.id in ^Enum.map(options, fn {_, id} -> id end), + order_by: [desc: g.id] + ) + |> Repo.all() + |> Repo.preload([:light_player, :dark_player]) + |> Enum.map(&format_game_selection_tuple/1) + end + + def make_process_tuple(selected_id, %State{ + player_session: player_session + }) do + game = Repo.get(Game, selected_id) + + {Chessh.SSH.Client.Game, + %Chessh.SSH.Client.Game.State{ + player_session: player_session, + game: game + }} + end +end diff --git a/lib/chessh/ssh/client/menus/select_paginate_poller.ex b/lib/chessh/ssh/client/menus/select_paginate_poller.ex new file mode 100644 index 0000000..c6f9e1d --- /dev/null +++ b/lib/chessh/ssh/client/menus/select_paginate_poller.ex @@ -0,0 +1,263 @@ +defmodule Chessh.SSH.Client.SelectPaginatePoller do + @callback dynamic_options() :: boolean() + + @callback tick_delay_ms() :: integer() + @callback max_displayed_options() :: integer() + @callback max_box_cols() :: integer() + @callback make_process_tuple(selected :: any(), state :: any()) :: + {module :: module(), state :: any()} + + @callback initial_options(state :: any()) :: + [{line :: any(), selected :: any()}] + + @callback refresh_options_ms() :: integer() + @callback refresh_options(state :: any()) :: + [{line :: any(), selected :: any()}] + @callback next_page_options(state :: any()) :: + [{line :: any(), selected :: any()}] + @callback previous_page_options(state :: any()) :: + [{line :: any(), selected :: any()}] + + @callback title() :: [any()] + + defmodule State do + defstruct client_pid: nil, + selected_option_idx: 0, + player_session: nil, + options: [], + tick: 0, + cursor: nil + end + + defmacro __using__(_) do + quote do + @behaviour Chessh.SSH.Client.SelectPaginatePoller + use Chessh.SSH.Client.Screen + + alias IO.ANSI + alias Chessh.{Utils, PlayerSession} + alias Chessh.SSH.Client.SelectPaginatePoller.State + alias Chessh.SSH.Client.SelectPaginatePoller + + require Logger + + def init([%State{} = state | _]) do + if dynamic_options() do + Process.send_after(self(), :refresh_options, refresh_options_ms()) + end + + Process.send_after(self(), :tick, tick_delay_ms()) + + {:ok, %State{state | options: initial_options(state)}} + end + + def handle_info( + :refresh_options, + %State{ + selected_option_idx: selected_option_idx, + tick: tick, + client_pid: client_pid + } = state + ) do + if dynamic_options() do + options = refresh_options(state) + Process.send_after(self(), :refresh_options, refresh_options_ms()) + + {:noreply, + %State{ + state + | selected_option_idx: min(selected_option_idx, length(options) - 1), + options: options + }} + else + {:noreply, state} + end + end + + def handle_info( + :tick, + %State{ + tick: tick, + client_pid: client_pid + } = state + ) do + Process.send_after(self(), :tick, tick_delay_ms()) + + if client_pid do + send(client_pid, :refresh) + end + + {:noreply, %State{state | tick: tick + 1}} + end + + def handle_info( + x, + state + ) do + Logger.debug("unknown message in pagination poller - #{inspect(x)}") + + {:noreply, state} + end + + def render( + width, + height, + %State{ + client_pid: client_pid + } = state + ) do + send( + client_pid, + {:send_to_ssh, render_state(width, height, state)} + ) + + state + end + + def input( + width, + height, + action, + %State{ + client_pid: client_pid, + options: options, + selected_option_idx: selected_option_idx + } = state + ) do + max_item = min(length(options), max_displayed_options()) + + new_state = + if(max_item > 0, + do: + case action do + :up -> + %State{ + state + | selected_option_idx: Utils.wrap_around(selected_option_idx, -1, max_item), + tick: 0 + } + + :down -> + %State{ + state + | selected_option_idx: Utils.wrap_around(selected_option_idx, 1, max_item), + tick: 0 + } + + :left -> + if dynamic_options(), + do: %State{ + state + | options: previous_page_options(state), + selected_option_idx: 0, + tick: 0 + } + + :right -> + if dynamic_options(), + do: %State{ + state + | options: next_page_options(state), + selected_option_idx: 0, + tick: 0 + } + + :return -> + {_, selected} = Enum.at(options, selected_option_idx) + {module, state} = make_process_tuple(selected, state) + send(client_pid, {:set_screen_process, module, state}) + state + + _ -> + nil + end + ) || state + + if !(action == :return) do + render(width, height, new_state) + end + + new_state + end + + defp render_state( + width, + height, + %State{} = state + ) do + lines = + title() ++ + [""] ++ + render_lines(width, height, state) ++ + if dynamic_options(), do: ["", "<= Previous | Next =>"], else: [] + + {y, x} = Utils.center_rect({min(width, max_box_cols()), length(lines)}, {width, height}) + + [ANSI.clear()] ++ + Enum.flat_map( + Enum.zip(0..(length(lines) - 1), lines), + fn {i, line} -> + [ + ANSI.cursor(y + i, x), + line + ] + end + ) ++ [ANSI.home()] + end + + defp render_lines(width, _height, %State{ + tick: tick, + options: options, + selected_option_idx: selected_option_idx + }) do + if options && length(options) > 0 do + Enum.map( + Enum.zip(0..(max_displayed_options() - 1), options), + fn {i, {line, _}} -> + box_cols = min(max_box_cols(), width) + linelen = String.length(line) + + line = + if linelen > box_cols do + delta = max(box_cols - 3 - 1, 0) + overflow = linelen - delta + start = if i == selected_option_idx, do: rem(tick, overflow), else: 0 + "#{String.slice(line, start..(start + delta))}..." + else + line + end + + if i == selected_option_idx do + ANSI.format_fragment( + [:light_cyan, :bright, "> #{line} <", :reset], + true + ) + else + line + end + end + ) + else + ["Looks like there's nothing here.", "Use Ctrl+b to go back"] + end + end + + def refresh_options_ms(), do: 3000 + def next_page_options(%State{options: options}), do: options + def previous_page_options(%State{options: options}), do: options + def refresh_options(%State{options: options}), do: options + + def tick_delay_ms(), do: 1000 + def max_displayed_options(), do: 10 + def max_box_cols(), do: 90 + + defoverridable refresh_options_ms: 0, + next_page_options: 1, + previous_page_options: 1, + refresh_options: 1, + tick_delay_ms: 0, + max_displayed_options: 0, + max_box_cols: 0 + end + end +end |