patch 9.1.1070: Cannot control cursor positioning of getchar()

Problem:  Cannot control cursor positioning of getchar().
Solution: Add "cursor" flag to {opts}, with possible values "hide",
          "keep" and "msg".

related: #10603
closes: #16569

Signed-off-by: zeertzjq <zeertzjq@outlook.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index 46c3ba8..39ff14e 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -3953,6 +3953,13 @@
 		The optional argument {opts} is a Dict and supports the
 		following items:
 
+			cursor		A String specifying cursor behavior
+					when waiting for a character.
+					"hide": hide the cursor.
+					"keep": keep current cursor unchanged.
+					"msg": move cursor to message area.
+					(default: "msg")
+
 			number		If |TRUE|, return a Number when getting
 					a single character.
 					If |FALSE|, the return value is always
diff --git a/runtime/doc/todo.txt b/runtime/doc/todo.txt
index 2d48cc9..5338b8b 100644
--- a/runtime/doc/todo.txt
+++ b/runtime/doc/todo.txt
@@ -1,4 +1,4 @@
-*todo.txt*      For Vim version 9.1.  Last change: 2025 Jan 16
+*todo.txt*      For Vim version 9.1.  Last change: 2025 Feb 02
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -467,9 +467,6 @@
 When converting screen column to text position use this.
 The line number can be obtained from win->w_lines[].
 
-Version of getchar() that does not move the cursor - #10603 Use a separate
-argument for the new flag.
-
 test_arglist func Test_all_not_allowed_from_cmdwin() hangs on MS-Windows.
 
 Can we add highlighting to ":echowindow"?
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 010a0cb..68287d3 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -41636,6 +41636,8 @@
 - add |dist#vim9#Launch()| and |dist#vim9#Open()| to the |vim-script-library|
   and decouple it from |netrw|
 - new digraph "APPROACHES THE LIMIT" using ".="
+- Add the optional {opts} |Dict| argument to |getchar()| to control: cursor
+  behaviour, return type and whether or not to simplify the returned key
 
 							*added-9.2*
 Added ~
