Current section

Files

Jump to
observer_cli src observer_cli_plugin.erl
Raw

src/observer_cli_plugin.erl

%%% @author zhongwen <zhongwencool@gmail.com>
-module(observer_cli_plugin).
%% API
-export([start/1]).
-callback kv_label() -> [Rows] when
Rows :: #{
key => string(), key_width => pos_integer(),
value => string()|integer()|{byte, pos_integer()}, value_width => pos_integer()
}.
-callback sheet_header() -> [SheetHeader] when
SheetHeader :: #{title => string(), width => pos_integer(), shortcut => string()}.
-callback sheet_body() -> [SheetBody] when
SheetBody :: list().
-define(LAST_LINE, "refresh: ~wms q(quit) Positive Number(set refresh interval time ms) F/B(forward/back) Current pages is ~w").
-include("observer_cli.hrl").
-spec start(ViewOpts) -> no_return() when
ViewOpts :: view_opts().
start(#view_opts{plug = Plugs, auto_row = AutoRow} = ViewOpts) ->
NewPlugs = init_config(Plugs),
Pid = spawn_link(fun() ->
?output(?CLEAR),
render_worker(?INIT_TIME_REF, NewPlugs, AutoRow)
end),
manager(Pid, ViewOpts#view_opts{plug = NewPlugs}).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
init_config(#plug{plugs = []}) ->
application:ensure_all_started(observer_cli),
Plugs = application:get_env(observer_cli, plugins, []),
{_, NewPlugs} = lists:foldl(fun(M = #{module := Mod}, {Index, Acc}) ->
SheetWidth = get_sheet_width(Mod),
Config = maps:merge(#{cur_page => 1, sort_column => 2,
interval => 1500, sheet_width => SheetWidth}, M),
{Index + 1, maps:put(Index, Config, Acc)}
end, {1, #{}}, Plugs),
#plug{cur_index = 1, plugs = NewPlugs};
init_config(Plugs) -> Plugs.
manager(ChildPid, ViewOpts) ->
#view_opts{plug = PlugOpts = #plug{cur_index = CurIndex, plugs = Plugs}} = ViewOpts,
case parse_cmd() of
quit -> erlang:send(ChildPid, quit);
go_home ->
observer_cli_lib:exit_processes([ChildPid]),
observer_cli:start(ViewOpts);
{new_interval, NewMs} ->
observer_cli_lib:exit_processes([ChildPid]),
NewPlugs = update_plugins(CurIndex, Plugs, #{interval => NewMs}),
start(ViewOpts#view_opts{plug = PlugOpts#plug{plugs = NewPlugs}});
page_down_top_n ->
CurPlugs = maps:get(CurIndex, Plugs),
CurPage = maps:get(cur_page, CurPlugs),
NewPage = max(CurPage + 1, 1),
NewPlugs = update_plugins(CurIndex, Plugs, #{cur_page => NewPage}),
observer_cli_lib:exit_processes([ChildPid]),
start(ViewOpts#view_opts{plug = PlugOpts#plug{plugs = NewPlugs}});
page_up_top_n ->
CurPlugs = maps:get(CurIndex, Plugs),
CurPage = maps:get(cur_page, CurPlugs),
NewPage = max(CurPage - 1, 1),
NewPlugs = update_plugins(CurIndex, Plugs, #{cur_page => NewPage}),
observer_cli_lib:exit_processes([ChildPid]),
start(ViewOpts#view_opts{plug = PlugOpts#plug{plugs = NewPlugs}});
{input_str, Cmd} ->
case maybe_shortcut(Cmd, ViewOpts) of
{ok, menu, Index} ->
observer_cli_lib:exit_processes([ChildPid]),
start(ViewOpts#view_opts{plug = PlugOpts#plug{cur_index = Index}});
{ok, sheet, SortColumn} ->
NewPlugs = update_plugins(CurIndex, Plugs, #{sort_column => SortColumn}),
observer_cli_lib:exit_processes([ChildPid]),
start(ViewOpts#view_opts{plug = PlugOpts#plug{plugs = NewPlugs}});
{error, _} -> manager(ChildPid, ViewOpts)
end;
_ -> manager(ChildPid, ViewOpts)
end.
update_plugins(CurIndex, Lists, UpdateItems) ->
CurPlugs = maps:get(CurIndex, Lists),
NewPlugs = maps:merge(CurPlugs, UpdateItems),
maps:put(CurIndex, NewPlugs, Lists).
maybe_shortcut(Cmd, ViewOpts) ->
#view_opts{plug = #plug{cur_index = CurIndex, plugs = Plugs}} = ViewOpts,
case match_menu_shortcut(Cmd, maps:to_list(Plugs)) of
{ok, Index} ->
{ok, menu, Index};
{error, not_found} ->
case maps:find(CurIndex, Plugs) of
{ok, #{module := CurMod}} ->
case match_sheet_shortcut(Cmd, CurMod:sheet_header(), 1) of
{ok, Index} -> {ok, sheet, Index};
{error, _Reason} = Err -> Err
end;
_ -> {error, not_found}
end
end.
match_menu_shortcut(_Cmd, []) -> {error, not_found};
match_menu_shortcut(Cmd, [{Index, Plug} | Plugs]) ->
case Plug of
#{shortcut := Cmd} -> {ok, Index};
_ -> match_menu_shortcut(Cmd, Plugs)
end.
match_sheet_shortcut(_Cmd, [], _Index) -> {error, not_found};
match_sheet_shortcut(Shortcut, [#{shortcut := Shortcut} | _], Index) -> {ok, Index};
match_sheet_shortcut(Cmd, [_ | T], Index) -> match_sheet_shortcut(Cmd, T, Index + 1).
render_worker(LastTimeRef, #plug{cur_index = CurIndex, plugs = Plugs} = PlugInfo, AutoRow) ->
TerminalRow = observer_cli_lib:get_terminal_rows(AutoRow),
case maps:find(CurIndex, Plugs) of
{ok, #{interval := Interval, cur_page := CurPage, sheet_width := SheetWidth} = CurPlug} ->
Menu = render_menu(PlugInfo, SheetWidth),
{Labels, LabelLine} = render_kv_label(CurPlug),
Sheet = render_sheet(erlang:max(0, TerminalRow - LabelLine - 4), CurPlug),
LastText = io_lib:format(?LAST_LINE, [Interval, CurPage]),
LastLine = ?render([?UNDERLINE, ?GRAY_BG, ?W(LastText, SheetWidth)]),
?output([?CURSOR_TOP, Menu, Labels, Sheet, LastLine]),
NextTimeRef = observer_cli_lib:next_redraw(LastTimeRef, Interval),
receive
quit -> quit;
_ -> render_worker(NextTimeRef, PlugInfo, AutoRow)
end;
error ->
Menu = ?render([?UNDERLINE, ?W(?UNSELECT("Home(H)"), 30), ?W(?SELECT("EmptyPlugin"), 144)]),
ErrInfo = "|Plugin Error: Can't find your observer_cli config.\n|Please visit \"How to write your own plugin\" in readme.\n",
?output([?CURSOR_TOP, Menu, ErrInfo])
end.
parse_cmd() ->
case observer_cli_lib:to_list(io:get_line("")) of
"H\n" -> go_home;
"B\n" -> page_up_top_n; %% backward
"F\n" -> page_down_top_n; %% forward
"q\n" -> quit;
Number -> observer_cli_lib:parse_integer(Number)
end.
render_menu(#plug{cur_index = CurIndex, plugs = Plugs}, SheetWidth) ->
Num = maps:size(Plugs),
Title = get_menu_title(CurIndex, Plugs, Num, []),
[Time] = observer_cli_lib:uptime(),
?render([?UNDERLINE, ?W(
[
?UNSELECT("Home(H)"), "|",
Title
],
SheetWidth + Num * 21 + 20 - 19), Time]).
get_menu_title(CurIndex, Plugs, CurIndex, Acc) ->
{ok, #{title := Title, shortcut := Shortcut}} = maps:find(CurIndex, Plugs),
NewTitle = Title ++ "(" ++ Shortcut ++ ")",
NewAcc = [?SELECT(NewTitle), "|" | Acc],
get_menu_title(CurIndex, Plugs, CurIndex - 1, NewAcc);
get_menu_title(CurIndex, Plugs, Pos, Acc) ->
case maps:find(Pos, Plugs) of
error -> Acc;
{ok, #{title := Title, shortcut := Shortcut}} ->
NewTitle = Title ++ "(" ++ Shortcut ++ ")",
NewAcc = [?UNSELECT(NewTitle), "|" | Acc],
get_menu_title(CurIndex, Plugs, Pos - 1, NewAcc)
end.
render_kv_label(#{module := Module}) ->
RenderLabel =
[begin
L = [begin [?W(Name, NWidth), ?W(Value, VWidth)] end ||
#{key := Name, key_width := NWidth, value := Value, value_width := VWidth} <- Label],
?render(lists:flatten(L))
end || Label <- Module:kv_label()],
{RenderLabel, length(RenderLabel)}.
render_sheet(Rows, #{sort_column := SortColumn, cur_page := CurPage, module := Module}) ->
{Headers, Widths} = render_sheet_header(Module, SortColumn),
Body = render_sheet_body(Module, CurPage, Rows, SortColumn, Widths),
[Headers | Body].
render_sheet_header(Module, SortRow) ->
SheetHeader = Module:sheet_header(),
{Headers, Widths, _} = lists:foldl(fun(#{title := Header, width := Width} = H,
{HeaderAcc, WidthAcc, Index}) ->
Title =
case maps:get(shortcut, H, "") of
"" -> Header;
Shortcut -> Header ++ "(" ++ Shortcut ++ ")"
end,
case Index =:= SortRow of
true ->
{
[?UNDERLINE, ?W2(?RED_BG, Title, Width) | HeaderAcc],
[Width | WidthAcc],
Index - 1
};
false ->
{
[?UNDERLINE, ?W2(?GRAY_BG, Title, Width) | HeaderAcc],
[Width | WidthAcc],
Index - 1
}
end
end, {[], [], length(SheetHeader)},
lists:reverse(SheetHeader)),
{?render(Headers), Widths}.
render_sheet_body(Module, CurPage, Rows, SortRow, Widths) ->
DataSet = lists:map(fun(I) ->
{0, lists:nth(SortRow, I), I} end,
Module:sheet_body()),
SortData = observer_cli_lib:sublist(DataSet, Rows, CurPage),
[begin
List = mix_value_width(Item, Widths, []),
?render(List)
end || {_, _, Item} <- SortData].
mix_value_width([], _, Acc) -> lists:reverse(Acc);
mix_value_width([I | IRest], [W | WRest], []) -> %% first
mix_value_width(IRest, WRest, [?W(I, W - 1)]);
mix_value_width([I], [W], Acc) -> %% last
mix_value_width([], [], [?W(I, W - 1) | Acc]);
mix_value_width([I | IRest], [W | WRest], Acc) -> %% middle
mix_value_width(IRest, WRest, [?W(I, W - 2) | Acc]).
get_sheet_width(Mod) ->
Width = lists:foldl(fun(#{width := W}, Acc) -> Acc + W + 1 end, 1, Mod:sheet_header()),
case Width > 1 of
true -> Width - 2;
false -> ?COLUMN + 5
end.