diff options
author | Simponic <loganhunt@simponic.xyz> | 2023-01-20 23:12:23 -0700 |
---|---|---|
committer | Simponic <loganhunt@simponic.xyz> | 2023-01-20 23:12:23 -0700 |
commit | 06f1ca76037397fb61c69319802ed029ac73e715 (patch) | |
tree | 37cdf1ebf5fcbaefda58d377bb6b4e3a8a11410b /lib/chessh/ssh/client/menus/select_paginate_poller.ex | |
parent | e0058fedfb191db8802a95548cbaf96b6fe58f80 (diff) | |
download | chessh-06f1ca76037397fb61c69319802ed029ac73e715.tar.gz chessh-06f1ca76037397fb61c69319802ed029ac73e715.zip |
Add pagination menus
Diffstat (limited to 'lib/chessh/ssh/client/menus/select_paginate_poller.ex')
-rw-r--r-- | lib/chessh/ssh/client/menus/select_paginate_poller.ex | 263 |
1 files changed, 263 insertions, 0 deletions
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 |