Current section

Files

Jump to
extty src tty_cli.erl
Raw

src/tty_cli.erl

%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2005-2023. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%
%% This file is almost copied verbatem from Erlang's lib/ssh/src/ssh_cli.erl.
%% Last compared with OTP-26.0.2 and https://github.com/erlang/otp/pull/7499
%%
%% Description: a gen_server implementing a simple
%% terminal (using the group module) for a CLI
-module(tty_cli).
-include("tty_pty.hrl").
-export([io_request/4,to_group/2]).
to_group([], _Group) ->
ok;
to_group([$\^C | Tail], Group) ->
exit(Group, interrupt),
to_group(Tail, Group);
to_group(Data, Group) ->
Func = fun(C) -> C /= $\^C end,
Tail = case lists:splitwith(Func, Data) of
{[], Right} ->
Right;
{Left, Right} ->
Group ! {self(), {data, Left}},
Right
end,
to_group(Tail, Group).
%%--------------------------------------------------------------------
%%% io_request, handle io requests from the user process,
%%% Note, this is not the real I/O-protocol, but the mockup version
%%% used between edlin and a user_driver. The protocol tags are
%%% similar, but the message set is different.
%%% The protocol only exists internally between edlin and a character
%%% displaying device...
%%% We are *not* really unicode aware yet, we just filter away characters
%%% beyond the latin1 range. We however handle the unicode binaries...
io_request({window_change, OldTty}, Buf, Tty, _Group) ->
window_change(Tty, OldTty, Buf);
io_request({put_chars, Cs}, Buf, Tty, _Group) ->
put_chars(bin_to_list(Cs), Buf, Tty);
io_request({put_chars, unicode, Cs}, Buf, Tty, _Group) ->
put_chars(unicode:characters_to_list(Cs,unicode), Buf, Tty);
io_request({put_expand_no_trim, unicode, Expand}, Buf, Tty, _Group) ->
insert_chars(unicode:characters_to_list(Expand, unicode), Buf, Tty);
io_request({insert_chars, Cs}, Buf, Tty, _Group) ->
insert_chars(bin_to_list(Cs), Buf, Tty);
io_request({insert_chars, unicode, Cs}, Buf, Tty, _Group) ->
insert_chars(unicode:characters_to_list(Cs,unicode), Buf, Tty);
io_request({move_rel, N}, Buf, Tty, _Group) ->
move_rel(N, Buf, Tty);
io_request({move_line, N}, Buf, Tty, _Group) ->
move_line(N, Buf, Tty);
io_request({move_combo, L, V, R}, Buf, Tty, _Group) ->
{ML, Buf1} = move_rel(L, Buf, Tty),
{MV, Buf2} = move_line(V, Buf1, Tty),
{MR, Buf3} = move_rel(R, Buf2, Tty),
{[ML,MV,MR], Buf3};
io_request(new_prompt, _Buf, _Tty, _Group) ->
{[], {[], {[],[]}, [], 0 }};
io_request(delete_line, {_, {_, _}, _, Col}, Tty, _Group) ->
MoveToBeg = move_cursor(Col, 0, Tty),
{[MoveToBeg, "\e[J"],
{[],{[],[]},[],0}};
io_request({redraw_prompt, Pbs, Pbs2, {LB, {Bef, Aft}, LA}}, Buf, Tty, _Group) ->
{ClearLine, Cleared} = io_request(delete_line, Buf, Tty, _Group),
CL = lists:reverse(Bef,Aft),
Text = Pbs ++ lists:flatten(lists:join("\n"++Pbs2, lists:reverse(LB)++[CL|LA])),
Moves = if LA /= [] ->
[Last|_] = lists:reverse(LA),
{move_combo, -length(Last), -length(LA), length(Bef)};
true ->
{move_rel, -length(Aft)}
end,
{T, InsertedText} = io_request({insert_chars, unicode:characters_to_binary(Text)}, Cleared, Tty, _Group),
{M, Moved} = io_request(Moves, InsertedText, Tty, _Group),
{[ClearLine, T, M], Moved};
io_request({delete_chars,N}, Buf, Tty, _Group) ->
delete_chars(N, Buf, Tty);
io_request(clear, Buf, _Tty, _Group) ->
{"\e[H\e[2J", Buf};
io_request(beep, Buf, _Tty, _Group) ->
{[7], Buf};
%% New in R12
io_request({get_geometry,columns},Buf,Tty, _Group) ->
{ok, Tty#tty_pty.width, Buf};
io_request({get_geometry,rows},Buf,Tty, _Group) ->
{ok, Tty#tty_pty.height, Buf};
io_request({requests,Rs}, Buf, Tty, Group) ->
io_requests(Rs, Buf, Tty, [], Group);
io_request(tty_geometry, Buf, Tty, Group) ->
io_requests([{move_rel, 0}, {put_chars, unicode, [10]}],
Buf, Tty, [], Group);
%% New in 18
io_request({put_chars_sync, Class, Cs, Reply}, Buf, Tty, Group) ->
%% We handle these asynchronous for now, if we need output guarantees
%% we have to handle these synchronously
Group ! {reply, Reply, ok},
io_request({put_chars, Class, Cs}, Buf, Tty, Group);
io_request(_R, Buf, _Tty, _Group) ->
{[], Buf}.
io_requests([R|Rs], Buf, Tty, Acc, Group) ->
{Chars, NewBuf} = io_request(R, Buf, Tty, Group),
io_requests(Rs, NewBuf, Tty, [Acc|Chars], Group);
io_requests([], Buf, _Tty, Acc, _Group) ->
{Acc, Buf}.
%%% return commands for cursor navigation, assume everything is ansi
%%% (vt100), add clauses for other terminal types if needed
ansi_tty(N, L) ->
["\e[", integer_to_list(N), L].
get_tty_command(up, N, _TerminalType) ->
ansi_tty(N, $A);
get_tty_command(down, N, _TerminalType) ->
ansi_tty(N, $B);
get_tty_command(right, N, _TerminalType) ->
ansi_tty(N, $C);
get_tty_command(left, N, _TerminalType) ->
ansi_tty(N, $D).
-define(PAD, 10).
-define(TABWIDTH, 8).
%% convert input characters to buffer and to writeout
%% Note that the buf is reversed but the buftail is not
%% (this is handy; the head is always next to the cursor)
conv_buf([], {LB, {Bef, Aft}, LA, Col}, AccWrite, _Tty) ->
{{LB, {Bef, Aft}, LA}, lists:reverse(AccWrite), Col};
conv_buf([13, 10 | Rest], {LB, {Bef, Aft}, LA, Col}, AccWrite, Tty = #tty_pty{width = W}) ->
conv_buf(Rest, {[lists:reverse(Bef)|LB], {[], tl2(Aft)}, LA, Col+(W-(Col rem W))}, [10, 13 | AccWrite], Tty);
conv_buf([13 | Rest], {LB, {Bef, Aft}, LA, Col}, AccWrite, Tty = #tty_pty{width = W}) ->
conv_buf(Rest, {[lists:reverse(Bef)|LB], {[], tl1(Aft)}, LA, Col+(W-(Col rem W))}, [13 | AccWrite], Tty);
conv_buf([10 | Rest],{LB, {Bef, Aft}, LA, Col}, AccWrite0, Tty = #tty_pty{width = W}) ->
AccWrite =
case pty_opt(onlcr,Tty) of
0 -> [10 | AccWrite0];
1 -> [10,13 | AccWrite0];
undefined -> [10 | AccWrite0]
end,
conv_buf(Rest, {[lists:reverse(Bef)|LB], {[], tl1(Aft)}, LA, Col+(W - (Col rem W))}, AccWrite, Tty);
conv_buf([C | Rest], {LB, {Bef, Aft}, LA, Col}, AccWrite, Tty) ->
conv_buf(Rest, {LB, {[C|Bef], tl1(Aft)}, LA, Col+1}, [C | AccWrite], Tty).
%%% put characters before the prompt
put_chars(Chars, Buf, Tty) ->
case Buf of
{[],{[],[]},[],_} -> {_, WriteBuf, _} = conv_buf(Chars, Buf, [], Tty),
{WriteBuf, Buf};
_ ->
{Delete, DeletedState} = io_request(delete_line, Buf, Tty, []),
{_, PutBuffer, _} = conv_buf(Chars, DeletedState, [], Tty),
{Redraw, _} = io_request(redraw_prompt_pre_deleted, Buf, Tty, []),
{[Delete, PutBuffer, Redraw], Buf}
end.
%%% insert character at current position
insert_chars([], Buf, _Tty) ->
{[], Buf};
insert_chars(Chars, {_LB,{_Bef, Aft},LA, _Col}=Buf, Tty) ->
{{NewLB, {NewBef, _NewAft}, _NewLA}, WriteBuf, NewCol} = conv_buf(Chars, Buf, [], Tty),
M = move_cursor(special_at_width(NewCol+length(Aft), Tty), NewCol, Tty),
{[WriteBuf, Aft | M], {NewLB,{NewBef, Aft},LA, NewCol}}.
%%% delete characters at current position, (backwards if negative argument)
delete_chars(0, {LB,{Bef, Aft},LA, Col}, _Tty) ->
{[], {LB,{Bef, Aft},LA, Col}};
delete_chars(N, {LB,{Bef, Aft},LA, Col}, Tty) when N > 0 ->
NewAft = nthtail(N, Aft),
M = move_cursor(Col + length(NewAft) + N, Col, Tty),
{[NewAft, lists:duplicate(N, $ ) | M],
{LB,{Bef, NewAft},LA, Col}};
delete_chars(N, {LB,{Bef, Aft},LA, Col}, Tty) -> % N < 0
NewBef = nthtail(-N, Bef),
NewCol = case Col + N of V when V >= 0 -> V; _ -> 0 end,
M1 = move_cursor(Col, NewCol, Tty),
M2 = move_cursor(special_at_width(NewCol+length(Aft)-N, Tty), NewCol, Tty),
{[M1, Aft, lists:duplicate(-N, $ ) | M2],
{LB,{NewBef, Aft},LA, NewCol}}.
%%% Window change, redraw the current line (and clear out after it
%%% if current window is wider than previous)
window_change(Tty, OldTty, Buf)
when OldTty#tty_pty.width == Tty#tty_pty.width ->
%% No line width change
{[], Buf};
window_change(Tty, OldTty, {LB, {Bef, Aft}, LA, Col}) ->
case OldTty#tty_pty.width - Tty#tty_pty.width of
0 ->
%% No line width change
{[], {LB, {Bef, Aft}, LA, Col}};
DeltaW0 when DeltaW0 < 0,
Aft == [] ->
% Line width is decreased, cursor is at end of input
{[], {LB, {Bef, Aft}, LA, Col}};
DeltaW0 when DeltaW0 < 0,
Aft =/= [] ->
% Line width is decreased, cursor is not at end of input
{[], {LB, {Bef, Aft}, LA, Col}};
DeltaW0 when DeltaW0 > 0 ->
% Line width is increased
{[], {LB, {Bef, Aft}, LA, Col}}
end.
%% move around in buffer, respecting pad characters
step_over(0, {LB, {Bef, [?PAD |Aft]}, LA, Col}) ->
{LB, {[?PAD | Bef], Aft}, LA, Col+1};
step_over(0, {LB, {Bef, Aft}, LA, Col}) ->
{LB, {Bef, Aft}, LA, Col};
step_over(N, {LB, {[C | Bef], Aft}, LA, Col}) when N < 0 ->
N1 = ifelse(C == ?PAD, N, N+1),
step_over(N1, {LB, {Bef, [C | Aft]}, LA, Col-1});
step_over(N, {LB, {Bef, [C | Aft]}, LA, Col}) when N > 0 ->
N1 = ifelse(C == ?PAD, N, N-1),
step_over(N1, {LB, {[C | Bef], Aft}, LA, Col+1}).
%%% col and row from position with given width
col(N, W) -> N rem W.
row(N, W) -> N div W.
%%% move relative N characters
move_rel(N, {_LB, {_Bef, _Aft}, _LA, Col}=Buf, Tty) ->
{NewLB, {NewBef, NewAft}, NewLA, NewCol} = step_over(N, Buf),
M = move_cursor(Col, NewCol, Tty),
{M, {NewLB, {NewBef, NewAft}, NewLA, NewCol}}.
move_line(V, {_LB, {_Bef, _Aft}, _LA, Col}, Tty = #tty_pty{width=W})
when V < 0, length(_LB) >= -V ->
{LinesJumped, [B|NewLB]} = lists:split(-V -1, _LB),
CL = lists:reverse(_Bef,_Aft),
NewLA = lists:reverse([CL|LinesJumped], _LA),
{NewBB, NewAft} = lists:split(min(length(_Bef),length(B)), B),
NewBef = lists:reverse(NewBB),
NewCol = Col - length(_Bef) - lists:sum([((length(L)-1) div W)*W + W || L <- [B|LinesJumped]]) + length(NewBB),
M = move_cursor(Col, NewCol, Tty),
{M, {NewLB, {NewBef, NewAft}, NewLA, NewCol}};
move_line(V, {_LB, {_Bef, _Aft}, _LA, Col}, Tty = #tty_pty{width=W})
when V > 0, length(_LA) >= V ->
{LinesJumped, [A|NewLA]} = lists:split(V -1, _LA),
CL = lists:reverse(_Bef,_Aft),
NewLB = lists:reverse([CL|LinesJumped],_LB),
{NewBB, NewAft} = lists:split(min(length(_Bef),length(A)), A),
NewBef = lists:reverse(NewBB),
NewCol = Col - length(_Bef) + lists:sum([((length(L)-1) div W)*W + W || L <- [CL|LinesJumped]]) + length(NewBB),
M = move_cursor(Col, NewCol, Tty),
{M, {NewLB, {NewBef, NewAft}, NewLA, NewCol}};
move_line(_, Buf, _) ->
{"", Buf}.
%%% give move command for tty
move_cursor(A, A, _Tty) ->
[];
move_cursor(From, To, #tty_pty{width=Width, term=Type}) ->
Tcol = case col(To, Width) - col(From, Width) of
0 -> "";
I when I < 0 -> get_tty_command(left, -I, Type);
I -> get_tty_command(right, I, Type)
end,
Trow = case row(To, Width) - row(From, Width) of
0 -> "";
J when J < 0 -> get_tty_command(up, -J, Type);
J -> get_tty_command(down, J, Type)
end,
[Tcol | Trow].
%%% Caution for line "breaks"
special_at_width(From0, #tty_pty{width=Width}) when (From0 rem Width) == 0 -> From0 - 1;
special_at_width(From0, _) -> From0.
%%% tail, works with empty lists
tl1([_|A]) -> A;
tl1(_) -> [].
%%% second tail
tl2([_,_|A]) -> A;
tl2(_) -> [].
%%% nthtail as in lists, but no badarg if n > the length of list
nthtail(0, A) -> A;
nthtail(N, [_ | A]) when N > 0 -> nthtail(N-1, A);
nthtail(_, _) -> [].
ifelse(Cond, A, B) ->
case Cond of
true -> A;
_ -> B
end.
bin_to_list(B) when is_binary(B) ->
binary_to_list(B);
bin_to_list(L) when is_list(L) ->
lists:flatten([bin_to_list(A) || A <- L]);
bin_to_list(I) when is_integer(I) ->
I.
%%%----------------------------------------------------------------
pty_opt(Name, Tty) ->
try
proplists:get_value(Name, Tty#tty_pty.modes, undefined)
catch
_:_ -> undefined
end.