1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
defmodule Chessh.SSH.Client do
alias IO.ANSI
alias Chessh.SSH.Client.Menu
require Logger
use GenServer
@clear_codes [
ANSI.clear(),
ANSI.reset(),
ANSI.home()
]
@min_terminal_width 64
@min_terminal_height 31
@max_terminal_width 255
@max_terminal_height 127
@terminal_bad_dim_msg [
@clear_codes | "The dimensions of your terminal are not within in the valid range"
]
defmodule State do
defstruct tui_pid: nil,
width: 0,
height: 0,
player_session: nil,
buffer: [],
state_stack: [{Menu, %Menu.State{}}]
end
@impl true
def init([%State{tui_pid: tui_pid} = state]) do
send(tui_pid, {:send_data, render(state)})
{:ok, state}
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{tui_pid: tui_pid, state_stack: [{module, _screen_state} | _tail]} = state
) do
action = keymap(data)
if action == :quit do
{:stop, :normal, state}
else
new_state = module.handle_input(action, state)
send(tui_pid, {:send_data, render(new_state)})
{:noreply, new_state}
end
end
def handle({:resize, {width, height}}, %State{tui_pid: tui_pid} = state) do
new_state = %State{state | width: width, height: height}
if height <= @max_terminal_height || width <= @max_terminal_width do
send(tui_pid, {:send_data, render(new_state)})
end
{:noreply, new_state}
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
x -> x
end
end
def terminal_size_allowed(width, height) do
Enum.member?(@min_terminal_width..@max_terminal_width, width) &&
Enum.member?(@min_terminal_height..@max_terminal_height, height)
end
defp render(
%State{width: width, height: height, state_stack: [{module, _screen_state}]} = state
) do
if terminal_size_allowed(width, height) do
[
@clear_codes ++
module.render(state)
]
else
@terminal_bad_dim_msg
end
end
end
|