Get the first command output on screen and the last scrolled one

This commit is contained in:
pagedown
2021-11-14 15:13:05 +08:00
committed by Kovid Goyal
parent 9fe9c74021
commit f3dd2a8bfd
9 changed files with 201 additions and 34 deletions

View File

@@ -93,8 +93,13 @@ instead. If you want line wrap markers as well, use ``screen-ansi``
or just ``screen``. For the scrollback buffer as well, use or just ``screen``. For the scrollback buffer as well, use
``history``, ``ansi-history`` or ``screen-history``. To get ``history``, ``ansi-history`` or ``screen-history``. To get
the currently selected text, use ``selection``. To get the output the currently selected text, use ``selection``. To get the output
of the first command run in the shell on screen, use ``first-output``
or ``first-output-ansi`` or ``first-output-screen-ansi``. To get the output
of the last command run in the shell, use ``output`` or ``output-ansi`` of the last command run in the shell, use ``output`` or ``output-ansi``
or ``output-screen-ansi``. Note that using ``output`` requires or ``output-screen-ansi``. To get the first command output below the last
scrolled position via scroll_to_prompt, use ``last-visited-output`` or
``last-visited-output-ansi`` or ``last-visited-output-screen-ansi``. Note that
using ``first-output`` or ``output`` or ``last-visited-output`` requires
:ref:`shell_integration`. :ref:`shell_integration`.

View File

@@ -110,10 +110,18 @@ def data_for_at(w: Optional[Window], arg: str, add_wrap_markers: bool = False) -
return as_text(as_ansi=True, alternate_screen=True) return as_text(as_ansi=True, alternate_screen=True)
if arg == '@ansi_alternate_scrollback': if arg == '@ansi_alternate_scrollback':
return as_text(as_ansi=True, alternate_screen=True, add_history=True) return as_text(as_ansi=True, alternate_screen=True, add_history=True)
if arg == '@first_cmd_output_on_screen':
return w.first_cmd_output_on_screen(add_wrap_markers=add_wrap_markers)
if arg == '@ansi_first_cmd_output_on_screen':
return w.first_cmd_output_on_screen(as_ansi=True, add_wrap_markers=add_wrap_markers)
if arg == '@last_cmd_output': if arg == '@last_cmd_output':
return w.last_cmd_output(add_wrap_markers=add_wrap_markers) return w.last_cmd_output(add_wrap_markers=add_wrap_markers)
if arg == '@ansi_last_cmd_output': if arg == '@ansi_last_cmd_output':
return w.last_cmd_output(as_ansi=True, add_wrap_markers=add_wrap_markers) return w.last_cmd_output(as_ansi=True, add_wrap_markers=add_wrap_markers)
if arg == '@last_visited_cmd_output':
return w.last_visited_cmd_output(add_wrap_markers=add_wrap_markers)
if arg == '@ansi_last_visited_cmd_output':
return w.last_visited_cmd_output(as_ansi=True, add_wrap_markers=add_wrap_markers)
return None return None
@@ -1314,9 +1322,15 @@ class Boss:
data = sel.encode('utf-8') if sel else None data = sel.encode('utf-8') if sel else None
elif type_of_input is None: elif type_of_input is None:
data = None data = None
elif type_of_input in ('first-output', 'first-output-screen', 'first-output-screen-ansi', 'first-output-ansi'):
q = type_of_input.split('-')
data = w.first_cmd_output_on_screen(as_ansi='ansi' in q, add_wrap_markers='screen' in q).encode('utf-8')
elif type_of_input in ('output', 'output-screen', 'output-screen-ansi', 'output-ansi'): elif type_of_input in ('output', 'output-screen', 'output-screen-ansi', 'output-ansi'):
q = type_of_input.split('-') q = type_of_input.split('-')
data = w.last_cmd_output(as_ansi='ansi' in q, add_wrap_markers='screen' in q).encode('utf-8') data = w.last_cmd_output(as_ansi='ansi' in q, add_wrap_markers='screen' in q).encode('utf-8')
elif type_of_input in ('last-visited-output', 'last-visited-output-screen', 'last-visited-output-screen-ansi', 'last-visited-output-ansi'):
q = type_of_input.split('-')
data = w.last_visited_cmd_output(as_ansi='ansi' in q, add_wrap_markers='screen' in q).encode('utf-8')
else: else:
raise ValueError(f'Unknown type_of_input: {type_of_input}') raise ValueError(f'Unknown type_of_input: {type_of_input}')
else: else:

View File