diff --git a/src/getchar.c b/src/getchar.c
index 06f4ad4..83a9861 100644
--- a/src/getchar.c
+++ b/src/getchar.c
@@ -2386,9 +2386,11 @@
     static void
 getchar_common(typval_T *argvars, typval_T *rettv, int allow_number)
 {
-    varnumber_T		n;
+    varnumber_T		n = 0;
+    int			called_emsg_start = called_emsg;
     int			error = FALSE;
     int			simplify = TRUE;
+    char_u		cursor_flag = 'm';
 
     if ((in_vim9script()
 		&& check_for_opt_bool_or_number_arg(argvars, 0) == FAIL)
@@ -2399,18 +2401,31 @@
     if (argvars[0].v_type != VAR_UNKNOWN && argvars[1].v_type == VAR_DICT)
     {
 	dict_T		*d = argvars[1].vval.v_dict;
+	char_u		*cursor_str;
 
 	if (allow_number)
 	    allow_number = dict_get_bool(d, "number", TRUE);
 	else if (dict_has_key(d, "number"))
-	{
 	    semsg(_(e_invalid_argument_str), "number");
-	    error = TRUE;
-	}
 
 	simplify = dict_get_bool(d, "simplify", TRUE);
+
+	cursor_str = dict_get_string(d, "cursor", FALSE);
+	if (cursor_str != NULL)
+	{
+	    if (STRCMP(cursor_str, "hide") != 0
+		    && STRCMP(cursor_str, "keep") != 0
+		    && STRCMP(cursor_str, "msg") != 0)
+		semsg(_(e_invalid_value_for_argument_str_str), "cursor",
+								   cursor_str);
+	    else
+		cursor_flag = cursor_str[0];
+	}
     }
 
+    if (called_emsg != called_emsg_start)
+	return;
+
 #ifdef MESSAGE_QUEUE
     // vpeekc() used to check for messages, but that caused problems, invoking
     // a callback where it was not expected.  Some plugins use getchar(1) in a
@@ -2418,14 +2433,16 @@
     parse_queued_messages();
 #endif
 
-    // Position the cursor.  Needed after a message that ends in a space.
-    windgoto(msg_row, msg_col);
+    if (cursor_flag == 'h')
+	cursor_sleep();
+    else if (cursor_flag == 'm')
+	windgoto(msg_row, msg_col);
 
     ++no_mapping;
     ++allow_keys;
     if (!simplify)
 	++no_reduce_keys;
-    while (!error)
+    for (;;)
     {
 	if (argvars[0].v_type == VAR_UNKNOWN
 		|| (argvars[0].v_type == VAR_NUMBER
@@ -2453,6 +2470,9 @@
     if (!simplify)
 	--no_reduce_keys;
 
+    if (cursor_flag == 'h')
+	cursor_unsleep();
+
     set_vim_var_nr(VV_MOUSE_WIN, 0);
     set_vim_var_nr(VV_MOUSE_WINID, 0);
     set_vim_var_nr(VV_MOUSE_LNUM, 0);
diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index 4672fc0..5783c7a 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -2628,6 +2628,14 @@
 
   call assert_fails('call getchar(1, 1)', 'E1206:')
   call assert_fails('call getcharstr(1, 1)', 'E1206:')
+  call assert_fails('call getchar(1, #{cursor: "foo"})', 'E475:')
+  call assert_fails('call getcharstr(1, #{cursor: "foo"})', 'E475:')
+  call assert_fails('call getchar(1, #{cursor: 0z})', 'E976:')
+  call assert_fails('call getcharstr(1, #{cursor: 0z})', 'E976:')
+  call assert_fails('call getchar(1, #{simplify: 0z})', 'E974:')
+  call assert_fails('call getcharstr(1, #{simplify: 0z})', 'E974:')
+  call assert_fails('call getchar(1, #{number: []})', 'E745:')
+  call assert_fails('call getchar(1, #{number: {}})', 'E728:')
   call assert_fails('call getcharstr(1, #{number: v:true})', 'E475:')
   call assert_fails('call getcharstr(1, #{number: v:false})', 'E475:')
 
@@ -2646,6 +2654,57 @@
   enew!
 endfunc
 
+func Test_getchar_cursor_position()
+  CheckRunVimInTerminal
+
+  let lines =<< trim END
+    call setline(1, ['foobar', 'foobar', 'foobar'])
+    call cursor(3, 6)
+    nnoremap <F1> <Cmd>echo 1234<Bar>call getchar()<CR>
+    nnoremap <F2> <Cmd>call getchar()<CR>
+    nnoremap <F3> <Cmd>call getchar(-1, {})<CR>
+    nnoremap <F4> <Cmd>call getchar(-1, #{cursor: 'msg'})<CR>
+    nnoremap <F5> <Cmd>call getchar(-1, #{cursor: 'keep'})<CR>
+    nnoremap <F6> <Cmd>call getchar(-1, #{cursor: 'hide'})<CR>
+  END
+  call writefile(lines, 'XgetcharCursorPos', 'D')
+  let buf = RunVimInTerminal('-S XgetcharCursorPos', {'rows': 6})
+  call WaitForAssert({-> assert_equal([3, 6], term_getcursor(buf)[0:1])})
+
+  call term_sendkeys(buf, "\<F1>")
+  call WaitForAssert({-> assert_equal([6, 5], term_getcursor(buf)[0:1])})
+  call assert_true(term_getcursor(buf)[2].visible)
+  call term_sendkeys(buf, 'a')
+  call WaitForAssert({-> assert_equal([3, 6], term_getcursor(buf)[0:1])})
+  call assert_true(term_getcursor(buf)[2].visible)
+
+  for key in ["\<F2>", "\<F3>", "\<F4>"]
+    call term_sendkeys(buf, key)
+    call WaitForAssert({-> assert_equal([6, 1], term_getcursor(buf)[0:1])})
+    call assert_true(term_getcursor(buf)[2].visible)
+    call term_sendkeys(buf, 'a')
+    call WaitForAssert({-> assert_equal([3, 6], term_getcursor(buf)[0:1])})
+    call assert_true(term_getcursor(buf)[2].visible)
+  endfor
+
+  call term_sendkeys(buf, "\<F5>")
+  call TermWait(buf, 50)
+  call assert_equal([3, 6], term_getcursor(buf)[0:1])
+  call assert_true(term_getcursor(buf)[2].visible)
+  call term_sendkeys(buf, 'a')
+  call TermWait(buf, 50)
+  call assert_equal([3, 6], term_getcursor(buf)[0:1])
+  call assert_true(term_getcursor(buf)[2].visible)
+
+  call term_sendkeys(buf, "\<F6>")
+  call WaitForAssert({-> assert_false(term_getcursor(buf)[2].visible)})
+  call term_sendkeys(buf, 'a')
+  call WaitForAssert({-> assert_true(term_getcursor(buf)[2].visible)})
+  call assert_equal([3, 6], term_getcursor(buf)[0:1])
+
+  call StopVimInTerminal(buf)
+endfunc
+
 func Test_libcall_libcallnr()
   CheckFeature libcall
 
diff --git a/src/version.c b/src/version.c
index f9d99a5..5ed5bcf 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1070,
+/**/
     1069,
 /**/
     1068,