The following series implements filename completion in cmd, à la Windows (tab (resp. shift-tab) rotates in alphabetical order (resp. backwards alpha order) the available filenames starting with a given prefix).
It requires two fixes in programs/conhost.
Notes to Jacek - for the second patch, I tried to pick most of the APIs requiring cursor positionning. I wonder if SetConsoleCursorPosition (when y is outside a given interval) shouldn't be part of it (no concrete example at sight, just wild thinking) (something like deltay < -5 || deltay > 0) (This can be done later on anyway)
Signed-off-by: Eric Pouech eric.pouech@gmail.com ---
Eric Pouech (3): programs/conhost: correctly recompute start of edit line on ReadConsole programs/conhost: support relative cursor positionning programs/cmd: implement filename completion
programs/cmd/batch.c | 279 ++++++++++++++++++++++++++++++++++++- programs/conhost/conhost.c | 22 ++- programs/conhost/conhost.h | 1 + 3 files changed, 294 insertions(+), 8 deletions(-)
Signed-off-by: Eric Pouech eric.pouech@gmail.com
--- programs/conhost/conhost.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/programs/conhost/conhost.c b/programs/conhost/conhost.c index 11e97993fd8..461adc05bcc 100644 --- a/programs/conhost/conhost.c +++ b/programs/conhost/conhost.c @@ -1426,8 +1426,8 @@ static NTSTATUS read_console( struct console *console, unsigned int ioctl, size_ if (offset > ctx->home_x) { int deltay; - offset -= ctx->home_x; - deltay = offset / console->active->width; + offset -= ctx->home_x + 1; + deltay = offset / console->active->width + 1; if (ctx->home_y >= deltay) ctx->home_y -= deltay; else
Signed-off-by: Jacek Caban jacek@codeweavers.com
added also fallback method when legacy console APIs where used
Signed-off-by: Eric Pouech eric.pouech@gmail.com
--- programs/conhost/conhost.c | 18 ++++++++++++++++++ programs/conhost/conhost.h | 1 + 2 files changed, 19 insertions(+)
diff --git a/programs/conhost/conhost.c b/programs/conhost/conhost.c index 461adc05bcc..d494e3f53a5 100644 --- a/programs/conhost/conhost.c +++ b/programs/conhost/conhost.c @@ -328,6 +328,12 @@ static void init_tty_output( struct console *console ) console->tty_cursor_visible = TRUE; }
+/* no longer use relative cursor positionning (legacy API have been used) */ +static void enter_absolute_mode( struct console *console ) +{ + console->use_relative_cursor = 0; +} + static void scroll_to_cursor( struct screen_buffer *screen_buffer ) { unsigned int cursor_x = get_bounded_cursor_x( screen_buffer ); @@ -1920,13 +1926,17 @@ static NTSTATUS set_output_info( struct screen_buffer *screen_buffer,
if (screen_buffer->cursor_x != info->cursor_x || screen_buffer->cursor_y != info->cursor_y) { + struct console *console = screen_buffer->console; screen_buffer->cursor_x = info->cursor_x; screen_buffer->cursor_y = info->cursor_y; + if (console->use_relative_cursor) + set_tty_cursor_relative( console, screen_buffer->cursor_x, screen_buffer->cursor_y ); scroll_to_cursor( screen_buffer ); } } if (params->mask & SET_CONSOLE_OUTPUT_INFO_SIZE) { + enter_absolute_mode( screen_buffer->console ); /* new screen-buffer cannot be smaller than actual window */ if (info->width < screen_buffer->win.right - screen_buffer->win.left + 1 || info->height < screen_buffer->win.bottom - screen_buffer->win.top + 1) @@ -1962,6 +1972,7 @@ static NTSTATUS set_output_info( struct screen_buffer *screen_buffer, } if (params->mask & SET_CONSOLE_OUTPUT_INFO_DISPLAY_WINDOW) { + enter_absolute_mode( screen_buffer->console ); if (info->win_left < 0 || info->win_left > info->win_right || info->win_right >= screen_buffer->width || info->win_top < 0 || info->win_top > info->win_bottom || @@ -1980,6 +1991,7 @@ static NTSTATUS set_output_info( struct screen_buffer *screen_buffer, } if (params->mask & SET_CONSOLE_OUTPUT_INFO_MAX_SIZE) { + enter_absolute_mode( screen_buffer->console ); screen_buffer->max_width = info->max_width; screen_buffer->max_height = info->max_height; } @@ -2085,6 +2097,7 @@ static NTSTATUS write_output( struct screen_buffer *screen_buffer, const struct char_info_t *dest; char *src;
+ enter_absolute_mode( screen_buffer->console ); if (*out_size == sizeof(SMALL_RECT) && !params->width) return STATUS_INVALID_PARAMETER;
entry_size = params->mode == CHAR_INFO_MODE_TEXTATTR ? sizeof(char_info_t) : sizeof(WCHAR); @@ -2187,6 +2200,7 @@ static NTSTATUS read_output( struct screen_buffer *screen_buffer, const struct c unsigned int x, y, width; unsigned int i, count;
+ enter_absolute_mode( screen_buffer->console ); x = params->x; y = params->y; mode = params->mode; @@ -2265,6 +2279,7 @@ static NTSTATUS fill_output( struct screen_buffer *screen_buffer, const struct c
TRACE( "(%u %u) mode %u\n", params->x, params->y, params->mode );
+ enter_absolute_mode( screen_buffer->console ); if (params->y >= screen_buffer->height) return STATUS_SUCCESS; dest = screen_buffer->data + min( params->y * screen_buffer->width + params->x, screen_buffer->height * screen_buffer->width ); @@ -2318,6 +2333,7 @@ static NTSTATUS scroll_output( struct screen_buffer *screen_buffer, const struct RECT update_rect; SMALL_RECT clip;
+ enter_absolute_mode( screen_buffer->console ); xsrc = params->scroll.Left; ysrc = params->scroll.Top; w = params->scroll.Right - params->scroll.Left + 1; @@ -2736,6 +2752,7 @@ static NTSTATUS process_console_ioctls( struct console *console ) if (code == IOCTL_CONDRV_INIT_OUTPUT) { TRACE( "initializing output %x\n", output ); + enter_absolute_mode( console ); if (console->active) create_screen_buffer( console, output, console->active->width, console->active->height ); else @@ -2862,6 +2879,7 @@ int __cdecl wmain(int argc, WCHAR *argv[]) if (!wcscmp( argv[i], L"--unix")) { console.is_unix = 1; + console.use_relative_cursor = 1; headless = 1; continue; } diff --git a/programs/conhost/conhost.h b/programs/conhost/conhost.h index 35876689419..8ca09bb80d0 100644 --- a/programs/conhost/conhost.h +++ b/programs/conhost/conhost.h @@ -78,6 +78,7 @@ struct console unsigned int mode; /* input mode */ struct screen_buffer *active; /* active screen buffer */ int is_unix; /* UNIX terminal mode */ + int use_relative_cursor; /* use relative cursor positionning */ INPUT_RECORD *records; /* input records */ unsigned int record_count; /* number of input records */ unsigned int record_size; /* size of input records buffer */
Signed-off-by: Jacek Caban jacek@codeweavers.com
Signed-off-by: Eric Pouech eric.pouech@gmail.com
--- programs/cmd/batch.c | 279 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 273 insertions(+), 6 deletions(-)
diff --git a/programs/cmd/batch.c b/programs/cmd/batch.c index 9a262c5fec5..934cbcc7a39 100644 --- a/programs/cmd/batch.c +++ b/programs/cmd/batch.c @@ -227,6 +227,211 @@ WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **start, BOOL raw, return WCMD_parameter_with_delims (s, n, start, raw, wholecmdline, L" \t,=;"); }
+struct completion_info +{ + WCHAR** subfiles; + DWORD index_search; + DWORD num_results; +}; + +/* gets directory name out of 'path', appends 'in' and surround with quotes if needed */ +static WCHAR* dup_quoted(const WCHAR* path, const WCHAR* in) +{ + const WCHAR* last = wcsrchr(path, L'/'); + size_t dirlen; + size_t len; + WCHAR* ret; + + if (last) + { + WCHAR* last1 = wcschr(last + 1, L'\'); + if (last1) last = last1; + } + else last = wcsrchr(path, L'\'); + dirlen = last ? (last - path) + 1 : 0; + len = wcslen(in); + ret = malloc((dirlen + len + 1) * sizeof(WCHAR)); + if (ret) + { + memcpy(ret, path, dirlen * sizeof(WCHAR)); + wcscpy(ret + dirlen, in); + if (wcspbrk(ret, L" \t")) /* do we need to surround with quotes? */ + { + WCHAR* new = realloc(ret, (1 + dirlen + len + 1 + 1) * sizeof(WCHAR)); + if (!new) + { + free(ret); + return NULL; + } + ret = new; + memmove(ret + 1, ret, (dirlen + len) * sizeof(WCHAR)); + ret[0] = ret[1 + dirlen + len] = L'"'; + ret[1 + dirlen + len + 1] = L'\0'; + } + } + return ret; +} + +static BOOL init_completion(struct completion_info* ci, const WCHAR* from, DWORD len, BOOL forward) +{ + WCHAR* ptr; + HANDLE hff; + WIN32_FIND_DATAW fd; + BOOL in_quotes = FALSE; + DWORD i, j = 0; + + ptr = malloc((len + 1 + 1) * sizeof(WCHAR)); + if (!ptr) return FALSE; + for (i = 0; i < len; i++) + { + switch (from[i]) + { + case L'"': + in_quotes = !in_quotes; + continue; + case L'\': + if (!in_quotes && i + 1 < len && wcschr(L" \t"\", from[i + 1]) != NULL) + ++i; + break; + case L' ': + case L'\t': + /* shouldn't happen, as 'from' should contain a single argument */ + if (!in_quotes) + { + free(ptr); + return FALSE; + } + break; + } + ptr[j++] = from[i]; + } + ptr[j] = L'*'; + ptr[j + 1] = L'\0'; + hff = FindFirstFileW(ptr, &fd); + ci->num_results = 0; + ci->subfiles = NULL; + if (hff != INVALID_HANDLE_VALUE) + { + do + { + WCHAR** new; + if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..")) + continue; + new = realloc(ci->subfiles, (ci->num_results + 1) * sizeof(*ci->subfiles)); + if (!new) + { + FindClose(hff); + free(ptr); + return FALSE; + } + ci->subfiles = new; + if (!(ci->subfiles[ci->num_results++] = dup_quoted(ptr, fd.cFileName))) + { + FindClose(hff); + free(ptr); + return FALSE; + } + } while (FindNextFileW(hff, &fd)); + FindClose(hff); + } + free(ptr); + if (!ci->num_results) return FALSE; + ci->index_search = forward ? 0 : ci->num_results - 1; + return TRUE; +} + +static void dispose_completion(struct completion_info* ci) +{ + if (ci->subfiles) + { + unsigned i; + for (i = 0; i < ci->num_results; i++) + free(ci->subfiles[i]); + free(ci->subfiles); + ci->subfiles = NULL; + } +} + +static WCHAR* next_completion(struct completion_info* ci, const WCHAR* from, DWORD len, BOOL forward) +{ + if (!ci->subfiles || + memcmp(ci->subfiles[ci->index_search], from, len * sizeof(WCHAR)) || + ci->subfiles[ci->index_search][len]) + { + dispose_completion(ci); + if (!init_completion(ci, from, len, forward)) return NULL; + } + else + { + ci->index_search += forward ? 1 : (ci->num_results - 1); + ci->index_search %= ci->num_results; + } + return ci->subfiles[ci->index_search]; +} + +/* backup cursor position by 'len' (display) characters */ +static BOOL backup_cursor_position(HANDLE hout, int len) +{ + CONSOLE_SCREEN_BUFFER_INFO sbi; + if (GetConsoleScreenBufferInfo(hout, &sbi)) + { + if (len > sbi.dwCursorPosition.X) + { + len -= sbi.dwCursorPosition.X + 1; + sbi.dwCursorPosition.X = sbi.dwSize.X - 1 - (len % sbi.dwSize.X); + sbi.dwCursorPosition.Y -= len / sbi.dwSize.X + 1; + if (sbi.dwCursorPosition.Y < 0) + sbi.dwCursorPosition.Y = 0; + } + else + sbi.dwCursorPosition.X -= len; + return SetConsoleCursorPosition(hout, sbi.dwCursorPosition); + } + return FALSE; +} + +/* write numspaces the character ' ' from cursor position, without altering cursor position */ +static void clear_spaces(HANDLE hout, int numspaces) +{ + static const WCHAR spaces[] = {L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' '}; + int ns = numspaces; + + while (ns > 0) + { + DWORD nwritten; + + if (!WriteConsoleW(hout, spaces, min(ns, ARRAY_SIZE(spaces)), &nwritten, NULL) || !nwritten) + break; + ns -= nwritten; + } + backup_cursor_position(hout, numspaces - ns); +} + +/* number of characters to display a string (including escape for control characters */ +static DWORD get_display_length(const WCHAR* str, unsigned len) +{ + DWORD ret = len; + unsigned i; + + for (i = 0; i < len; i++) if (str[i] < ' ') ++ret; + return ret; +} + +static WCHAR *find_start_of_parameter(WCHAR *buf, WCHAR *ptr) +{ + int n = 0; + for (;; n++) + { + WCHAR* next; + WCHAR* pmt = WCMD_parameter(buf, n, &next, TRUE, FALSE); + if (!pmt[0] || next > ptr) + return ptr; + if (ptr <= next + wcslen(pmt)) + return next; + } + return NULL; /* shouldn't happen */ +} + /**************************************************************************** * WCMD_fgets * @@ -242,14 +447,19 @@ WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **start, BOOL raw,
WCHAR *WCMD_fgets(WCHAR *buf, DWORD noChars, HANDLE h) { + CONSOLE_READCONSOLE_CONTROL crc; DWORD charsRead; BOOL status; DWORD i;
+ crc.nLength = sizeof(crc); + crc.nInitialChars = 0; + crc.dwCtrlWakeupMask = 1 << '\t'; + crc.dwConsoleKeyState = 0; /* We can't use the native f* functions because of the filename syntax differences between DOS and Unix. Also need to lose the LF (or CRLF) from the line. */
- if (!ReadConsoleW(h, buf, noChars, &charsRead, NULL)) { + if (!ReadConsoleW(h, buf, noChars - 1, &charsRead, &crc)) { LARGE_INTEGER filepos; char *bufA; UINT cp; @@ -282,13 +492,70 @@ WCHAR *WCMD_fgets(WCHAR *buf, DWORD noChars, HANDLE h) heap_free(bufA); } else { + struct completion_info ci = {NULL, 0, 0}; + HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD last_display_length = 0; + if (!charsRead) return NULL; + do + { + WCHAR* tabptr = wmemchr(buf, L'\t', charsRead); + if (tabptr) + { + WCHAR* start; + WCHAR* completion; + DWORD nwritten; + unsigned display_input_length; + + /* we still have <tab> in buf, so don't include it in count */ + display_input_length = get_display_length(buf, charsRead) - 2; + /* find start of word containing tabptr (if any) */ + buf[charsRead] = '\0'; + start = find_start_of_parameter(buf, tabptr); + completion = next_completion(&ci, start, tabptr - start, !(crc.dwConsoleKeyState & SHIFT_PRESSED)); + if (completion) + { + if (start - buf + wcslen(completion) + 1 > noChars) + { + memcpy(start, completion, (start - buf + noChars - 1) * sizeof(WCHAR)); + charsRead = noChars - 1; + } + else + { + wcscpy(start, completion); + charsRead = wcslen(buf); + } + } + else + { + /* Beep() -- not loud enough, maybe user won't hear it */ + charsRead = tabptr - buf; + } + /* scrolling or spurious write could have happened in ReadConsole + * so re-compute coordinates of start of input relative to current cursor position + */ + if (backup_cursor_position(hout, display_input_length)) + { + DWORD display_completion_length; + + WriteConsoleW(hout, buf, charsRead, &nwritten, NULL); + display_completion_length = get_display_length(buf, charsRead); + if (last_display_length > display_completion_length) + clear_spaces(hout, last_display_length - display_completion_length); + last_display_length = display_completion_length; + } + crc.nInitialChars = charsRead; + } + /* Find first EOL */ + for (i = 0; i < charsRead; i++) { + if (buf[i] == '\n' || buf[i] == '\r') + break; + } + if (i < charsRead) break;
- /* Find first EOL */ - for (i = 0; i < charsRead; i++) { - if (buf[i] == '\n' || buf[i] == '\r') - break; - } + } while (ReadConsoleW(h, buf, noChars - 1, &charsRead, &crc) && charsRead); + + dispose_completion(&ci); }
/* Truncate at EOL (or end of buffer) */
Hi Eric,
On 3/24/22 14:46, Eric Pouech wrote:
Signed-off-by: Eric Pouech eric.pouech@gmail.com
programs/cmd/batch.c | 279 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 273 insertions(+), 6 deletions(-)
diff --git a/programs/cmd/batch.c b/programs/cmd/batch.c index 9a262c5fec5..934cbcc7a39 100644 --- a/programs/cmd/batch.c +++ b/programs/cmd/batch.c @@ -227,6 +227,211 @@ WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **start, BOOL raw, return WCMD_parameter_with_delims (s, n, start, raw, wholecmdline, L" \t,=;"); }
+struct completion_info +{
- WCHAR** subfiles;
- DWORD index_search;
- DWORD num_results;
+};
+/* gets directory name out of 'path', appends 'in' and surround with quotes if needed */ +static WCHAR* dup_quoted(const WCHAR* path, const WCHAR* in) +{
- const WCHAR* last = wcsrchr(path, L'/');
- size_t dirlen;
- size_t len;
- WCHAR* ret;
- if (last)
- {
WCHAR* last1 = wcschr(last + 1, L'\\');
if (last1) last = last1;
- }
- else last = wcsrchr(path, L'\');
- dirlen = last ? (last - path) + 1 : 0;
I think that last1 should use wcsrchr as well? Or maybe even replace the whole dirlen logic with a simple loop.
+static BOOL init_completion(struct completion_info* ci, const WCHAR* from, DWORD len, BOOL forward) +{
- WCHAR* ptr;
- HANDLE hff;
- WIN32_FIND_DATAW fd;
- BOOL in_quotes = FALSE;
- DWORD i, j = 0;
- ptr = malloc((len + 1 + 1) * sizeof(WCHAR));
- if (!ptr) return FALSE;
- for (i = 0; i < len; i++)
- {
switch (from[i])
{
case L'"':
in_quotes = !in_quotes;
continue;
case L'\\':
if (!in_quotes && i + 1 < len && wcschr(L" \t\"\\", from[i + 1]) != NULL)
++i;
break;
case L' ':
case L'\t':
/* shouldn't happen, as 'from' should contain a single argument */
if (!in_quotes)
{
free(ptr);
return FALSE;
}
break;
}
ptr[j++] = from[i];
- }
- ptr[j] = L'*';
- ptr[j + 1] = L'\0';
- hff = FindFirstFileW(ptr, &fd);
- ci->num_results = 0;
- ci->subfiles = NULL;
- if (hff != INVALID_HANDLE_VALUE)
- {
do
{
WCHAR** new;
if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L".."))
continue;
new = realloc(ci->subfiles, (ci->num_results + 1) * sizeof(*ci->subfiles));
if (!new)
{
FindClose(hff);
free(ptr);
return FALSE;
}
ci->subfiles = new;
if (!(ci->subfiles[ci->num_results++] = dup_quoted(ptr, fd.cFileName)))
{
FindClose(hff);
free(ptr);
return FALSE;
}
} while (FindNextFileW(hff, &fd));
FindClose(hff);
- }
- free(ptr);
- if (!ci->num_results) return FALSE;
- ci->index_search = forward ? 0 : ci->num_results - 1;
- return TRUE;
+}
If I read it correctly, if another process creates a file when completion is already initialized, we will not take that into account. Is that intended?
Thanks,
Jacek
I think that last1 should use wcsrchr as well? Or maybe even replace the whole dirlen logic with a simple loop.
the idea is to find the last '/' or '\'... so if '/' if r-found, we try to find forward a '\'... so the
but wcsrpbrk doesn't exist AFAICT... but supporting here unix paths might be a bit overkill; so we could keep only the first wcsrchr(..., '\')
If I read it correctly, if another process creates a file when completion is already initialized, we will not take that into account. Is that intended?
well, that's the way it's coded <g>, and the way it's implemented on native (tested with W11)
ditto if a file is deleted
A+