@@ -1120,7 +1120,9 @@ class Screen:
pass pass
as_text_non_visual = as_text as_text_non_visual = as_text
as_text_alternate = as_text as_text_alternate = as_text
first_cmd_output_on_screen = as_text
last_cmd_output = as_text last_cmd_output = as_text
last_visited_cmd_output = as_text
def scroll_until_cursor(self) -> None: def scroll_until_cursor(self) -> None:
pass pass

View File

@@ -123,15 +123,17 @@ computers (for example, over ssh) or as other users.
--stdin-source --stdin-source
type=choices type=choices
default=none default=none
choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback,@last_cmd_output choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback,@first_cmd_output_on_screen,@last_cmd_output,@last_visited_cmd_output
Pass the screen contents as :code:`STDIN` to the child process. :code:`@selection` is Pass the screen contents as :code:`STDIN` to the child process. :code:`@selection` is
the currently selected text. :code:`@screen` is the contents of the currently active the currently selected text. :code:`@screen` is the contents of the currently active
window. :code:`@screen_scrollback` is the same as :code:`@screen`, but includes the window. :code:`@screen_scrollback` is the same as :code:`@screen`, but includes the
scrollback buffer as well. :code:`@alternate` is the secondary screen of the current scrollback buffer as well. :code:`@alternate` is the secondary screen of the current
active window. For example if you run a full screen terminal application, the active window. For example if you run a full screen terminal application, the
secondary screen will be the screen you return to when quitting the secondary screen will be the screen you return to when quitting the application.
application. :code:`@last_cmd_output` is the output from the last command run in the shell, :code:`@first_cmd_output_on_screen` is the output from the first command run in the shell on screen,
this needs :ref:`shell_integration` to work. :code:`@last_cmd_output` is the output from the last command run in the shell,
:code:`@last_visited_cmd_output` is the first output below the last scrolled position via
scroll_to_prompt, this three needs :ref:`shell_integration` to work.
--stdin-add-formatting --stdin-add-formatting
@@ -322,7 +324,8 @@ def launch(
if opts.stdin_source != 'none': if opts.stdin_source != 'none':
q = str(opts.stdin_source) q = str(opts.stdin_source)
if opts.stdin_add_formatting: if opts.stdin_add_formatting:
if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback'): if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback',
'@first_cmd_output_on_screen', '@last_cmd_output', '@last_visited_cmd_output'):
q = '@ansi_' + q[1:] q = '@ansi_' + q[1:]
if opts.stdin_add_line_wrap_markers: if opts.stdin_add_line_wrap_markers:
q += '_wrap' q += '_wrap'

View File

@@ -3032,6 +3032,10 @@ of the last command run in the shell using the :doc:`launch` function.
For example, the following opens the output in less in an overlay window:: For example, the following opens the output in less in an overlay window::
map f1 launch --stdin-source=@last_cmd_output --stdin-add-formatting --type=overlay less +G -R map f1 launch --stdin-source=@last_cmd_output --stdin-add-formatting --type=overlay less +G -R
To get the first command output on the screen, use :code:`@first_cmd_output_on_screen`.
To get the first command output below the last scrolled position via scroll_to_prompt, use
:code:`@last_visited_cmd_output`.
''') ''')
egr() # }}} egr() # }}}

View File

