148 lines
3.6 KiB
Elixir
148 lines
3.6 KiB
Elixir
defmodule Chessh.SSH.Client do
|
|
alias IO.ANSI
|
|
require Logger
|
|
|
|
use GenServer
|
|
|
|
@clear_codes [
|
|
ANSI.clear(),
|
|
ANSI.home()
|
|
]
|
|
|
|
@min_terminal_width 64
|
|
@min_terminal_height 31
|
|
@max_terminal_width 220
|
|
@max_terminal_height 100
|
|
|
|
defmodule State do
|
|
defstruct tui_pid: nil,
|
|
width: 0,
|
|
height: 0,
|
|
player_session: nil,
|
|
screen_processes: []
|
|
end
|
|
|
|
@impl true
|
|
def init([%State{} = state]) do
|
|
{:ok, screen_pid} =
|
|
GenServer.start_link(Chessh.SSH.Client.Board, [
|
|
%Chessh.SSH.Client.Board.State{client_pid: self()}
|
|
])
|
|
|
|
{:ok, %{state | screen_processes: [screen_pid]}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(:quit, %State{} = state) do
|
|
{:stop, :normal, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(msg, state) do
|
|
[burst_ms, burst_rate] =
|
|
Application.get_env(:chessh, RateLimits)
|
|
|> Keyword.take([:player_session_message_burst_ms, :player_session_message_burst_rate])
|
|
|> Keyword.values()
|
|
|
|
case Hammer.check_rate_inc(
|
|
"player-session-#{state.player_session.id}-burst-message-rate",
|
|
burst_ms,
|
|
burst_rate,
|
|
1
|
|
) do
|
|
{:allow, _count} ->
|
|
handle(msg, state)
|
|
|
|
{:deny, _limit} ->
|
|
{:noreply, state}
|
|
end
|
|
end
|
|
|
|
def handle(
|
|
{:data, data},
|
|
%State{width: width, height: height, screen_processes: [screen_pid | _]} = state
|
|
) do
|
|
action = keymap(data)
|
|
|
|
if action == :quit do
|
|
{:stop, :normal, state}
|
|
else
|
|
send(screen_pid, {:input, width, height, action})
|
|
{:noreply, state}
|
|
end
|
|
end
|
|
|
|
def handle(
|
|
:refresh,
|
|
%State{} = state
|
|
) do
|
|
render(state)
|
|
{:noreply, state}
|
|
end
|
|
|
|
def handle(
|
|
{:send_to_ssh, data},
|
|
%State{width: width, height: height, tui_pid: tui_pid} = state
|
|
) do
|
|
case get_terminal_dim_msg(width, height) do
|
|
{true, msg} -> send(tui_pid, {:send_data, msg})
|
|
{false, _} -> send(tui_pid, {:send_data, data})
|
|
end
|
|
|
|
{:noreply, state}
|
|
end
|
|
|
|
def handle(
|
|
{:resize, {width, height}},
|
|
%State{tui_pid: tui_pid, screen_processes: [screen_pid | _]} = state
|
|
) do
|
|
case get_terminal_dim_msg(width, height) do
|
|
{true, msg} -> send(tui_pid, {:send_data, msg})
|
|
{false, _} -> send(screen_pid, {:render, width, height})
|
|
end
|
|
|
|
{:noreply, %State{state | width: width, height: height}}
|
|
end
|
|
|
|
def keymap(key) do
|
|
case key do
|
|
# Exit keys - C-c and C-d
|
|
<<3>> -> :quit
|
|
<<4>> -> :quit
|
|
# Arrow keys
|
|
"\e[A" -> :up
|
|
"\e[B" -> :down
|
|
"\e[D" -> :left
|
|
"\e[C" -> :right
|
|
"\r" -> :return
|
|
x -> x
|
|
end
|
|
end
|
|
|
|
defp get_terminal_dim_msg(width, height) do
|
|
case {height > @max_terminal_height, height < @min_terminal_height,
|
|
width > @max_terminal_width, width < @min_terminal_width} do
|
|
{true, _, _, _} -> {true, @clear_codes ++ ["The terminal height is too large."]}
|
|
{_, true, _, _} -> {true, @clear_codes ++ ["The terminal height is too small."]}
|
|
{_, _, true, _} -> {true, @clear_codes ++ ["The terminal width is too large"]}
|
|
{_, _, _, true} -> {true, @clear_codes ++ ["The terminal width is too small."]}
|
|
{false, false, false, false} -> {false, nil}
|
|
end
|
|
end
|
|
|
|
defp render(%State{
|
|
tui_pid: tui_pid,
|
|
width: width,
|
|
height: height,
|
|
screen_processes: [screen_pid | _]
|
|
}) do
|
|
{out_of_range, msg} = get_terminal_dim_msg(width, height)
|
|
|
|
if out_of_range && msg do
|
|
send(tui_pid, {:send_data, msg})
|
|
else
|
|
send(screen_pid, {:render, width, height})
|
|
end
|
|
end
|
|
end
|