Add pagination menus
This commit is contained in:
parent
e0058fedfb
commit
06f1ca7603
@ -86,10 +86,7 @@ defmodule Chessh.PlayerSession do
|
||||
"Player #{player.username} has #{length(expired_sessions)} expired sessions - attempting to close them"
|
||||
)
|
||||
|
||||
Enum.map(expired_sessions, fn session_id ->
|
||||
:syn.publish(:player_sessions, {:session, session_id}, :session_closed)
|
||||
end)
|
||||
|
||||
Enum.map(expired_sessions, &close_session/1)
|
||||
Repo.delete_all(from(p in PlayerSession, where: p.id in ^expired_sessions))
|
||||
end
|
||||
|
||||
@ -112,8 +109,13 @@ defmodule Chessh.PlayerSession do
|
||||
def close_all_player_sessions(player) do
|
||||
player_sessions = Repo.all(from(p in PlayerSession, where: p.player_id == ^player.id))
|
||||
|
||||
Enum.map(player_sessions, fn session ->
|
||||
:syn.publish(:player_sessions, {:session, session.id}, :session_closed)
|
||||
end)
|
||||
Enum.map(player_sessions, &close_session(&1.id))
|
||||
end
|
||||
|
||||
defp close_session(id) do
|
||||
case :syn_backbone.get_table_name(:syn_pg_by_name, :player_sessions) do
|
||||
:undefined -> nil
|
||||
_ -> :syn.publish(:player_sessions, {:session, id}, :session_closed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -25,8 +25,8 @@ defmodule Chessh.SSH.Client do
|
||||
def init([%State{player_session: player_session} = state]) do
|
||||
send(
|
||||
self(),
|
||||
{:set_screen_process, Chessh.SSH.Client.Menu,
|
||||
%Chessh.SSH.Client.Menu.State{player_session: player_session}}
|
||||
{:set_screen_process, Chessh.SSH.Client.MainMenu,
|
||||
%Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}
|
||||
)
|
||||
|
||||
{:ok, state}
|
||||
|
@ -171,6 +171,11 @@ defmodule Chessh.SSH.Client.Game do
|
||||
{:noreply, new_state}
|
||||
end
|
||||
|
||||
def handle_info(x, state) do
|
||||
Logger.debug("unknown message in game process #{inspect(x)}")
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def input(
|
||||
width,
|
||||
height,
|
||||
|
@ -2,7 +2,6 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
||||
alias IO.ANSI
|
||||
alias Chessh.{Utils, Player}
|
||||
alias Chessh.SSH.Client.Game
|
||||
require Logger
|
||||
|
||||
@chess_board_height 8
|
||||
@chess_board_width 8
|
||||
@ -64,36 +63,11 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
||||
height: _height,
|
||||
highlighted: highlighted,
|
||||
flipped: flipped,
|
||||
game: %Chessh.Game{
|
||||
dark_player: %Player{username: dark_player},
|
||||
light_player: %Player{username: light_player},
|
||||
turn: turn,
|
||||
status: status,
|
||||
winner: winner
|
||||
}
|
||||
game: %Chessh.Game{} = game
|
||||
}) do
|
||||
rendered = [
|
||||
Enum.join(
|
||||
[
|
||||
ANSI.clear_line(),
|
||||
ANSI.format_fragment([@light_piece_color, light_player]),
|
||||
"#{ANSI.default_color()} --vs-- ",
|
||||
ANSI.format_fragment([@dark_piece_color, dark_player]),
|
||||
ANSI.default_color(),
|
||||
case status do
|
||||
:continue ->
|
||||
", #{ANSI.format_fragment([if(turn == :light, do: @light_piece_color, else: @dark_piece_color), if(turn == :dark, do: dark_player, else: light_player)])} to move"
|
||||
|
||||
:draw ->
|
||||
"ended in a draw"
|
||||
|
||||
:winner ->
|
||||
", #{ANSI.format_fragment([if(winner == :light, do: @light_piece_color, else: @dark_piece_color), if(winner == :dark, do: dark_player, else: light_player)])} won!"
|
||||
end,
|
||||
ANSI.default_color()
|
||||
],
|
||||
""
|
||||
)
|
||||
make_status_line(game, true)
|
||||
| draw_board(
|
||||
fen,
|
||||
{@tile_width, @tile_height},
|
||||
@ -111,6 +85,77 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
||||
)
|
||||
end
|
||||
|
||||
def make_status_line(
|
||||
%Chessh.Game{
|
||||
light_player: light_player
|
||||
} = game,
|
||||
fancy
|
||||
)
|
||||
when is_nil(light_player),
|
||||
do:
|
||||
make_status_line(
|
||||
%Chessh.Game{game | light_player: %Player{username: "(no opponent)"}},
|
||||
fancy
|
||||
)
|
||||
|
||||
def make_status_line(
|
||||
%Chessh.Game{
|
||||
dark_player: dark_player
|
||||
} = game,
|
||||
fancy
|
||||
)
|
||||
when is_nil(dark_player),
|
||||
do:
|
||||
make_status_line(
|
||||
%Chessh.Game{game | dark_player: %Player{username: "(no opponent)"}},
|
||||
fancy
|
||||
)
|
||||
|
||||
def make_status_line(
|
||||
%Chessh.Game{
|
||||
id: game_id,
|
||||
dark_player: %Player{username: dark_player},
|
||||
light_player: %Player{username: light_player},
|
||||
turn: turn,
|
||||
status: status,
|
||||
winner: winner,
|
||||
moves: moves
|
||||
},
|
||||
fancy
|
||||
) do
|
||||
Enum.join(
|
||||
[
|
||||
if(fancy,
|
||||
do: ANSI.clear_line(),
|
||||
else: ""
|
||||
),
|
||||
"Game #{game_id} - ",
|
||||
if(fancy,
|
||||
do: ANSI.format_fragment([@light_piece_color, light_player]),
|
||||
else: "#{light_player} (L)"
|
||||
),
|
||||
"#{if fancy, do: ANSI.default_color(), else: ""} --vs-- ",
|
||||
if(fancy,
|
||||
do: ANSI.format_fragment([@dark_piece_color, dark_player]),
|
||||
else: "#{dark_player} (D)"
|
||||
),
|
||||
if(fancy, do: ANSI.default_color(), else: ""),
|
||||
case status do
|
||||
:continue ->
|
||||
", #{moves} moves, #{ANSI.format_fragment([if(fancy, do: if(turn == :light, do: @light_piece_color, else: @dark_piece_color), else: ""), if(turn == :dark, do: dark_player, else: light_player)])} to move"
|
||||
|
||||
:draw ->
|
||||
"ended in a draw after #{moves} moves"
|
||||
|
||||
:winner ->
|
||||
", #{ANSI.format_fragment([if(fancy, do: if(winner == :light, do: @light_piece_color, else: @dark_piece_color), else: ""), if(winner == :dark, do: dark_player, else: light_player)])} won after #{moves} moves!"
|
||||
end,
|
||||
if(fancy, do: ANSI.default_color(), else: "")
|
||||
],
|
||||
""
|
||||
)
|
||||
end
|
||||
|
||||
defp draw_board(
|
||||
fen,
|
||||
{tile_width, tile_height} = tile_dims,
|
||||
|
@ -1,144 +0,0 @@
|
||||
defmodule Chessh.SSH.Client.Menu do
|
||||
alias IO.ANSI
|
||||
alias Chessh.{Utils, Repo, Game}
|
||||
import Ecto.Query
|
||||
|
||||
require Logger
|
||||
|
||||
defmodule State do
|
||||
defstruct client_pid: nil,
|
||||
selected: 0,
|
||||
player_session: nil,
|
||||
options: []
|
||||
end
|
||||
|
||||
@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"
|
||||
use Chessh.SSH.Client.Screen
|
||||
|
||||
def init([%State{} = state | _]) do
|
||||
{:ok, %State{state | options: options(state)}}
|
||||
end
|
||||
|
||||
def options(%State{player_session: player_session}) do
|
||||
current_games =
|
||||
Repo.all(
|
||||
from(g in Game,
|
||||
where: g.light_player_id == ^player_session.player_id,
|
||||
or_where: g.dark_player_id == ^player_session.player_id,
|
||||
where: g.status == :continue
|
||||
)
|
||||
)
|
||||
|
||||
joinable_games =
|
||||
Repo.all(
|
||||
from(g in Game,
|
||||
where: is_nil(g.light_player_id),
|
||||
or_where: is_nil(g.dark_player_id)
|
||||
)
|
||||
)
|
||||
|
||||
[
|
||||
{"Start A Game As Light",
|
||||
{Chessh.SSH.Client.Game,
|
||||
%Chessh.SSH.Client.Game.State{player_session: player_session, color: :light}}},
|
||||
{"Start A Game As Dark",
|
||||
{Chessh.SSH.Client.Game,
|
||||
%Chessh.SSH.Client.Game.State{player_session: player_session, color: :dark}}}
|
||||
] ++
|
||||
Enum.map(current_games, fn game ->
|
||||
{"Current Game - #{game.id}",
|
||||
{Chessh.SSH.Client.Game,
|
||||
%Chessh.SSH.Client.Game.State{player_session: player_session, game: game}}}
|
||||
end) ++
|
||||
Enum.map(joinable_games, fn game ->
|
||||
{"Joinable Game - #{game.id}",
|
||||
{Chessh.SSH.Client.Game,
|
||||
%Chessh.SSH.Client.Game.State{player_session: player_session, game: game}}}
|
||||
end) ++
|
||||
[
|
||||
{"Help", {}}
|
||||
]
|
||||
end
|
||||
|
||||
def input(
|
||||
width,
|
||||
height,
|
||||
action,
|
||||
%State{options: options, client_pid: client_pid, selected: selected} = state
|
||||
) do
|
||||
new_state =
|
||||
case(action) do
|
||||
:up ->
|
||||
%State{
|
||||
state
|
||||
| selected: Utils.wrap_around(selected, -1, length(options))
|
||||
}
|
||||
|
||||
:down ->
|
||||
%State{state | selected: Utils.wrap_around(selected, 1, length(options))}
|
||||
|
||||
:return ->
|
||||
{_option, {module, state}} = Enum.at(options, selected)
|
||||
send(client_pid, {:set_screen_process, module, state})
|
||||
state
|
||||
|
||||
_ ->
|
||||
state
|
||||
end
|
||||
|
||||
if !(action == :return) do
|
||||
send(client_pid, {:send_to_ssh, render_state(width, height, new_state)})
|
||||
end
|
||||
|
||||
new_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
|
||||
|
||||
defp render_state(
|
||||
width,
|
||||
height,
|
||||
%State{options: options, selected: selected}
|
||||
) do
|
||||
logo_lines = String.split(@logo, "\n")
|
||||
{logo_width, logo_height} = Utils.text_dim(@logo)
|
||||
{y, x} = Utils.center_rect({logo_width, logo_height + length(logo_lines)}, {width, height})
|
||||
|
||||
[ANSI.clear()] ++
|
||||
Enum.flat_map(
|
||||
Enum.zip(1..length(logo_lines), logo_lines),
|
||||
fn {i, line} ->
|
||||
[
|
||||
ANSI.cursor(y + i, x),
|
||||
line
|
||||
]
|
||||
end
|
||||
) ++
|
||||
Enum.flat_map(
|
||||
Enum.zip(0..(length(options) - 1), options),
|
||||
fn {i, {option, _}} ->
|
||||
[
|
||||
ANSI.cursor(y + length(logo_lines) + i + 1, x),
|
||||
if(i == selected,
|
||||
do:
|
||||
ANSI.format_fragment(
|
||||
[:light_cyan, :bright, "> #{option} <", :reset],
|
||||
true
|
||||
),
|
||||
else: option
|
||||
)
|
||||
]
|
||||
end
|
||||
) ++ [ANSI.home()]
|
||||
end
|
||||
end
|
24
lib/chessh/ssh/client/menus/game_selector.ex
Normal file
24
lib/chessh/ssh/client/menus/game_selector.ex
Normal file
@ -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
|
45
lib/chessh/ssh/client/menus/main_menu.ex
Normal file
45
lib/chessh/ssh/client/menus/main_menu.ex
Normal file
@ -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
|
85
lib/chessh/ssh/client/menus/select_current_game.ex
Normal file
85
lib/chessh/ssh/client/menus/select_current_game.ex
Normal file
@ -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
|
87
lib/chessh/ssh/client/menus/select_joinable_game.ex
Normal file
87
lib/chessh/ssh/client/menus/select_joinable_game.ex
Normal file
@ -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
|
263
lib/chessh/ssh/client/menus/select_paginate_poller.ex
Normal file
263
lib/chessh/ssh/client/menus/select_paginate_poller.ex
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user