diff options
Diffstat (limited to 'lib/kernel')
-rw-r--r-- | lib/kernel/src/group.erl | 348 | ||||
-rw-r--r-- | lib/kernel/src/prim_tty.erl | 380 | ||||
-rw-r--r-- | lib/kernel/src/user_drv.erl | 48 | ||||
-rw-r--r-- | lib/kernel/test/interactive_shell_SUITE.erl | 150 |
4 files changed, 686 insertions, 240 deletions
diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl index 665b326ea0..65b39489c4 100644 --- a/lib/kernel/src/group.erl +++ b/lib/kernel/src/group.erl @@ -470,50 +470,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) -> @@ -545,65 +551,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 @@ -616,15 +630,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 -> @@ -671,66 +683,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) -> @@ -739,29 +783,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) -> @@ -801,21 +848,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 @@ -896,8 +943,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]}). @@ -921,22 +968,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. @@ -950,16 +997,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 7ed418de5e..3a70873748 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 @@ -165,10 +167,20 @@ }. -type request() :: {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{}. @@ -275,9 +287,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 @@ -320,7 +332,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 @@ -532,26 +544,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? @@ -560,34 +608,134 @@ 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 = #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), @@ -597,29 +745,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 @@ -630,7 +806,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) -> @@ -680,6 +856,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) -> @@ -723,6 +970,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. @@ -774,7 +1022,7 @@ characters_to_output(Chars) -> (Char) -> Char end, Chars) - ) + ) end. characters_to_buffer(Chars) -> lists:flatmap( @@ -810,12 +1058,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 -> @@ -824,10 +1072,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 bcc0d2b78b..b9d879d33c 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) -> @@ -766,9 +794,15 @@ io_request({put_chars_sync, unicode, 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) -> @@ -786,6 +820,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]> $"} |