@@ -17,7 +17,8 @@ class GetText(RemoteCommand):
''' '''
match: The tab to focus match: The tab to focus
extent: One of :code:`screen`, :code:`last_cmd_output`, :code:`all`, or :code:`selection` extent: One of :code:`screen`, :code:`first_cmd_output_on_screen`, :code:`last_cmd_output`, \
:code:`last_visited_cmd_output`, :code:`all`, or :code:`selection`
ansi: Boolean, if True send ANSI formatting codes ansi: Boolean, if True send ANSI formatting codes
cursor: Boolean, if True send cursor position/style as ANSI codes cursor: Boolean, if True send cursor position/style as ANSI codes
wrap_markers: Boolean, if True add wrap markers to output wrap_markers: Boolean, if True add wrap markers to output
@@ -28,11 +29,14 @@ class GetText(RemoteCommand):
options_spec = MATCH_WINDOW_OPTION + '''\n options_spec = MATCH_WINDOW_OPTION + '''\n
--extent --extent
default=screen default=screen
choices=screen, all, selection, last_cmd_output choices=screen, all, selection, first_cmd_output_on_screen, last_cmd_output, last_visited_cmd_output
What text to get. The default of :code:`screen` means all text currently on the screen. What text to get. The default of :code:`screen` means all text currently on the screen.
:code:`all` means all the screen+scrollback and :code:`selection` means the :code:`all` means all the screen+scrollback and :code:`selection` means the
currently selected text. Finally, :code:`last_cmd_output` means the output of the last currently selected text. :code:`first_cmd_output_on_screen` means the output of the first
command that was run in the window, which requires :ref:`shell_integration` to be enabled. command that was run in the window on screen. :code:`last_cmd_output` means
the output of the last command that was run in the window. :code:`last_visited_cmd_output` means
the first command output below the last scrolled position via scroll_to_prompt. The last three
requires :ref:`shell_integration` to be enabled.
--ansi --ansi
@@ -73,11 +77,21 @@ If specified get text from the window this command is run in, rather than the ac
window = self.windows_for_match_payload(boss, window, payload_get)[0] window = self.windows_for_match_payload(boss, window, payload_get)[0]
if payload_get('extent') == 'selection': if payload_get('extent') == 'selection':
ans = window.text_for_selection() ans = window.text_for_selection()
elif payload_get('extent') == 'first_cmd_output_on_screen':
ans = window.first_cmd_output_on_screen(
as_ansi=bool(payload_get('ansi')),
add_wrap_markers=bool(payload_get('wrap_markers')),
)
elif payload_get('extent') == 'last_cmd_output': elif payload_get('extent') == 'last_cmd_output':
ans = window.last_cmd_output( ans = window.last_cmd_output(
as_ansi=bool(payload_get('ansi')), as_ansi=bool(payload_get('ansi')),
add_wrap_markers=bool(payload_get('wrap_markers')), add_wrap_markers=bool(payload_get('wrap_markers')),
) )
elif payload_get('extent') == 'last_visited_cmd_output':
ans = window.last_visited_cmd_output(
as_ansi=bool(payload_get('ansi')),
add_wrap_markers=bool(payload_get('wrap_markers')),
)
else: else:
ans = window.as_text( ans = window.as_text(
as_ansi=bool(payload_get('ansi')), as_ansi=bool(payload_get('ansi')),

View File

@@ -2646,6 +2646,7 @@ as_text_alternate(Screen *self, PyObject *args) {
typedef struct OutputOffset { typedef struct OutputOffset {
Screen *screen; Screen *screen;
int start; int start;
unsigned num_lines;
} OutputOffset; } OutputOffset;
static Line* static Line*
@@ -2654,37 +2655,122 @@ get_line_from_offset(void *x, int y) {
return range_line_(r->screen, r->start + y); return range_line_(r->screen, r->start + y);
} }
static bool
find_cmd_output(Screen *self, OutputOffset *oo, index_type start_y, unsigned int scrolled_by, int direction, bool on_screen_only) {
bool found_prompt = false, found_output = false, found_next_prompt = false;
int start = 0, end = 0;
int y1 = start_y - scrolled_by, y2 = y1;
const int upward_limit = -self->historybuf->count;
const int downward_limit = self->lines - 1;
const int screen_limit = -scrolled_by + downward_limit;
// find around
if (direction == 0) {
Line *line = range_line_(self, y1);
if (line->attrs.prompt_kind == PROMPT_START) {
found_prompt = true;
// change direction to downwards to find command output
direction = 1;
} else if (line->attrs.prompt_kind == OUTPUT_START) {
found_output = true; start = y1;
found_prompt = true;
// keep finding the first output start upwards
}
y1--; y2++;
}
// find upwards
if (direction <= 0) {
// find around: only needs to find the first output start
// find upwards: find prompt after the output, and the first output
while (y1 >= upward_limit) {
Line *line = range_line_(self, y1);
if (line->attrs.prompt_kind == PROMPT_START) {
if (direction == 0) {
// find around: stop at prompt start
start = y1 + 1;
break;
}
found_next_prompt = true; end = y1;
} else if (line->attrs.prompt_kind == OUTPUT_START) {
start = y1;
break;
}
y1--;
}
if (y1 < upward_limit) {
start = upward_limit;
} else {
// resizing screen can cause multiple consecutive output start lines,
// so find the first one
while (start > upward_limit) {
Line *line = range_line_(self, start - 1);
if (line->attrs.prompt_kind != OUTPUT_START) break;
start--;
}
}
found_output = true;
found_prompt = true;
}
// find downwards
if (direction >= 0) {
while (y2 <= downward_limit) {
if (on_screen_only && !found_output && y2 > screen_limit) break;
Line *line = range_line_(self, y2);
if (line->attrs.prompt_kind == PROMPT_START) {
if (!found_prompt) found_prompt = true;
else if (found_output && !found_next_prompt) {
found_next_prompt = true; end = y2;
break;
}
} else if (line->attrs.prompt_kind == OUTPUT_START && found_prompt && !found_output) {
found_output = true; start = y2;
}
y2++;
}
}
if (found_next_prompt) {
oo->num_lines = end - start;
} else if (found_output) {
oo->num_lines = (direction < 0 ? start_y : downward_limit) - start;
} else return false;
oo->start = start;
return true;
}
static PyObject*
first_cmd_output_on_screen(Screen *self, PyObject *args) {
if (self->linebuf != self->main_linebuf) return PyUnicode_FromString("");
OutputOffset oo = {.screen=self};
if (find_cmd_output(self, &oo, 0, self->scrolled_by, 1, true)) {
return as_text_generic(args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf);
}
return PyUnicode_FromString("");
}
static PyObject* static PyObject*
last_cmd_output(Screen *self, PyObject *args) { last_cmd_output(Screen *self, PyObject *args) {
if (self->linebuf != self->main_linebuf) return PyUnicode_FromString(""); if (self->linebuf != self->main_linebuf) return PyUnicode_FromString("");
OutputOffset oo = {.screen=self}; OutputOffset oo = {.screen=self};
unsigned num_lines = 0; if (find_cmd_output(self, &oo, self->cursor->y, self->scrolled_by, -1, false)) {
int prompt_pos = self->cursor->y, y = self->cursor->y; return as_text_generic(args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf);
const int limit = -self->historybuf->count;
while (y >= limit) {
Line *line = range_line_(self, y);
if (line->attrs.prompt_kind == PROMPT_START) prompt_pos = y;
if (line->attrs.prompt_kind == OUTPUT_START) {
oo.start = y;
num_lines = prompt_pos - y;
break;
}
y--;
} }
if (y < limit) { return PyUnicode_FromString("");
oo.start = limit; }
num_lines = prompt_pos - limit;
} else { static PyObject*
// resizing screen can cause multiple consecutive output start lines, last_visited_cmd_output(Screen *self, PyObject *args) {
// so find the first one if (self->linebuf != self->main_linebuf || self->last_visited_prompt_scrolled_by > self->historybuf->count) return PyUnicode_FromString("");
while (oo.start > limit) {
Line *line = range_line_(self, oo.start - 1); OutputOffset oo = {.screen=self};
if (line->attrs.prompt_kind != OUTPUT_START) break; if (find_cmd_output(self, &oo, 0, self->last_visited_prompt_scrolled_by, 0, false)) {
oo.start--; num_lines++; return as_text_generic(args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf);
}
} }
return as_text_generic(args, &oo, get_line_from_offset, num_lines, &self->as_ansi_buf); return PyUnicode_FromString("");
} }
@@ -3677,7 +3763,9 @@ static PyMethodDef methods[] = {
MND(as_text, METH_VARARGS) MND(as_text, METH_VARARGS)
MND(as_text_non_visual, METH_VARARGS) MND(as_text_non_visual, METH_VARARGS)
MND(as_text_alternate, METH_VARARGS) MND(as_text_alternate, METH_VARARGS)
MND(first_cmd_output_on_screen, METH_VARARGS)
MND(last_cmd_output, METH_VARARGS) MND(last_cmd_output, METH_VARARGS)
MND(last_visited_cmd_output, METH_VARARGS)
MND(tab, METH_NOARGS) MND(tab, METH_NOARGS)
MND(backspace, METH_NOARGS) MND(backspace, METH_NOARGS)
MND(linefeed, METH_NOARGS) MND(linefeed, METH_NOARGS)

View File

@@ -1041,11 +1041,21 @@ class Window:
) -> str: ) -> str:
return as_text(self.screen, as_ansi, add_history, add_wrap_markers, alternate_screen, add_cursor) return as_text(self.screen, as_ansi, add_history, add_wrap_markers, alternate_screen, add_cursor)
def first_cmd_output_on_screen(self, as_ansi: bool = False, add_wrap_markers: bool = False) -> str:
lines: List[str] = []
self.screen.first_cmd_output_on_screen(lines.append, as_ansi, add_wrap_markers)
return ''.join(lines)
def last_cmd_output(self, as_ansi: bool = False, add_wrap_markers: bool = False) -> str: def last_cmd_output(self, as_ansi: bool = False, add_wrap_markers: bool = False) -> str:
lines: List[str] = [] lines: List[str] = []
self.screen.last_cmd_output(lines.append, as_ansi, add_wrap_markers) self.screen.last_cmd_output(lines.append, as_ansi, add_wrap_markers)
return ''.join(lines) return ''.join(lines)
def last_visited_cmd_output(self, as_ansi: bool = False, add_wrap_markers: bool = False) -> str:
lines: List[str] = []
self.screen.last_visited_cmd_output(lines.append, as_ansi, add_wrap_markers)
return ''.join(lines)
@property @property
def cwd_of_child(self) -> Optional[str]: def cwd_of_child(self) -> Optional[str]:
return self.child.foreground_cwd or self.child.current_cwd return self.child.foreground_cwd or self.child.current_cwd
@@ -1075,6 +1085,16 @@ class Window:
cursor_on_screen = self.screen.scrolled_by < self.screen.lines - self.screen.cursor.y cursor_on_screen = self.screen.scrolled_by < self.screen.lines - self.screen.cursor.y
get_boss().display_scrollback(self, data['text'], data['input_line_number'], report_cursor=cursor_on_screen) get_boss().display_scrollback(self, data['text'], data['input_line_number'], report_cursor=cursor_on_screen)
@ac('cp', '''
Show output from the first shell command on screen in a pager like less
Requires :ref:`shell_integration` to work
''')
def show_first_command_output_on_screen(self) -> None:
text = self.first_cmd_output_on_screen(as_ansi=True, add_wrap_markers=True)
text = text.replace('\r\n', '\n').replace('\r', '\n')
get_boss().display_scrollback(self, text, title='First command output on screen', report_cursor=False)
@ac('cp', ''' @ac('cp', '''
Show output from the last shell command in a pager like less Show output from the last shell command in a pager like less
@@ -1085,6 +1105,16 @@ class Window:
text = text.replace('\r\n', '\n').replace('\r', '\n') text = text.replace('\r\n', '\n').replace('\r', '\n')
get_boss().display_scrollback(self, text, title='Last command output', report_cursor=False) get_boss().display_scrollback(self, text, title='Last command output', report_cursor=False)
@ac('cp', '''
Show the first output below the last scrolled position via scroll_to_prompt in a pager like less
Requires :ref:`shell_integration` to work
''')
def show_last_visited_command_output(self) -> None:
text = self.last_visited_cmd_output(as_ansi=True, add_wrap_markers=True)
text = text.replace('\r\n', '\n').replace('\r', '\n')
get_boss().display_scrollback(self, text, title='Last visited command output', report_cursor=False)
def paste_bytes(self, text: Union[str, bytes]) -> None: def paste_bytes(self, text: Union[str, bytes]) -> None:
# paste raw bytes without any processing # paste raw bytes without any processing
if isinstance(text, str): if isinstance(text, str):

View File

@@ -957,6 +957,11 @@ class TestScreen(BaseTest):
self.assertTrue(s.scroll_to_prompt()) self.assertTrue(s.scroll_to_prompt())
self.ae(str(s.visual_line(0)), '$ 1') self.ae(str(s.visual_line(0)), '$ 1')
def fco():
a = []
s.first_cmd_output_on_screen(a.append)
return ''.join(a)
def lco(): def lco():
a = [] a = []
s.last_cmd_output(a.append) s.last_cmd_output(a.append)
@@ -965,6 +970,7 @@ class TestScreen(BaseTest):
s = self.create_screen() s = self.create_screen()
s.draw('abcd'), s.index(), s.carriage_return() s.draw('abcd'), s.index(), s.carriage_return()
s.draw('12'), s.index(), s.carriage_return() s.draw('12'), s.index(), s.carriage_return()
self.ae(fco(), '')
self.ae(lco(), 'abcd\n12') self.ae(lco(), 'abcd\n12')
s = self.create_screen() s = self.create_screen()
mark_prompt(), s.draw('$ 0') mark_prompt(), s.draw('$ 0')
@@ -973,4 +979,5 @@ class TestScreen(BaseTest):
s.draw('abcd'), s.index(), s.carriage_return() s.draw('abcd'), s.index(), s.carriage_return()
s.draw('12'), s.index(), s.carriage_return() s.draw('12'), s.index(), s.carriage_return()
mark_prompt(), s.draw('$ 1') mark_prompt(), s.draw('$ 1')
self.ae(fco(), 'abcd\n12')
self.ae(lco(), 'abcd\n12') self.ae(lco(), 'abcd\n12')