summaryrefslogtreecommitdiff
path: root/lib/kernel
diff options
context:
space:
mode:
Diffstat (limited to 'lib/kernel')
-rw-r--r--lib/kernel/src/group.erl348
-rw-r--r--lib/kernel/src/prim_tty.erl380
-rw-r--r--lib/kernel/src/user_drv.erl48
-rw-r--r--lib/kernel/test/interactive_shell_SUITE.erl150
4 files changed, 686 insertions, 240 deletions
diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl
index 311c2f3c5e..ca7251d7ec 100644
--- a/lib/kernel/src/group.erl
+++ b/lib/kernel/src/group.erl
@@ -483,50 +483,56 @@ get_chars_n(Prompt, M, F, Xa, Drv, Shell, Buf, Encoding) ->
Pbs = prompt_bytes(Prompt, Encoding),
case get(echo) of
true ->
- get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf, start, Encoding);
+ get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf, start, [], Encoding);
false ->
get_chars_n_loop(Pbs, M, F, Xa, Drv, Shell, Buf, start, Encoding)
end.
get_chars_line(Prompt, M, F, Xa, Drv, Shell, Buf, Encoding) ->
Pbs = prompt_bytes(Prompt, Encoding),
- get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf, start, Encoding).
+ get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf, start, [], Encoding).
-get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf0, State, Encoding) ->
+get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf0, State, LineCont0, Encoding) ->
Result = case get(echo) of
- true ->
- get_line(Buf0, Pbs, Drv, Shell, Encoding);
- false ->
- % get_line_echo_off only deals with lists
- % and does not need encoding...
- get_line_echo_off(Buf0, Pbs, Drv, Shell)
- end,
+ true ->
+ get_line(Buf0, Pbs, LineCont0, Drv, Shell, Encoding);
+ false ->
+ %% get_line_echo_off only deals with lists
+ %% and does not need encoding...
+ get_line_echo_off(Buf0, Pbs, Drv, Shell)
+ end,
case Result of
- {done,Line,Buf} ->
- get_chars_apply(Pbs, M, F, Xa, Drv, Shell, Buf, State, Line, Encoding);
- interrupted ->
- {error,{error,interrupted},[]};
- terminated ->
- {exit,terminated}
+ {done,LineCont1,Buf} ->
+ get_chars_apply(Pbs, M, F, Xa, Drv, Shell, Buf, State, LineCont1, Encoding);
+
+ interrupted ->
+ {error,{error,interrupted},[]};
+ terminated ->
+ {exit,terminated}
end.
-get_chars_apply(Pbs, M, F, Xa, Drv, Shell, Buf, State0, Line, Encoding) ->
- case catch M:F(State0, cast(Line,get(read_mode), Encoding), Encoding, Xa) of
+get_chars_apply(Pbs, M, F, Xa, Drv, Shell, Buf, State0, LineCont, Encoding) ->
+ %% multi line support means that we should not keep the state
+ %% but we need to keep it for oldshell mode
+ {State, Line} = case get(echo) of
+ true -> {start, edlin:current_line(LineCont)};
+ false -> {State0, LineCont}
+ end,
+ case catch M:F(State, cast(Line,get(read_mode), Encoding), Encoding, Xa) of
{stop,Result,eof} ->
{ok,Result,eof};
{stop,Result,Rest} ->
- case {M,F} of
- {io_lib, get_until} ->
- save_line_buffer(Line, get_lines(new_stack(get(line_buffer)))),
- {ok,Result,append(Rest, Buf, Encoding)};
- _ ->
- {ok,Result,append(Rest, Buf, Encoding)}
- end;
+ _ = case {M,F} of
+ {io_lib, get_until} ->
+ save_line_buffer(string:trim(Line, both)++"\n", get_lines(new_stack(get(line_buffer))));
+ _ ->
+ skip
+ end,
+ {ok,Result,append(Rest, Buf, Encoding)};
{'EXIT',_} ->
{error,{error,err_func(M, F, Xa)},[]};
State1 ->
- save_line_buffer(Line, get_lines(new_stack(get(line_buffer)))),
- get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf, State1, Encoding)
+ get_chars_loop(Pbs, M, F, Xa, Drv, Shell, Buf, State1, LineCont, Encoding)
end.
get_chars_n_loop(Pbs, M, F, Xa, Drv, Shell, Buf0, State, Encoding) ->
@@ -558,65 +564,73 @@ err_func(_, F, _) ->
%% Get a line with eventual line editing. Handle other io requests
%% while getting line.
%% Returns:
-%% {done,LineChars,RestChars}
-%% interrupted
-
-get_line(Chars, Pbs, Drv, Shell, Encoding) ->
- {more_chars,Cont,Rs} = edlin:start(Pbs),
+%% {done,LineChars,RestChars}
+%% interrupted
+get_line(Chars, Pbs, Cont, Drv, Shell, Encoding) ->
+ {more_chars,Cont1,Rs} = case Cont of
+ [] -> edlin:start(Pbs);
+ _ -> edlin:start(Pbs, Cont)
+ end,
send_drv_reqs(Drv, Rs),
- get_line1(edlin:edit_line(Chars, Cont), Drv, Shell, new_stack(get(line_buffer)),
- Encoding).
+ get_line1(edlin:edit_line(Chars, Cont1), Drv, Shell, new_stack(get(line_buffer)),
+ Encoding).
-get_line1({done,Line,Rest,Rs}, Drv, _Shell, _Ls, _Encoding) ->
+get_line1({done, Cont, Rest, Rs}, Drv, _Shell, _Ls, _Encoding) ->
send_drv_reqs(Drv, Rs),
- {done,Line,Rest};
+ {done, Cont, Rest};
get_line1({undefined,{_A, Mode, Char}, _Cs, Cont, Rs}, Drv, Shell, Ls0, Encoding)
- when ((Mode =:= none) and (Char =:= $\^O)) ->
+ when Mode =:= none, Char =:= $\^O;
+ Mode =:= meta, Char =:= $o ->
send_drv_reqs(Drv, Rs),
- Buffer = edlin:current_chars(Cont),
+ Buffer = edlin:current_line(Cont),
send_drv(Drv, {open_editor, Buffer}),
receive
{Drv, {editor_data, Cs}} ->
- send_drv_reqs(Drv, edlin:erase_line(Cont)),
+ send_drv_reqs(Drv, edlin:erase_line()),
{more_chars,NewCont,NewRs} = edlin:start(edlin:prompt(Cont)),
send_drv_reqs(Drv, NewRs),
get_line1(edlin:edit_line(Cs, NewCont), Drv, Shell, Ls0, Encoding)
end;
+%% Move Up, Down in History: Ctrl+P, Ctrl+N
get_line1({undefined,{_A,Mode,Char},Cs,Cont,Rs}, Drv, Shell, Ls0, Encoding)
- when ((Mode =:= none) and (Char =:= $\^P))
- or ((Mode =:= meta_left_sq_bracket) and (Char =:= $A)) ->
+ when Mode =:= none, Char =:= $\^P;
+ Mode =:= meta_left_sq_bracket, Char =:= $A ->
send_drv_reqs(Drv, Rs),
case up_stack(save_line(Ls0, edlin:current_line(Cont))) of
- {none,_Ls} ->
- send_drv(Drv, beep),
- get_line1(edlin:edit_line(Cs, Cont), Drv, Shell, Ls0, Encoding);
- {Lcs,Ls} ->
- send_drv_reqs(Drv, edlin:erase_line(Cont)),
- {more_chars,Ncont,Nrs} = edlin:start(edlin:prompt(Cont)),
- send_drv_reqs(Drv, Nrs),
- get_line1(edlin:edit_line1(lists:sublist(Lcs, 1, length(Lcs)-1),
- Ncont),
- Drv,
- Shell,
- Ls, Encoding)
+ {none,_Ls} ->
+ send_drv(Drv, beep),
+ get_line1(edlin:edit_line(Cs, Cont), Drv, Shell, Ls0, Encoding);
+ {Lcs,Ls} ->
+ send_drv_reqs(Drv, edlin:erase_line()),
+ {more_chars,Ncont,Nrs} = edlin:start(edlin:prompt(Cont)),
+ send_drv_reqs(Drv, Nrs),
+ get_line1(edlin:edit_line1(string:to_graphemes(lists:sublist(Lcs,
+ 1,
+ length(Lcs)-1)),
+ Ncont),
+ Drv,
+ Shell,
+ Ls, Encoding)
end;
get_line1({undefined,{_A,Mode,Char},Cs,Cont,Rs}, Drv, Shell, Ls0, Encoding)
- when ((Mode =:= none) and (Char =:= $\^N))
- or ((Mode =:= meta_left_sq_bracket) and (Char =:= $B)) ->
+ when Mode =:= none, Char =:= $\^N;
+ Mode =:= meta_left_sq_bracket, Char =:= $B ->
send_drv_reqs(Drv, Rs),
case down_stack(save_line(Ls0, edlin:current_line(Cont))) of
- {none,_Ls} ->
- send_drv(Drv, beep),
- get_line1(edlin:edit_line(Cs, Cont), Drv, Shell, Ls0, Encoding);
- {Lcs,Ls} ->
- send_drv_reqs(Drv, edlin:erase_line(Cont)),
- {more_chars,Ncont,Nrs} = edlin:start(edlin:prompt(Cont)),
- send_drv_reqs(Drv, Nrs),
- get_line1(edlin:edit_line1(lists:sublist(Lcs, 1, length(Lcs)-1),
- Ncont),
- Drv,
- Shell,
- Ls, Encoding)
+ {none,_Ls} ->
+ send_drv(Drv, beep),
+ get_line1(edlin:edit_line(Cs, Cont), Drv, Shell, Ls0, Encoding);
+ {Lcs,Ls} ->
+ send_drv_reqs(Drv, edlin:erase_line()),
+ {more_chars,Ncont,Nrs} = edlin:start(edlin:prompt(Cont)),
+ send_drv_reqs(Drv, Nrs),
+ get_line1(edlin:edit_line1(string:to_graphemes(lists:sublist(Lcs,
+ 1,
+ length(Lcs)-1)),
+ Ncont),
+ Drv,
+ Shell,
+ Ls, Encoding)
end;
%% ^R = backward search, ^S = forward search.
%% Search is tricky to implement and does a lot of back-and-forth
@@ -629,15 +643,13 @@ get_line1({undefined,{_A,Mode,Char},Cs,Cont,Rs}, Drv, Shell, Ls0, Encoding)
%% the regular ones (none, meta_left_sq_bracket) and handle special
%% cases of history search.
get_line1({undefined,{_A,Mode,Char},Cs,Cont,Rs}, Drv, Shell, Ls, Encoding)
- when ((Mode =:= none) and (Char =:= $\^R)) ->
+ when Mode =:= none, Char =:= $\^R ->
send_drv_reqs(Drv, Rs),
%% drop current line, move to search mode. We store the current
%% prompt ('N>') and substitute it with the search prompt.
- send_drv_reqs(Drv, edlin:erase_line(Cont)),
- put(search_quit_prompt, edlin:prompt(Cont)),
- Pbs = prompt_bytes("(search)`': ", Encoding),
- {more_chars,Ncont,Nrs} = edlin:start(Pbs, search),
- send_drv_reqs(Drv, Nrs),
+ put(search_quit_prompt, Cont),
+ Pbs = prompt_bytes("\033[;1;4msearch:\033[0m ", Encoding),
+ {more_chars,Ncont,_Nrs} = edlin:start(Pbs, search),
get_line1(edlin:edit_line1(Cs, Ncont), Drv, Shell, Ls, Encoding);
get_line1({Expand, Before, Cs0, Cont,Rs}, Drv, Shell, Ls0, Encoding)
when Expand =:= expand; Expand =:= expand_full ->
@@ -684,66 +696,98 @@ get_line1({Expand, Before, Cs0, Cont,Rs}, Drv, Shell, Ls0, Encoding)
_ ->
%% If there are more results than fit on
%% screen we expand above
- send_drv(Drv, {put_chars, unicode, NlMatchStr}),
+ send_drv_reqs(Drv, [{put_chars_keep_state, unicode, NlMatchStr},redraw_prompt]),
[$\e, $l | Cs1]
end
end;
false ->
- send_drv(Drv, {put_chars, unicode, NlMatchStr}),
+ send_drv(Drv, {put_chars_keep_state, unicode, NlMatchStr}),
[$\e, $l | Cs1]
end
end,
get_line1(edlin:edit_line(Cs, Cont), Drv, Shell, Ls0, Encoding);
+get_line1({undefined, {_, search_quit, _}, _Cs, _Cont={line, P, Line, none}, Rs}, Drv, Shell, Ls, Encoding) ->
+ get_line1({more_chars, {line, P, Line, search_quit}, Rs}, Drv, Shell, Ls, Encoding);
get_line1({undefined,_Char,Cs,Cont,Rs}, Drv, Shell, Ls, Encoding) ->
send_drv_reqs(Drv, Rs),
send_drv(Drv, beep),
get_line1(edlin:edit_line(Cs, Cont), Drv, Shell, Ls, Encoding);
%% The search item was found and accepted (new line entered on the exact
%% result found)
-get_line1({_What,Cont={line,_Prompt,_Chars,search_found},Rs}, Drv, Shell, Ls0, Encoding) ->
- Line = edlin:current_line(Cont),
- %% this may create duplicate entries.
- Ls = save_line(new_stack(get_lines(Ls0)), Line),
- get_line1({done, Line, "", Rs}, Drv, Shell, Ls, Encoding);
+get_line1({_What,{line,_,_Drv,search_found},Rs}, Drv, Shell, Ls0, Encoding) ->
+ SearchResult = get(search_result),
+ LineCont = case SearchResult of
+ [] -> {[],{[],[]},[]};
+ _ -> [Last| LB] = lists:reverse(SearchResult),
+ {LB, {lists:reverse(Last),[]},[]}
+ end,
+ Prompt = edlin:prompt(get(search_quit_prompt)),
+ send_drv_reqs(Drv, Rs),
+ send_drv_reqs(Drv, edlin:erase_line()),
+ send_drv_reqs(Drv, edlin:redraw_line({line, Prompt, LineCont, none})),
+ put(search_result, []),
+ %% TODO, do even need to save it, won't it be saved by handling {done...}?
+ Ls = save_line(new_stack(get_lines(Ls0)), edlin:current_line({line, edlin:prompt(get(search_quit_prompt)), LineCont, none})),
+ get_line1({done, LineCont, "\n", Rs}, Drv, Shell, Ls, Encoding);
%% The search mode has been exited, but the user wants to remain in line
%% editing mode wherever that was, but editing the search result.
-get_line1({What,Cont={line,_Prompt,_Chars,search_quit},Rs}, Drv, Shell, Ls, Encoding) ->
- Line = edlin:current_chars(Cont),
+get_line1({What,{line,_,_,search_quit},Rs}, Drv, Shell, Ls, Encoding) ->
%% Load back the old prompt with the correct line number.
- case get(search_quit_prompt) of
- undefined -> % should not happen. Fallback.
- LsFallback = save_line(new_stack(get_lines(Ls)), Line),
- get_line1({done, "\n", Line, Rs}, Drv, Shell, LsFallback, Encoding);
- Prompt -> % redraw the line and keep going with the same stack position
- NCont = {line,Prompt,{lists:reverse(Line),[]},none},
- send_drv_reqs(Drv, Rs),
- send_drv_reqs(Drv, edlin:erase_line(Cont)),
- send_drv_reqs(Drv, edlin:redraw_line(NCont)),
- get_line1({What, NCont ,[]}, Drv, Shell, pad_stack(Ls), Encoding)
+ case edlin:prompt(get(search_quit_prompt)) of
+ Prompt -> % redraw the line and keep going with the same stack position
+ SearchResult = get(search_result),
+ L = case SearchResult of
+ [] -> {[],{[],[]},[]};
+ _ -> [Last|LB] = lists:reverse(SearchResult),
+ {LB, {lists:reverse(Last), []}, []}
+ end,
+ NCont = {line,Prompt,L,none},
+ put(search_result, []),
+ send_drv_reqs(Drv, [delete_line|Rs]),
+ send_drv_reqs(Drv, edlin:redraw_line(NCont)),
+ get_line1({What, NCont ,[]}, Drv, Shell, pad_stack(Ls), Encoding)
end;
+get_line1({What,_Cont={line,_,_,search_cancel},Rs}, Drv, Shell, Ls, Encoding) ->
+ NCont = get(search_quit_prompt),
+ put(search_result, []),
+ send_drv_reqs(Drv, [delete_line|Rs]),
+ send_drv_reqs(Drv, edlin:redraw_line(NCont)),
+ get_line1({What, NCont, []}, Drv, Shell, Ls, Encoding);
%% Search mode is entered.
-get_line1({What,{line,Prompt,{RevCmd0,_Aft},search},Rs},
- Drv, Shell, Ls0, Encoding) ->
- send_drv_reqs(Drv, Rs),
+get_line1({What,{line,Prompt,{_,{RevCmd0,_},_},search},_Rs},
+ Drv, Shell, Ls0, Encoding) ->
%% Figure out search direction. ^S and ^R are returned through edlin
%% whenever we received a search while being already in search mode.
{Search, Ls1, RevCmd} = case RevCmd0 of
- [$\^S|RevCmd1] ->
- {fun search_down_stack/2, Ls0, RevCmd1};
- [$\^R|RevCmd1] ->
- {fun search_up_stack/2, Ls0, RevCmd1};
- _ -> % new search, rewind stack for a proper search.
- {fun search_up_stack/2, new_stack(get_lines(Ls0)), RevCmd0}
- end,
+ [$\^S|RevCmd1] ->
+ {fun search_down_stack/2, Ls0, RevCmd1};
+ [$\^R|RevCmd1] ->
+ {fun search_up_stack/2, Ls0, RevCmd1};
+ _ -> % new search, rewind stack for a proper search.
+ {fun search_up_stack/2, new_stack(get_lines(Ls0)), RevCmd0}
+ end,
Cmd = lists:reverse(RevCmd),
{Ls, NewStack} = case Search(Ls1, Cmd) of
- {none, Ls2} ->
- send_drv(Drv, beep),
- {Ls2, {RevCmd, "': "}};
- {Line, Ls2} -> % found. Complete the output edlin couldn't have done.
- send_drv_reqs(Drv, [{put_chars, Encoding, Line}]),
- {Ls2, {RevCmd, "': "++Line}}
- end,
+ {none, Ls2} ->
+ send_drv(Drv, beep),
+ put(search_result, []),
+ send_drv(Drv, delete_line),
+ send_drv(Drv, {put_chars, unicode, unicode:characters_to_binary(Prompt++Cmd)}),
+ {Ls2, {[],{RevCmd, []},[]}};
+ {Line, Ls2} -> % found. Complete the output edlin couldn't have done.
+ Lines = string:split(string:to_graphemes(Line), "\n", all),
+ Output = if length(Lines) > 5 ->
+ [A,B,C,D,E|_]=Lines,
+ (["\n " ++ Line1 || Line1 <- [A,B,C,D,E]] ++
+ [io_lib:format("~n ... (~w lines omitted)",[length(Lines)-5])]);
+ true -> ["\n " ++ Line1 || Line1 <- Lines]
+ end,
+ put(search_result, Lines),
+ send_drv(Drv, delete_line),
+ send_drv(Drv, {put_chars, unicode, unicode:characters_to_binary(Prompt++Cmd)}),
+ send_drv(Drv, {put_expand_no_trim, unicode, unicode:characters_to_binary(Output)}),
+ {Ls2, {[],{RevCmd, []},[]}}
+ end,
Cont = {line,Prompt,NewStack,search},
more_data(What, Cont, Drv, Shell, Ls, Encoding);
get_line1({What,Cont0,Rs}, Drv, Shell, Ls, Encoding) ->
@@ -752,29 +796,32 @@ get_line1({What,Cont0,Rs}, Drv, Shell, Ls, Encoding) ->
more_data(What, Cont0, Drv, Shell, Ls, Encoding) ->
receive
- {Drv,{data,Cs}} ->
- get_line1(edlin:edit_line(Cs, Cont0), Drv, Shell, Ls, Encoding);
- {Drv,eof} ->
- get_line1(edlin:edit_line(eof, Cont0), Drv, Shell, Ls, Encoding);
- {io_request,From,ReplyAs,Req} when is_pid(From) ->
- {more_chars,Cont,_More} = edlin:edit_line([], Cont0),
- send_drv_reqs(Drv, edlin:erase_line(Cont)),
- io_request(Req, From, ReplyAs, Drv, Shell, []), %WRONG!!!
- send_drv_reqs(Drv, edlin:redraw_line(Cont)),
- get_line1({more_chars,Cont,[]}, Drv, Shell, Ls, Encoding);
+ {Drv, activate} ->
+ send_drv_reqs(Drv, edlin:redraw_line(Cont0)),
+ more_data(What, Cont0, Drv, Shell, Ls, Encoding);
+ {Drv,{data,Cs}} ->
+ get_line1(edlin:edit_line(Cs, Cont0), Drv, Shell, Ls, Encoding);
+ {Drv,eof} ->
+ get_line1(edlin:edit_line(eof, Cont0), Drv, Shell, Ls, Encoding);
+ {io_request,From,ReplyAs,Req} when is_pid(From) ->
+ {more_chars,Cont,_More} = edlin:edit_line([], Cont0),
+ send_drv_reqs(Drv, edlin:erase_line()),
+ io_request(Req, From, ReplyAs, Drv, Shell, []), %WRONG!!!
+ send_drv_reqs(Drv, edlin:redraw_line(Cont)),
+ get_line1({more_chars,Cont,[]}, Drv, Shell, Ls, Encoding);
{reply,{From,ReplyAs},Reply} ->
%% We take care of replies from puts here as well
io_reply(From, ReplyAs, Reply),
more_data(What, Cont0, Drv, Shell, Ls, Encoding);
- {'EXIT',Drv,interrupt} ->
- interrupted;
- {'EXIT',Drv,_} ->
- terminated;
- {'EXIT',Shell,R} ->
- exit(R)
+ {'EXIT',Drv,interrupt} ->
+ interrupted;
+ {'EXIT',Drv,_} ->
+ terminated;
+ {'EXIT',Shell,R} ->
+ exit(R)
after
- get_line_timeout(What)->
- get_line1(edlin:edit_line([], Cont0), Drv, Shell, Ls, Encoding)
+ get_line_timeout(What)->
+ get_line1(edlin:edit_line([], Cont0), Drv, Shell, Ls, Encoding)
end.
get_line_echo_off(Chars, Pbs, Drv, Shell) ->
@@ -814,21 +861,21 @@ get_chars_echo_off1(Drv, Shell) ->
receive
{Drv, {data, Cs}} ->
Cs;
- {Drv, eof} ->
+ {Drv, eof} ->
eof;
- {io_request,From,ReplyAs,Req} when is_pid(From) ->
- io_request(Req, From, ReplyAs, Drv, Shell, []),
- get_chars_echo_off1(Drv, Shell);
+ {io_request,From,ReplyAs,Req} when is_pid(From) ->
+ io_request(Req, From, ReplyAs, Drv, Shell, []),
+ get_chars_echo_off1(Drv, Shell);
{reply,{From,ReplyAs},Reply} when From =/= undefined ->
%% We take care of replies from puts here as well
io_reply(From, ReplyAs, Reply),
get_chars_echo_off1(Drv, Shell);
- {'EXIT',Drv,interrupt} ->
- interrupted;
- {'EXIT',Drv,_} ->
- terminated;
- {'EXIT',Shell,R} ->
- exit(R)
+ {'EXIT',Drv,interrupt} ->
+ interrupted;
+ {'EXIT',Drv,_} ->
+ terminated;
+ {'EXIT',Shell,R} ->
+ exit(R)
end.
%% We support line editing for the ICANON mode except the following
@@ -909,8 +956,8 @@ get_all_lines({stack, U, {}, []}) ->
U;
get_all_lines({stack, U, {}, D}) ->
case lists:reverse(D, U) of
- ["\n"|Lines] -> Lines;
- Lines -> Lines
+ ["\n"|Lines] -> Lines;
+ Lines -> Lines
end;
get_all_lines({stack, U, L, D}) ->
get_all_lines({stack, U, {}, [L|D]}).
@@ -934,22 +981,22 @@ save_line_buffer(Lines) ->
search_up_stack(Stack, Substr) ->
case up_stack(Stack) of
- {none,NewStack} -> {none,NewStack};
- {L, NewStack} ->
+ {none,NewStack} -> {none,NewStack};
+ {L, NewStack} ->
case string:find(L, Substr) of
nomatch -> search_up_stack(NewStack, Substr);
_ -> {string:trim(L, trailing, "$\n"), NewStack}
- end
+ end
end.
search_down_stack(Stack, Substr) ->
case down_stack(Stack) of
- {none,NewStack} -> {none,NewStack};
- {L, NewStack} ->
- case string:find(L, Substr) of
- nomatch -> search_down_stack(NewStack, Substr);
- _ -> {string:trim(L, trailing, "$\n"), NewStack}
- end
+ {none,NewStack} -> {none,NewStack};
+ {L, NewStack} ->
+ case string:find(L, Substr) of
+ nomatch -> search_down_stack(NewStack, Substr);
+ _ -> {string:trim(L, trailing, "$\n"), NewStack}
+ end
end.
@@ -963,16 +1010,15 @@ get_password1({Chars,[]}, Drv, Shell) ->
{Drv,{data,Cs}} ->
get_password1(edit_password(Cs,Chars),Drv,Shell);
{io_request,From,ReplyAs,Req} when is_pid(From) ->
- %send_drv_reqs(Drv, [{delete_chars, -length(Pbs)}]),
io_request(Req, From, ReplyAs, Drv, Shell, []), %WRONG!!!
%% I guess the reason the above line is wrong is that Buf is
%% set to []. But do we expect anything but plain output?
- get_password1({Chars, []}, Drv, Shell);
+ get_password1({Chars, []}, Drv, Shell);
{reply,{From,ReplyAs},Reply} ->
%% We take care of replies from puts here as well
io_reply(From, ReplyAs, Reply),
- get_password1({Chars, []},Drv, Shell);
+ get_password1({Chars, []}, Drv, Shell);
{'EXIT',Drv,interrupt} ->
interrupted;
{'EXIT',Drv,_} ->
diff --git a/lib/kernel/src/prim_tty.erl b/lib/kernel/src/prim_tty.erl
index 4ef10c752d..a03e746cc9 100644
--- a/lib/kernel/src/prim_tty.erl
+++ b/lib/kernel/src/prim_tty.erl
@@ -136,6 +136,8 @@
writer,
options,
unicode,
+ lines_before = [], %% All lines before the current line in reverse order
+ lines_after = [], %% All lines after the current line.
buffer_before = [], %% Current line before cursor in reverse
buffer_after = [], %% Current line after cursor not in reverse
buffer_expand, %% Characters in expand buffer
@@ -166,10 +168,20 @@
-type request() ::
{putc_raw, binary()} |
{putc, unicode:unicode_binary()} |
+ {putc_keep_state, unicode:unicode_binary()} |
{expand, unicode:unicode_binary()} |
+ {expand_with_trim, unicode:unicode_binary()} |
{insert, unicode:unicode_binary()} |
{delete, integer()} |
+ delete_after_cursor |
+ delete_line |
+ redraw_prompt |
+ {redraw_prompt, string(), string(), tuple()} |
+ redraw_prompt_pre_deleted |
+ new_prompt |
{move, integer()} |
+ {move_line, integer()} |
+ {move_combo, integer(), integer(), integer()} |
clear |
beep.
-opaque state() :: #state{}.
@@ -276,9 +288,9 @@ init(State, {unix,_}) ->
%% See https://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html#SEC23
%% for a list of all possible termcap capabilities
Clear = case tgetstr("clear") of
- {ok, C} -> C;
- false -> (#state{})#state.clear
- end,
+ {ok, C} -> C;
+ false -> (#state{})#state.clear
+ end,
Cols = case tgetnum("co") of
{ok, Cs} -> Cs;
_ -> (#state{})#state.cols
@@ -321,7 +333,7 @@ init(State, {unix,_}) ->
{ok, <<"\e[6n">> = U7} ->
%% User 7 should contain the codes for getting
%% cursor position.
- % User 6 should contain how to parse the reply
+ %% User 6 should contain how to parse the reply
{ok, <<"\e[%i%d;%dR">>} = tgetstr("u6"),
<<"\e[6n">> = U7;
false -> (#state{})#state.position
@@ -535,26 +547,62 @@ handle_request(State = #state{ options = #{ tty := false } }, Request) ->
_Ignore ->
{<<>>, State}
end;
+handle_request(State, {redraw_prompt, Pbs, Pbs2, {LB, {Bef, Aft}, LA}}) ->
+ {ClearLine, Cleared} = handle_request(State, delete_line),
+ 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, -logical(Last), length(LA), logical(Bef)};
+ true ->
+ {move, -logical(Aft)}
+ end,
+ {_, InsertedText} = handle_request(Cleared, {insert, unicode:characters_to_binary(Text)}),
+ {_, Moved} = handle_request(InsertedText, Moves),
+ {Redraw, NewState} = handle_request(Moved, redraw_prompt_pre_deleted),
+ {[ClearLine, Redraw], NewState};
+handle_request(State, redraw_prompt) ->
+ {ClearLine, _} = handle_request(State, delete_line),
+ {Redraw, NewState} = handle_request(State, redraw_prompt_pre_deleted),
+ {[ClearLine, Redraw], NewState};
+handle_request(State = #state{unicode = U, cols = W}, redraw_prompt_pre_deleted) ->
+ {Movement, TextInView} = in_view(State),
+ {_, NewPrompt} = handle_request(State, new_prompt),
+ {Redraw, RedrawState} = insert_buf(NewPrompt#state{xn = false}, unicode:characters_to_binary(TextInView)),
+ {Output, _} = case State#state.buffer_expand of
+ undefined ->
+ {[encode(Redraw, U), xnfix(RedrawState, RedrawState#state.buffer_before), Movement], RedrawState};
+ BufferExpand ->
+ BBCols = cols(State#state.buffer_before, U),
+ End = BBCols + cols(State#state.buffer_after,U),
+ {ExpandBuffer, NewState} = insert_buf(RedrawState#state{ buffer_expand = [] }, iolist_to_binary(BufferExpand)),
+ BECols = cols(W, End, NewState#state.buffer_expand, U),
+ MoveToEnd = move_cursor(RedrawState, BECols, End),
+ {[encode(Redraw,U),encode(ExpandBuffer, U), MoveToEnd, Movement], RedrawState}
+
+ end,
+ {Output, State};
%% Clear the expand buffer after the cursor when we handle any request.
-handle_request(State = #state{ buffer_expand = Expand, unicode = U }, Request)
+handle_request(State = #state{ buffer_expand = Expand, unicode = U}, Request)
when Expand =/= undefined ->
- BBCols = cols(State#state.buffer_before, U),
- BACols = cols(State#state.buffer_after, U),
- ClearExpand = [move_cursor(State, BBCols, BBCols + BACols),
- State#state.delete_after_cursor,
- move_cursor(State, BBCols + BACols, BBCols)],
- {Output, NewState} = handle_request(State#state{ buffer_expand = undefined }, Request),
- {[ClearExpand, encode(Output, U)], NewState};
+ {Redraw, NoExpandState} = handle_request(State#state{ buffer_expand = undefined }, redraw_prompt),
+ {Output, NewState} = handle_request(NoExpandState#state{ buffer_expand = undefined }, Request),
+ {[encode(Redraw, U), encode(Output, U)], NewState};
+handle_request(State, new_prompt) ->
+ {"", State#state{buffer_before = [],
+ buffer_after = [],
+ lines_before = [],
+ lines_after = []}};
%% Print characters in the expandbuffer after the cursor
-handle_request(State = #state{ unicode = U }, {expand, Binary}) ->
- BBCols = cols(State#state.buffer_before, U),
- BACols = cols(State#state.buffer_after, U),
- Expand = iolist_to_binary(["\r\n",string:trim(Binary, both)]),
- MoveToEnd = move_cursor(State, BBCols, BBCols + BACols),
- {ExpandBuffer, NewState} = insert_buf(State#state{ buffer_expand = [] }, Expand),
- BECols = cols(NewState#state.cols, BBCols + BACols, NewState#state.buffer_expand, U),
- MoveToOrig = move_cursor(State, BECols, BBCols),
- {[MoveToEnd, encode(ExpandBuffer, U), MoveToOrig], NewState};
+handle_request(State, {expand, Expand}) ->
+ handle_request(State#state{buffer_expand = Expand}, redraw_prompt);
+handle_request(State, {expand_with_trim, Binary}) ->
+ handle_request(State,
+ {expand, iolist_to_binary(["\r\n",string:trim(Binary, both)])});
+%% putc_keep_state prints Binary and keeps the current prompt unchanged
+handle_request(State = #state{ unicode = U }, {putc_keep_state, Binary}) ->
+ {PutBuffer, _NewState} = insert_buf(State, Binary),
+ {encode(PutBuffer, U), State};
%% putc prints Binary and overwrites any existing characters
handle_request(State = #state{ unicode = U }, {putc, Binary}) ->
%% Todo should handle invalid unicode?
@@ -563,36 +611,136 @@ handle_request(State = #state{ unicode = U }, {putc, Binary}) ->
{encode(PutBuffer, U), NewState};
true ->
%% Delete any overwritten characters after current the cursor
- OldLength = logical(State#state.buffer_before),
- NewLength = logical(NewState#state.buffer_before),
+ OldLength = logical(State#state.buffer_before) + lists:sum([logical(L) || L <- State#state.lines_before]),
+ NewLength = logical(NewState#state.buffer_before) + lists:sum([logical(L) || L <- NewState#state.lines_before]),
{_, _, _, NewBA} = split(NewLength - OldLength, NewState#state.buffer_after, U),
{encode(PutBuffer, U), NewState#state{ buffer_after = NewBA }}
end;
handle_request(State, {putc_raw, Binary}) ->
handle_request(State, {putc, unicode:characters_to_binary(Binary, latin1)});
-handle_request(State = #state{ unicode = U }, {delete, N}) when N > 0 ->
+handle_request(State = #state{}, delete_after_cursor) ->
+ {[State#state.delete_after_cursor],
+ State#state{buffer_after = [],
+ lines_after = []}};
+handle_request(State = #state{unicode = U, cols = W, buffer_before = Bef,
+ lines_before = LinesBefore,
+ lines_after = _LinesAfter}, delete_line) ->
+ MoveToBeg = move_cursor(State, cols_multiline(Bef, LinesBefore, W, U), 0),
+ {[MoveToBeg, State#state.delete_after_cursor],
+ State#state{buffer_before = [],
+ buffer_after = [],
+ lines_before = [],
+ lines_after = []}};
+handle_request(State = #state{ unicode = U, cols = W }, {delete, N}) when N > 0 ->
{_DelNum, DelCols, _, NewBA} = split(N, State#state.buffer_after, U),
BBCols = cols(State#state.buffer_before, U),
+ BACols = cols(State#state.buffer_after, U),
NewBACols = cols(NewBA, U),
- {[encode(NewBA, U),
- lists:duplicate(DelCols, $\s),
- xnfix(State, BBCols + NewBACols + DelCols),
- move_cursor(State,
- BBCols + NewBACols + DelCols,
- BBCols)],
- State#state{ buffer_after = NewBA }};
-handle_request(State = #state{ unicode = U }, {delete, N}) when N < 0 ->
+ Output = [encode(NewBA, U),
+ lists:duplicate(DelCols, $\s),
+ xnfix(State, BBCols + NewBACols + DelCols),
+ move_cursor(State,
+ BBCols + NewBACols + DelCols,
+ BBCols)],
+ NewState0 = State#state{ buffer_after = NewBA },
+ if State#state.lines_after =/= [], (BBCols + BACols-N) rem W =:= 0 ->
+ {Delete, _} = handle_request(State, delete_line),
+ {Redraw, NewState1} = handle_request(NewState0, redraw_prompt_pre_deleted),
+ {[Delete, Redraw], NewState1};
+ true ->
+ {Output, NewState0}
+ end;
+handle_request(State = #state{ unicode = U, cols = W }, {delete, N}) when N < 0 ->
{_DelNum, DelCols, _, NewBB} = split(-N, State#state.buffer_before, U),
- NewBBCols = cols(NewBB, U),
+ BBCols = cols(State#state.buffer_before, U),
BACols = cols(State#state.buffer_after, U),
- {[move_cursor(State, NewBBCols + DelCols, NewBBCols),
- encode(State#state.buffer_after,U),
- lists:duplicate(DelCols, $\s),
- xnfix(State, NewBBCols + BACols + DelCols),
- move_cursor(State, NewBBCols + BACols + DelCols, NewBBCols)],
- State#state{ buffer_before = NewBB } };
+ NewBBCols = cols(NewBB, U),
+ Output = [move_cursor(State, NewBBCols + DelCols, NewBBCols),
+ encode(State#state.buffer_after,U),
+ lists:duplicate(DelCols, $\s),
+ xnfix(State, NewBBCols + BACols + DelCols),
+ move_cursor(State, NewBBCols + BACols + DelCols, NewBBCols)],
+ NewState0 = State#state{ buffer_before = NewBB },
+ if State#state.lines_after =/= [], (BBCols+BACols+N) rem W =:= 0 ->
+ {Delete, _} = handle_request(State, delete_line),
+ {Redraw, NewState1} = handle_request(NewState0, redraw_prompt_pre_deleted),
+ {[Delete, Redraw], NewState1};
+ true ->
+ {Output, NewState0}
+ end;
handle_request(State, {delete, 0}) ->
{"",State};
+%% {move_combo, before_line_movement, line_movement, after_line_movement}
+%% Many of the move operations comes in threes, this is a helper to make
+%% movement a little bit easier. We move to the beginning of
+%% the line before switching line and then move to the right column on
+%% the next line.
+handle_request(State, {move_combo, V1, L, V2}) ->
+ {Moves1, NewState1} = handle_request(State, {move, V1}),
+ {Moves2, NewState2} = handle_request(NewState1, {move_line, L}),
+ {Moves3, NewState3} = handle_request(NewState2, {move, V2}),
+ {Moves1 ++ Moves2 ++ Moves3, NewState3};
+handle_request(State = #state{ cols = W,
+ rows = R,
+ unicode = U,
+ buffer_before = Bef,
+ buffer_after = Aft,
+ lines_before = LinesBefore,
+ lines_after = LinesAfter},
+ {move_line, L}) when L < 0, length(LinesBefore) >= -L ->
+ {LinesJumped, [B|NewLinesBefore]} = lists:split(-L -1, LinesBefore),
+ PrevLinesCols = cols_multiline([B|LinesJumped], W, U),
+ N_Cols = min(cols(Bef, U), cols(B, U)),
+ {_, _, NewBB, NewBA} = split_cols(N_Cols, B, U),
+ Moves = move_cursor(State, PrevLinesCols, 0),
+ CL = lists:reverse(Bef,Aft),
+ NewLinesAfter = lists:reverse([CL|LinesJumped], LinesAfter),
+ NewState = State#state{buffer_before = NewBB,
+ buffer_after = NewBA,
+ lines_before = NewLinesBefore,
+ lines_after = NewLinesAfter},
+ RowsInView = cols_multiline([B,CL|LinesBefore], W, U) div W,
+ Output = if
+ %% When we move up and the view is "full"
+ RowsInView >= R ->
+ {Movement, TextInView} = in_view(NewState),
+ {ClearLine, Cleared} = handle_request(State, delete_line),
+ {Redraw, _} = handle_request(Cleared, {insert, unicode:characters_to_binary(TextInView)}),
+ [ClearLine, Redraw, Movement];
+ true -> Moves
+ end,
+ {Output, NewState};
+handle_request(State = #state{ cols = W,
+ rows = R,
+ unicode = U,
+ buffer_before = Bef,
+ buffer_after = Aft,
+ lines_before = LinesBefore,
+ lines_after = LinesAfter},
+ {move_line, L}) when L > 0, length(LinesAfter) >= L ->
+ {LinesJumped, [A|NewLinesAfter]} = lists:split(L - 1, LinesAfter),
+ NextLinesCols = cols_multiline([(Bef++Aft)|LinesJumped], W, U),
+ N_Cols = min(cols(Bef, U), cols(A, U)),
+ {_, _, NewBB, NewBA} = split_cols(N_Cols, A, U),
+ Moves = move_cursor(State, 0, NextLinesCols),
+ CL = lists:reverse(Bef, Aft),
+ NewLinesBefore = lists:reverse([CL|LinesJumped],LinesBefore),
+ NewState = State#state{buffer_before = NewBB,
+ buffer_after = NewBA,
+ lines_before = NewLinesBefore,
+ lines_after = NewLinesAfter},
+ RowsInView = cols_multiline([A|NewLinesBefore], W, U) div W,
+ Output = if
+ RowsInView >= R ->
+ {Movement, TextInView} = in_view(NewState),
+ {ClearLine, Cleared} = handle_request(State, delete_line),
+ {Redraw, _} = handle_request(Cleared, {insert, unicode:characters_to_binary(TextInView)}),
+ [ClearLine, Redraw, Movement];
+ true -> Moves
+ end,
+ {Output, NewState};
+handle_request(State, {move_line, _}) ->
+ {"", State};
handle_request(State = #state{ unicode = U }, {move, N}) when N < 0 ->
{_DelNum, DelCols, NewBA, NewBB} = split(-N, State#state.buffer_before, U),
NewBBCols = cols(NewBB, U),
@@ -602,29 +750,57 @@ handle_request(State = #state{ unicode = U }, {move, N}) when N < 0 ->
handle_request(State = #state{ unicode = U }, {move, N}) when N > 0 ->
{_DelNum, DelCols, NewBB, NewBA} = split(N, State#state.buffer_after, U),
BBCols = cols(State#state.buffer_before, U),
- {move_cursor(State, BBCols, BBCols + DelCols),
- State#state{ buffer_after = NewBA,
- buffer_before = NewBB ++ State#state.buffer_before} };
+ Moves = move_cursor(State, BBCols, BBCols + DelCols),
+ {Moves, State#state{ buffer_after = NewBA,
+ buffer_before = NewBB ++ State#state.buffer_before} };
handle_request(State, {move, 0}) ->
{"",State};
-handle_request(State = #state{ xn = OrigXn, unicode = U }, {insert, Chars}) ->
+handle_request(State = #state{cols = W, xn = OrigXn, unicode = U,lines_after = LinesAfter}, {insert, Chars}) ->
{InsertBuffer, NewState0} = insert_buf(State#state{ xn = false }, Chars),
- NewState = NewState0#state{ xn = OrigXn },
- BBCols = cols(NewState#state.buffer_before, U),
- BACols = cols(NewState#state.buffer_after, U),
- {[ encode(InsertBuffer, U),
- encode(NewState#state.buffer_after, U),
- xnfix(State, BBCols + BACols),
- move_cursor(State, BBCols + BACols, BBCols) ],
- NewState};
+ NewState1 = NewState0#state{ xn = OrigXn },
+ NewBBCols = cols(NewState1#state.buffer_before, U),
+ NewBACols = cols(NewState1#state.buffer_after, U),
+ Output = [ encode(InsertBuffer, U),
+ encode(NewState1#state.buffer_after, U),
+ xnfix(State, NewBBCols + NewBACols),
+ move_cursor(State, NewBBCols + NewBACols, NewBBCols) ],
+ if LinesAfter =:= []; (NewBBCols + NewBACols) rem W =:= 0 ->
+ {Output, NewState1};
+ true ->
+ {Delete, _} = handle_request(State, delete_line),
+ {Redraw, NewState2} = handle_request(NewState1, redraw_prompt_pre_deleted),
+ {[Delete, Redraw,""], NewState2}
+ end;
handle_request(State, beep) ->
{<<7>>, State};
handle_request(State, clear) ->
- {State#state.clear, State};
+ {State#state.clear, State#state{buffer_before = [],
+ buffer_after = [],
+ lines_before = [],
+ lines_after = []}};
handle_request(State, Req) ->
erlang:display({unhandled_request, Req}),
{"", State}.
+%% Split the buffer after N cols
+%% Returns the number of characters deleted, and the column length (N)
+%% of those characters.
+split_cols(N_Cols, Buff, Unicode) ->
+ split_cols(N_Cols, Buff, [], 0, 0, Unicode).
+split_cols(N, [SkipChars | T], Acc, Cnt, Cols, Unicode) when is_binary(SkipChars) ->
+ split_cols(N, T, [SkipChars | Acc], Cnt, Cols, Unicode);
+split_cols(0, Buff, Acc, Chars, Cols, _Unicode) ->
+ {Chars, Cols, Acc, Buff};
+split_cols(N, _Buff, _Acc, _Chars, _Cols, _Unicode) when N < 0 ->
+ error;
+split_cols(_N, [], Acc, Chars, Cols, _Unicode) ->
+ {Chars, Cols, Acc, []};
+split_cols(N, [Char | T], Acc, Cnt, Cols, Unicode) when is_integer(Char) ->
+ split_cols(N - npwcwidth(Char), T, [Char | Acc], Cnt + 1, Cols + npwcwidth(Char, Unicode), Unicode);
+split_cols(N, [Chars | T], Acc, Cnt, Cols, Unicode) when is_list(Chars) ->
+ split_cols(N - length(Chars), T, [Chars | Acc],
+ Cnt + length(Chars), Cols + cols(Chars, Unicode), Unicode).
+
%% Split the buffer after N logical characters returning
%% the number of real characters deleted and the column length
%% of those characters
@@ -635,7 +811,7 @@ split(0, Buff, Acc, Chars, Cols, _Unicode) ->
?dbg({?FUNCTION_NAME, {Chars, Cols, Acc, Buff}}),
{Chars, Cols, Acc, Buff};
split(N, _Buff, _Acc, _Chars, _Cols, _Unicode) when N < 0 ->
- ok = N;
+ error;
split(_N, [], Acc, Chars, Cols, _Unicode) ->
{Chars, Cols, Acc, []};
split(N, [Char | T], Acc, Cnt, Cols, Unicode) when is_integer(Char) ->
@@ -685,6 +861,77 @@ move(left, #state{ left = Left }, N) ->
move(right, #state{ right = Right }, N) ->
lists:duplicate(N, Right).
+in_view(#state{lines_after = LinesAfter, buffer_before = Bef, buffer_after = Aft, lines_before = LinesBefore, rows=R, cols=W, unicode=U, buffer_expand = BufferExpand} = State) ->
+ BufferExpandLines = case BufferExpand of
+ undefined -> [];
+ _ -> string:split(erlang:binary_to_list(BufferExpand), "\r\n", all)
+ end,
+ ExpandRows = (cols_multiline(BufferExpandLines, W, U) div W),
+ InputBeforeRows = (cols_multiline(LinesBefore, W, U) div W),
+ InputRows = (cols_multiline([Bef ++ Aft], W, U) div W),
+ InputAfterRows = (cols_multiline(LinesAfter, W, U) div W),
+ %% Dont print lines after if we have expansion rows
+ SumRows = InputBeforeRows+ InputRows + ExpandRows + InputAfterRows,
+ if SumRows > R ->
+ RowsLeftAfterInputRows = R - InputRows,
+ RowsLeftAfterExpandRows = RowsLeftAfterInputRows - ExpandRows,
+ RowsLeftAfterInputBeforeRows = RowsLeftAfterExpandRows - InputBeforeRows,
+ Cols1 = max(0,W*max(RowsLeftAfterInputBeforeRows, RowsLeftAfterExpandRows)),
+ {_, LBAfter, _, {_, LBAHalf}} = split_cols_multiline(Cols1, LinesBefore, U, W),
+ LBAfter0 = case LBAHalf of [] -> LBAfter;
+ _ -> [LBAHalf|LBAfter]
+ end,
+
+ RowsLeftAfterInputAfterRows = RowsLeftAfterInputBeforeRows - InputAfterRows,
+ LAInViewLines = case BufferExpandLines of
+ [] ->
+ %% We must remove one line extra, since we may have an xnfix at the end which will
+ %% adds one extra line, so for consistency always remove one line
+ Cols2 = max(0,W*max(RowsLeftAfterInputAfterRows, RowsLeftAfterInputBeforeRows)-W),
+ {_, LABefore, _, {LABHalf, _}} = split_cols_multiline(Cols2, LinesAfter, U, W),
+ case LABHalf of [] -> LABefore;
+ _ -> [LABHalf|LABefore]
+ end;
+ _ ->
+ []
+ end,
+ LAInView = lists:flatten(["\n"++LA||LA<-lists:reverse(LAInViewLines)]),
+ LBInView = lists:flatten([LB++"\n"||LB<-LBAfter0]),
+ Text = LBInView ++ lists:reverse(Bef,Aft) ++ LAInView,
+ Movement = move_cursor(State,
+ cols_after_cursor(State#state{lines_after = LAInViewLines++[lists:reverse(Bef, Aft)]}),
+ cols(Bef,U)),
+ {Movement, Text};
+
+ true ->
+ %% Everything fits in the current window, just output everything
+ Movement = move_cursor(State, cols_after_cursor(State#state{lines_after = lists:reverse(LinesAfter)++[lists:reverse(Bef, Aft)]}), cols(Bef,U)),
+ Text = lists:flatten([LB++"\n"||LB<-lists:reverse(LinesBefore)]) ++
+ lists:reverse(Bef,Aft) ++ lists:flatten(["\n"++LA||LA<-LinesAfter]),
+ {Movement, Text}
+ end.
+cols_after_cursor(#state{lines_after=[LAST|LinesAfter],cols=W, unicode=U}) ->
+ cols_multiline(LAST, LinesAfter, W, U).
+split_cols_multiline(Cols, Lines, U, W) ->
+ split_cols_multiline(Cols, Lines, U, W, 0, []).
+split_cols_multiline(0, Lines, _U, _W, ColsAcc, AccBefore) ->
+ {ColsAcc, AccBefore, Lines, {[],[]}};
+split_cols_multiline(_Cols, [], _U, _W, ColsAcc, AccBefore) ->
+ {ColsAcc, AccBefore, [], {[],[]}};
+split_cols_multiline(Cols, [L|Lines], U, W, ColsAcc, AccBefore) ->
+ case cols(L, U) > Cols of
+ true ->
+ {_, _, LB, LA} = split_cols(Cols, L, U),
+ {ColsAcc+Cols, AccBefore, Lines, {lists:reverse(LB), LA}};
+ _ ->
+ Cols2 = (((cols(L,U)-1) div W)*W+W),
+ split_cols_multiline(Cols-Cols2, Lines, U, W, ColsAcc+Cols2, [L|AccBefore])
+ end.
+cols_multiline(Lines, W, U) ->
+ cols_multiline("", Lines, W, U).
+cols_multiline(ExtraCols, Lines, W, U) ->
+ cols(ExtraCols, U) + lists:sum([((cols(LB,U)-1) div W)*W + W || LB <- Lines]).
+
cols([],_Unicode) ->
0;
cols([Char | T], Unicode) when is_integer(Char) ->
@@ -728,6 +975,7 @@ npwcwidthstring(String) ->
_ ->
npwcwidth($\e) + npwcwidthstring(Rest)
end;
+ [H|Rest] when is_list(H)-> lists:sum([npwcwidth(A)||A<-H]) + npwcwidthstring(Rest);
[H|Rest] -> npwcwidth(H) + npwcwidthstring(Rest)
end.
@@ -779,7 +1027,7 @@ characters_to_output(Chars) ->
(Char) ->
Char
end, Chars)
- )
+ )
end.
characters_to_buffer(Chars) ->
lists:flatmap(
@@ -815,12 +1063,12 @@ insert_buf(State, Bin, LineAcc, Acc) ->
insert_buf(State, AnsiRest, [{ansi, Ansi} | LineAcc], Acc);
_ ->
insert_buf(State, Rest, [$\e | LineAcc], Acc)
- end;
+ end;
{Ansi, AnsiRest} ->
%% We include the graphics ansi sequences in the
%% buffer that we step over
insert_buf(State, AnsiRest, [Ansi | LineAcc], Acc)
- end;
+ end;
[NLCR | Rest] when NLCR =:= $\n; NLCR =:= $\r ->
Tail =
if NLCR =:= $\n ->
@@ -829,10 +1077,22 @@ insert_buf(State, Bin, LineAcc, Acc) ->
<<$\r>>
end,
if State#state.buffer_expand =:= undefined ->
- insert_buf(State#state{ buffer_before = [], buffer_after = [] }, Rest, [],
- [Acc, [characters_to_output(lists:reverse(LineAcc)), Tail]]);
- true ->
- insert_buf(State, Rest, [binary_to_list(Tail) | LineAcc], Acc)
+ CurrentLine = lists:reverse(State#state.buffer_before),
+ LinesBefore = State#state.lines_before,
+ LinesBefore1 =
+ case {CurrentLine, LineAcc} of
+ {[], []} ->
+ LinesBefore;
+ {[],_} ->
+ [lists:reverse(LineAcc)|LinesBefore];
+ {_,_} ->
+ [CurrentLine++lists:reverse(LineAcc)|LinesBefore]
+ end,
+ insert_buf(State#state{ buffer_before = [],
+ buffer_after = State#state.buffer_after,
+ lines_before=LinesBefore1},
+ Rest, [], [Acc, [characters_to_output(lists:reverse(LineAcc)), Tail]]);
+ true -> insert_buf(State, Rest, [binary_to_list(Tail) | LineAcc], Acc)
end;
[Cluster | Rest] when is_list(Cluster) ->
insert_buf(State, Rest, [Cluster | LineAcc], Acc);
diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl
index fc1a4434fb..25ebcbdd68 100644
--- a/lib/kernel/src/user_drv.erl
+++ b/lib/kernel/src/user_drv.erl
@@ -52,14 +52,25 @@
%% Same as put_chars/3, but sends Reply to From when the characters are
%% guaranteed to have been written to the terminal
{put_chars_sync, unicode, binary(), {From :: pid(), Reply :: term()}} |
+ %% Put text in expansion area
+ {put_expand} |
+ {put_expand_no_trim} |
%% Move the cursor X characters left or right (negative is left)
{move_rel, -32768..32767} |
+ %% Move the cursor Y rows up or down (negative is up)
+ {move_line, -32768..32767} |
+ %% Move combo, helper to simplify some move operations
+ {move_combo, -32768..32767, -32768..32767, -32768..32767} |
%% Insert characters at current cursor position moving any
%% characters after the cursor.
{insert_chars, unicode, binary()} |
%% Delete X chars before or after the cursor adjusting any test remaining
%% to the right of the cursor.
{delete_chars, -32768..32767} |
+ %% Deletes the current prompt and expression
+ delete_line |
+ %% Delete after the cursor
+ delete_after_cursor |
%% Trigger a terminal "bell"
beep |
%% Clears the screen
@@ -67,7 +78,12 @@
%% Execute multiple request() actions
{requests, [request()]} |
%% Open external editor
- {open_editor, string()}.
+ {open_editor, string()} |
+ %% Redraws the current prompt and expression
+ redraw_prompt |
+ {redraw_prompt, string(), string(), tuple()} |
+ %% Clears the state, not touching the characters
+ new_prompt.
-export_type([message/0]).
-export([start/0, start/1, start_shell/0, start_shell/1, whereis_group/0]).
@@ -298,7 +314,6 @@ init_remote_shell(State, Node, {M, F, A}) ->
end.
init_local_shell(State, InitialShell) ->
-
Slogan =
case application:get_env(
stdlib, shell_slogan,
@@ -595,8 +610,9 @@ switch_loop(internal, {line, Line}, State) ->
{ok, Groups} ->
Curr = gr_cur_pid(Groups),
put(current_group, Curr),
+ Curr ! {self(), activate},
{next_state, server,
- State#state{ current_group = Curr, groups = Groups } };
+ State#state{ current_group = Curr, groups = Groups }};
{retry, Requests} ->
{keep_state, State#state{ tty = io_requests(Requests, State#state.tty) },
{next_event, internal, line}};
@@ -617,7 +633,7 @@ switch_loop(internal, {line, Line}, State) ->
end;
switch_loop(info,{ReadHandle,{data,Cs}}, {Cont, #state{ read = ReadHandle } = State}) ->
case edlin:edit_line(unicode:characters_to_list(Cs), Cont) of
- {done,Line,_Rest, Rs} ->
+ {done,{[Line],_,_},_Rest, Rs} ->
{keep_state, State#state{ tty = io_requests(Rs, State#state.tty) },
{next_event, internal, {line, Line}}};
{undefined,_Char,MoreCs,NewCont,Rs} ->
@@ -759,6 +775,18 @@ group_opts() ->
{term(), reference(), prim_tty:state()}.
io_request({requests,Rs}, TTY) ->
{noreply, io_requests(Rs, TTY)};
+io_request(redraw_prompt, TTY) ->
+ write(prim_tty:handle_request(TTY, redraw_prompt));
+io_request({redraw_prompt, Pbs, Pbs2, LineState}, TTY) ->
+ write(prim_tty:handle_request(TTY, {redraw_prompt, Pbs, Pbs2, LineState}));
+io_request(new_prompt, TTY) ->
+ write(prim_tty:handle_request(TTY, new_prompt));
+io_request(delete_after_cursor, TTY) ->
+ write(prim_tty:handle_request(TTY, delete_after_cursor));
+io_request(delete_line, TTY) ->
+ write(prim_tty:handle_request(TTY, delete_line));
+io_request({put_chars_keep_state, unicode, Chars}, TTY) ->
+ write(prim_tty:handle_request(TTY, {putc_keep_state, unicode:characters_to_binary(Chars)}));
io_request({put_chars, unicode, Chars}, TTY) ->
write(prim_tty:handle_request(TTY, {putc, unicode:characters_to_binary(Chars)}));
io_request({put_chars_sync, unicode, Chars, Reply}, TTY) ->
@@ -770,9 +798,15 @@ io_request({put_chars_sync, latin1, Chars, Reply}, TTY) ->
{ok, MonitorRef} = prim_tty:write(NewTTY, Output, self()),
{Reply, MonitorRef, NewTTY};
io_request({put_expand, unicode, Chars}, TTY) ->
+ write(prim_tty:handle_request(TTY, {expand_with_trim, unicode:characters_to_binary(Chars)}));
+io_request({put_expand_no_trim, unicode, Chars}, TTY) ->
write(prim_tty:handle_request(TTY, {expand, unicode:characters_to_binary(Chars)}));
io_request({move_rel, N}, TTY) ->
write(prim_tty:handle_request(TTY, {move, N}));
+io_request({move_line, R}, TTY) ->
+ write(prim_tty:handle_request(TTY, {move_line, R}));
+io_request({move_combo, V1, R, V2}, TTY) ->
+ write(prim_tty:handle_request(TTY, {move_combo, V1, R, V2}));
io_request({insert_chars, unicode, Chars}, TTY) ->
write(prim_tty:handle_request(TTY, {insert, unicode:characters_to_binary(Chars)}));
io_request({delete_chars, N}, TTY) ->
@@ -790,6 +824,12 @@ io_requests([{insert_chars, unicode, C1},{insert_chars, unicode, C2}|Rs], TTY) -
io_requests([{insert_chars, unicode, [C1,C2]}|Rs], TTY);
io_requests([{put_chars, unicode, C1},{put_chars, unicode, C2}|Rs], TTY) ->
io_requests([{put_chars, unicode, [C1,C2]}|Rs], TTY);
+io_requests([{move_rel, N}, {move_line, R}, {move_rel, M}|Rs], TTY) ->
+ io_requests([{move_combo, N, R, M}|Rs], TTY);
+io_requests([{move_rel, N}, {move_line, R}|Rs], TTY) ->
+ io_requests([{move_combo, N, R, 0}|Rs], TTY);
+io_requests([{move_line, R}, {move_rel, M}|Rs], TTY) ->
+ io_requests([{move_combo, 0, R, M}|Rs], TTY);
io_requests([R|Rs], TTY) ->
{noreply, NewTTY} = io_request(R, TTY),
io_requests(Rs, NewTTY);
diff --git a/lib/kernel/test/interactive_shell_SUITE.erl b/lib/kernel/test/interactive_shell_SUITE.erl
index 04f4143ea9..b413800661 100644
--- a/lib/kernel/test/interactive_shell_SUITE.erl
+++ b/lib/kernel/test/interactive_shell_SUITE.erl
@@ -46,9 +46,9 @@
shell_history_custom/1, shell_history_custom_errors/1,
job_control_remote_noshell/1,ctrl_keys/1,
get_columns_and_rows_escript/1,
- shell_navigation/1, shell_xnfix/1, shell_delete/1,
+ shell_navigation/1, shell_multiline_navigation/1, shell_xnfix/1, shell_delete/1,
shell_transpose/1, shell_search/1, shell_insert/1,
- shell_update_window/1, shell_huge_input/1,
+ shell_update_window/1, shell_small_window_multiline_navigation/1, shell_huge_input/1,
shell_invalid_unicode/1, shell_support_ansi_input/1,
shell_invalid_ansi/1, shell_suspend/1, shell_full_queue/1,
shell_unicode_wrap/1, shell_delete_unicode_wrap/1,
@@ -66,7 +66,7 @@
-export([load/0, add/1]).
%% For custom prompt testing
-export([prompt/1]).
-
+-record(tmux, {peer, node, name, orig_location }).
suite() ->
[{ct_hooks,[ts_install_cth]},
{timetrap,{minutes,3}}].
@@ -124,9 +124,9 @@ groups() ->
]},
{tty_latin1,[],[{group,tty_tests}]},
{tty_tests, [parallel],
- [shell_navigation, shell_xnfix, shell_delete,
+ [shell_navigation, shell_multiline_navigation, shell_xnfix, shell_delete,
shell_transpose, shell_search, shell_insert,
- shell_update_window, shell_huge_input,
+ shell_update_window, shell_small_window_multiline_navigation, shell_huge_input,
shell_support_ansi_input,
shell_standard_error_nlcr,
shell_expand_location_above,
@@ -404,7 +404,68 @@ shell_navigation(Config) ->
after
stop_tty(Term)
end.
+shell_multiline_navigation(Config) ->
+ Term = start_tty(Config),
+ try
+ [begin
+ check_location(Term, {0, 0}),
+ send_tty(Term,"{aaa,"),
+ check_location(Term, {0,width("{aaa,")}),
+ send_tty(Term,"\n'b"++U++"b',"),
+ check_location(Term, {0, width("'b"++U++"b',")}),
+ send_tty(Term,"\nccc}"),
+ check_location(Term, {-2, 0}), %% Check that cursor jump backward (blink)
+ timer:sleep(1000), %% Wait for cursor to jump back
+ check_location(Term, {0, width("ccc}")}),
+ send_tty(Term,"Home"),
+ check_location(Term, {0, 0}),
+ send_tty(Term,"End"),
+ check_location(Term, {0, width("ccc}")}),
+ send_tty(Term,"Left"),
+ check_location(Term, {0, width("ccc")}),
+ send_tty(Term,"C-Left"),
+ check_location(Term, {0, 0}),
+ send_tty(Term,"C-Left"),
+ check_location(Term, {-1, width("'b"++U++"b',")}),
+ send_tty(Term,"C-Left"),
+ check_location(Term, {-1, 0}),
+ %send_tty(Term,"C-Left"),
+ %check_location(Term, {-1, 0}),
+ %send_tty(Term,"C-Right"),
+ %check_location(Term, {-1, 1}),
+ send_tty(Term,"C-Right"),
+ check_location(Term, {-1, width("'b"++U++"b'")}),
+ send_tty(Term,"C-Up"),
+ check_location(Term, {-2, width("{aaa,")}),
+ send_tty(Term,"C-Down"),
+ send_tty(Term,"C-Down"),
+ check_location(Term, {0, width("ccc}")}),
+ send_tty(Term,"Left"),
+ send_tty(Term,"C-Up"),
+ check_location(Term, {-1, width("'b"++U)}),
+ send_tty(Term,"M-<"),
+ check_location(Term, {-2, 0}),
+ send_tty(Term,"M->"),
+ send_tty(Term,"Left"),
+ check_location(Term, {0,width("ccc")}),
+ send_tty(Term,"Enter"),
+ send_tty(Term,"Right"),
+ check_location(Term, {0,0}),
+ send_tty(Term,"C-h"), % Backspace
+ check_location(Term, {-1,width("ccc}")}),
+ send_tty(Term,"Left"),
+ send_tty(Term,"M-Enter"),
+ send_tty(Term,"Right"),
+ check_location(Term, {0,1}),
+ send_tty(Term,"M-c"),
+ check_location(Term, {-3,0}),
+ send_tty(Term,"{'"++U++"',\n\n\nworks}.\n")
+ end || U <- hard_unicode()],
+ ok
+ after
+ stop_tty(Term)
+ end.
shell_clear(Config) ->
Term = start_tty(Config),
@@ -727,9 +788,7 @@ shell_transpose(Config) ->
end.
shell_search(C) ->
-
Term = start_tty(C),
- {_Row, Cols} = get_location(Term),
try
send_tty(Term,"a"),
@@ -743,20 +802,22 @@ shell_search(C) ->
send_tty(Term,"Enter"),
check_location(Term, {0, 0}),
send_tty(Term,"C-r"),
- check_location(Term, {0, - Cols + width(C, "(search)`': 'a😀'.") }),
+ check_content(Term, "search:\\s*\n\\s*'a😀'."),
send_tty(Term,"C-a"),
- check_location(Term, {0, width(C, "'a😀'.")}),
+ check_location(Term, {-1, width(C, "'a😀'.")}),
send_tty(Term,"Enter"),
send_tty(Term,"C-r"),
- check_location(Term, {0, - Cols + width(C, "(search)`': 'a😀'.") }),
+ check_content(Term, "search:\\s*\n\\s*'a😀'."),
send_tty(Term,"a"),
- check_location(Term, {0, - Cols + width(C, "(search)`a': 'a😀'.") }),
+ check_content(Term, "search: a\\s*\n\\s*'a😀'."),
send_tty(Term,"C-r"),
- check_location(Term, {0, - Cols + width(C, "(search)`a': a.") }),
+ check_content(Term, "search: a\\s*\n\\s*a."),
send_tty(Term,"BSpace"),
- check_location(Term, {0, - Cols + width(C, "(search)`': 'a😀'.") }),
+ check_content(Term, "search:\\s*\n\\s*'a😀'."),
send_tty(Term,"BSpace"),
- check_location(Term, {0, - Cols + width(C, "(search)`': 'a😀'.") }),
+ check_content(Term, "search:\\s*\n\\s*'a😀'."),
+ send_tty(Term,"M-c"),
+ check_location(Term, {-1, 0}),
ok
after
stop_tty(Term),
@@ -811,7 +872,49 @@ shell_update_window(Config) ->
after
stop_tty(Term)
end.
-
+shell_small_window_multiline_navigation(Config) ->
+ Term0 = start_tty(Config),
+ tmux(["resize-window -t ",tty_name(Term0)," -x ",30, " -y ", 6]),
+ {Row, Col} = get_location(Term0),
+ Term = Term0#tmux{orig_location = {Row, Col}},
+ Text = ("xbcdefghijklmabcdefghijklm\n"++
+ "abcdefghijkl\n"++
+ "abcdefghijklmabcdefghijklm\n"++
+ "abcdefghijklmabcdefghijklx"),
+ try
+ send_tty(Term,Text),
+ check_location(Term, {0, -4}),
+ send_tty(Term,"Home"),
+ check_location(Term, {-1, 0}),
+ send_tty(Term, "C-Up"),
+ check_location(Term, {-2, 0}),
+ send_tty(Term, "C-Down"),
+ check_location(Term, {-1, 0}),
+ send_tty(Term, "Left"),
+ check_location(Term, {-1, -4}),
+ send_tty(Term, "Right"),
+ check_location(Term, {-1, 0}),
+ send_tty(Term, "\e[1;4A"),
+ check_location(Term, {-5, 0}),
+ check_content(Term,"xbc"),
+ send_tty(Term, "\e[1;4B"),
+ check_location(Term, {0, -4}),
+ check_content(Term,"klx"),
+ send_tty(Term, " sets:is_e\t"),
+ check_content(Term,"is_element"),
+ check_content(Term,"is_empty"),
+ check_location(Term, {-3, 6}),
+ send_tty(Term, "C-Up"),
+ send_tty(Term,"Home"),
+ check_location(Term, {-2, 0}),
+ send_tty(Term, "sets:is_e\t"),
+ check_content(Term,"is_element"),
+ check_content(Term,"is_empty"),
+ check_location(Term, {-4, 9}),
+ ok
+ after
+ stop_tty(Term)
+ end.
shell_huge_input(Config) ->
Term = start_tty(Config),
@@ -968,7 +1071,7 @@ shell_expand_location_below(Config) ->
send_stdin(Term, "\t"),
%% The expansion does not fit on screen, verify that
%% expand above mode is used
- check_content(fun() -> get_content(Term, "-S -5") end,
+ check_content(fun() -> get_content(Term, "-S -7") end,
"3> long_module:" ++ FunctionName ++ "\nfunctions"),
check_content(Term, "3> long_module:" ++ FunctionName ++ "$"),
@@ -1063,18 +1166,19 @@ external_editor(Config) ->
tmux(["resize-window -t ",tty_name(Term)," -x 80"]),
send_tty(Term,"os:putenv(\"EDITOR\",\"nano\").\n"),
send_tty(Term, "\"some text with\nnewline in it\""),
- check_content(Term,"3> \"some text with\\s*\n.+3>\\s*newline in it\""),
+ check_content(Term,"3> \"some text with\\s*\n.+\\s*newline in it\""),
send_tty(Term, "C-O"),
check_content(Term,"GNU nano [\\d.]+"),
- check_content(Term,"newline in it\""),
+ check_content(Term,"\"some text with\\s*\n\\s*newline in it\""),
+ send_tty(Term, "Right"),
send_tty(Term, "still"),
send_tty(Term, "Enter"),
send_tty(Term, "C-O"), %% save in nano
send_tty(Term, "Enter"),
send_tty(Term, "C-X"), %% quit in nano
- check_content(Term,"still\n.+3> newline in it\""),
+ check_content(Term,"3> \"still\\s*\n\\s*.+\\s*some text with\\s*\n.+\\s*newline in it\""),
send_tty(Term,".\n"),
- check_content(Term,"\\Q\"some text with\\nstill\\nnewline in it\"\\E"),
+ check_content(Term,"\\Q\"still\\nsome text with\\nnewline in it\"\\E"),
ok
after
stop_tty(Term),
@@ -1380,8 +1484,6 @@ npwcwidth(CP) ->
end
end.
--record(tmux, {peer, node, name, orig_location }).
-
tmux([Cmd|_] = Command) when is_list(Cmd) ->
tmux(lists:concat(Command));
tmux(Command) ->
@@ -2125,15 +2227,13 @@ test_remote_job_control(Node) ->
{expect, "Unknown job"},
{expect, " --> $"},
{putline, "c 1"},
- {expect, "\r\n"},
- {putline, ""},
{expect, "\\Q("++RemNode++"@\\E[^)]*\\)[12]> $"},
{putdata, "\^g"},
{expect, " --> $"},
{putline, "j"},
{expect, "1[*] {shell,start,\\[init]}"},
{putline, "c"},
- {expect, "\r\n"},
+ {expect, "\\Q("++RemNode++"@\\E[^)]*\\)[123]> $"},
{sleep, 100},
{putline, "35."},
{expect, "\\Q("++RemNode++"@\\E[^)]*\\)[123]> $"}