A new action copy_ansi_to_clipboard to copy the current selection with ANSI formatting codes
Fixes #4665
This commit is contained in:
parent
ce8b0cf748
commit
1170cf474f
@ -93,6 +93,9 @@ Detailed list of changes
|
||||
- A new action to clear the screen up to the line containing the cursor, see
|
||||
:ac:`clear_terminal`
|
||||
|
||||
- A new action :ac:`copy_ansi_to_clipboard` to copy the current selection with ANSI formatting codes
|
||||
(:iss:`4665`)
|
||||
|
||||
- macOS: Fix a regression in the previous release that broke switching input
|
||||
sources by keyboard (:iss:`4621`)
|
||||
|
||||
|
||||
@ -1085,7 +1085,7 @@ class Screen:
|
||||
def has_selection(self) -> bool:
|
||||
pass
|
||||
|
||||
def text_for_selection(self) -> Tuple[str, ...]:
|
||||
def text_for_selection(self, ansi: bool, strip_trailing_spaces: bool) -> Tuple[str, ...]:
|
||||
pass
|
||||
|
||||
def is_rectangle_select(self) -> bool:
|
||||
|
||||
@ -43,8 +43,7 @@ requires :ref:`shell_integration` to be enabled.
|
||||
--ansi
|
||||
type=bool-set
|
||||
By default, only plain text is returned. If you specify this flag, the text will
|
||||
include the formatting escape codes for colors/bold/italic/etc. Note that when
|
||||
getting the current selection, the result is always plain text.
|
||||
include the formatting escape codes for colors/bold/italic/etc.
|
||||
|
||||
|
||||
--add-cursor
|
||||
@ -83,7 +82,7 @@ Clear the selection in the matched window, if any
|
||||
from kitty.window import CommandOutput
|
||||
window = self.windows_for_match_payload(boss, window, payload_get)[0]
|
||||
if payload_get('extent') == 'selection':
|
||||
ans = window.text_for_selection()
|
||||
ans = window.text_for_selection(as_ansi=payload_get('ansi'))
|
||||
elif payload_get('extent') == 'first_cmd_output_on_screen':
|
||||
ans = window.cmd_output(
|
||||
CommandOutput.first_on_screen,
|
||||
|
||||
@ -2403,8 +2403,25 @@ screen_apply_selection(Screen *self, void *address, size_t size) {
|
||||
self->url_ranges.last_rendered_count = self->url_ranges.count;
|
||||
}
|
||||
|
||||
static index_type
|
||||
limit_without_trailing_whitespace(const Line *line, index_type limit) {
|
||||
if (!limit) return limit;
|
||||
if (limit > line->xnum) limit = line->xnum;
|
||||
while (limit > 0) {
|
||||
CPUCell *cell = line->cpu_cells + limit - 1;
|
||||
if (cell->cc_idx[0]) break;
|
||||
switch(cell->ch) {
|
||||
case ' ': case '\t': case '\n': case '\r': case 0: break;
|
||||
default:
|
||||
return limit;
|
||||
}
|
||||
limit--;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
|
||||
text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) {
|
||||
IterationData idata;
|
||||
iteration_data(self, sel, &idata, -self->historybuf->count, false);
|
||||
int limit = MIN((int)self->lines, idata.y_limit);
|
||||
@ -2414,13 +2431,68 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
|
||||
Line *line = range_line_(self, y);
|
||||
XRange xr = xrange_for_iteration(&idata, y, line);
|
||||
char leading_char = (i > 0 && insert_newlines && !line->attrs.continued) ? '\n' : 0;
|
||||
PyObject *text = unicode_in_range(line, xr.x, xr.x_limit, true, leading_char, false);
|
||||
index_type x_limit = xr.x_limit;
|
||||
if (strip_trailing_whitespace) {
|
||||
index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
|
||||
if (new_limit != x_limit) {
|
||||
x_limit = new_limit;
|
||||
if (!x_limit) {
|
||||
PyObject *text = PyUnicode_FromString("\n");
|
||||
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); }
|
||||
PyTuple_SET_ITEM(ans, i, text);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
PyObject *text = unicode_in_range(line, xr.x, x_limit, true, leading_char, false);
|
||||
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); }
|
||||
PyTuple_SET_ITEM(ans, i, text);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) {
|
||||
IterationData idata;
|
||||
iteration_data(self, sel, &idata, -self->historybuf->count, false);
|
||||
int limit = MIN((int)self->lines, idata.y_limit);
|
||||
DECREF_AFTER_FUNCTION PyObject *ans = PyTuple_New(limit - idata.y + 1);
|
||||
DECREF_AFTER_FUNCTION PyObject *nl = PyUnicode_FromString("\n");
|
||||
if (!ans || !nl) return NULL;
|
||||
ANSIBuf output = {0};
|
||||
const GPUCell *prev_cell = NULL;
|
||||
for (int i = 0, y = idata.y; y < limit; y++, i++) {
|
||||
Line *line = range_line_(self, y);
|
||||
XRange xr = xrange_for_iteration(&idata, y, line);
|
||||
output.len = 0;
|
||||
if (i > 0 && insert_newlines && !line->attrs.continued) {
|
||||
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
|
||||
output.buf[output.len++] = '\n';
|
||||
}
|
||||
index_type x_limit = xr.x_limit;
|
||||
if (strip_trailing_whitespace) {
|
||||
index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
|
||||
if (new_limit != x_limit) {
|
||||
x_limit = new_limit;
|
||||
if (!x_limit) {
|
||||
PyTuple_SET_ITEM(ans, i, nl);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
line_as_ansi(line, &output, &prev_cell, xr.x, x_limit);
|
||||
PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
|
||||
if (!t) return NULL;
|
||||
PyTuple_SET_ITEM(ans, i, t);
|
||||
}
|
||||
PyObject *t = PyUnicode_FromFormat("%s%s", "\x1b[m", output.active_hyperlink_id ? "\x1b]8;;\x1b\\" : "");
|
||||
if (!t) return NULL;
|
||||
PyTuple_SET_ITEM(ans, PyTuple_GET_SIZE(ans) - 1, t);
|
||||
Py_INCREF(ans);
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
static hyperlink_id_type
|
||||
hyperlink_id_for_range(Screen *self, const Selection *sel) {
|
||||
IterationData idata;
|
||||
@ -2456,7 +2528,7 @@ current_url_text(Screen *self, PyObject *args UNUSED) {
|
||||
for (size_t i = 0; i < self->url_ranges.count; i++) {
|
||||
Selection *s = self->url_ranges.items + i;
|
||||
if (!is_selection_empty(s)) {
|
||||
PyObject *temp = text_for_range(self, s, false);
|
||||
PyObject *temp = text_for_range(self, s, false, false);
|
||||
if (!temp) goto error;
|
||||
PyObject *text = PyUnicode_Join(empty_string, temp);
|
||||
Py_CLEAR(temp);
|
||||
@ -3089,10 +3161,10 @@ copy_colors_from(Screen *self, Screen *other) {
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
text_for_selections(Screen *self, Selections *selections) {
|
||||
text_for_selections(Screen *self, Selections *selections, bool ansi, bool strip_trailing_whitespace) {
|
||||
PyObject *lines = NULL;
|
||||
for (size_t i = 0; i < selections->count; i++) {
|
||||
PyObject *temp = text_for_range(self, selections->items + i, true);
|
||||
PyObject *temp = ansi ? ansi_for_range(self, selections->items +i, true, strip_trailing_whitespace) : text_for_range(self, selections->items + i, true, strip_trailing_whitespace);
|
||||
if (temp) {
|
||||
if (lines) {
|
||||
lines = extend_tuple(lines, temp);
|
||||
@ -3106,13 +3178,17 @@ text_for_selections(Screen *self, Selections *selections) {
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
text_for_selection(Screen *self, PyObject *a UNUSED) {
|
||||
return text_for_selections(self, &self->selections);
|
||||
text_for_selection(Screen *self, PyObject *args) {
|
||||
int ansi = 0, strip_trailing_whitespace = 0;
|
||||
if (!PyArg_ParseTuple(args, "|pp", &ansi, &strip_trailing_whitespace)) return NULL;
|
||||
return text_for_selections(self, &self->selections, ansi, strip_trailing_whitespace);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
text_for_marked_url(Screen *self, PyObject *a UNUSED) {
|
||||
return text_for_selections(self, &self->url_ranges);
|
||||
text_for_marked_url(Screen *self, PyObject *args) {
|
||||
int ansi = 0, strip_trailing_whitespace = 0;
|
||||
if (!PyArg_ParseTuple(args, "|pp", &ansi, &strip_trailing_whitespace)) return NULL;
|
||||
return text_for_selections(self, &self->url_ranges, ansi, strip_trailing_whitespace);
|
||||
}
|
||||
|
||||
|
||||
@ -3853,8 +3929,8 @@ static PyMethodDef methods[] = {
|
||||
MND(detect_url, METH_VARARGS)
|
||||
MND(rescale_images, METH_NOARGS)
|
||||
MND(current_key_encoding_flags, METH_NOARGS)
|
||||
MND(text_for_selection, METH_NOARGS)
|
||||
MND(text_for_marked_url, METH_NOARGS)
|
||||
MND(text_for_selection, METH_VARARGS)
|
||||
MND(text_for_marked_url, METH_VARARGS)
|
||||
MND(is_rectangle_select, METH_NOARGS)
|
||||
MND(scroll, METH_VARARGS)
|
||||
MND(scroll_to_prompt, METH_VARARGS)
|
||||
|
||||
@ -1026,12 +1026,10 @@ class Window:
|
||||
self.show_cmd_output(CommandOutput.last_visited, 'Clicked command output')
|
||||
# }}}
|
||||
|
||||
def text_for_selection(self) -> str:
|
||||
lines = self.screen.text_for_selection()
|
||||
def text_for_selection(self, as_ansi: bool = False) -> str:
|
||||
sts = get_options().strip_trailing_spaces
|
||||
if sts == 'always' or (
|
||||
sts == 'smart' and not self.screen.is_rectangle_select()):
|
||||
return ''.join((ln.rstrip() or '\n') for ln in lines)
|
||||
strip_trailing_spaces = sts == 'always' or (sts == 'smart' and not self.screen.is_rectangle_select())
|
||||
lines = self.screen.text_for_selection(as_ansi, strip_trailing_spaces)
|
||||
return ''.join(lines)
|
||||
|
||||
def call_watchers(self, which: Iterable[Watcher], data: Dict[str, Any]) -> None:
|
||||
@ -1161,6 +1159,12 @@ class Window:
|
||||
if text:
|
||||
set_clipboard_string(text)
|
||||
|
||||
@ac('cp', 'Copy the selected text from the active window to the clipboard with ANSI formatting codes')
|
||||
def copy_ansi_to_clipboard(self) -> None:
|
||||
text = self.text_for_selection(as_ansi=True)
|
||||
if text:
|
||||
set_clipboard_string(text)
|
||||
|
||||
def encoded_key(self, key_event: KeyEvent) -> bytes:
|
||||
return encode_key_for_tty(
|
||||
key=key_event.key, shifted_key=key_event.shifted_key, alternate_key=key_event.alternate_key,
|
||||
|
||||
@ -530,6 +530,28 @@ class TestScreen(BaseTest):
|
||||
s.scroll(2, True)
|
||||
self.ae(s.text_for_selection(), expected)
|
||||
s.reset()
|
||||
s.draw('ab cd')
|
||||
s.start_selection(0, 0)
|
||||
s.update_selection(1, 3)
|
||||
self.ae(s.text_for_selection(), ('ab ', 'cd'))
|
||||
self.ae(s.text_for_selection(False, True), ('ab', 'cd'))
|
||||
s.reset()
|
||||
s.draw('ab cd')
|
||||
s.start_selection(0, 0)
|
||||
s.update_selection(3, 4)
|
||||
self.ae(s.text_for_selection(), ('ab ', ' ', 'cd'))
|
||||
self.ae(s.text_for_selection(False, True), ('ab', '\n', 'cd'))
|
||||
s.reset()
|
||||
s.draw('a')
|
||||
s.select_graphic_rendition(32)
|
||||
s.draw('b')
|
||||
s.select_graphic_rendition(39)
|
||||
s.draw('c xy')
|
||||
s.start_selection(0, 0)
|
||||
s.update_selection(1, 3)
|
||||
self.ae(s.text_for_selection(), ('abc ', 'xy'))
|
||||
self.ae(s.text_for_selection(True), ('a\x1b[32mb\x1b[39mc ', 'xy', '\x1b[m'))
|
||||
self.ae(s.text_for_selection(True, True), ('a\x1b[32mb\x1b[39mc', 'xy', '\x1b[m'))
|
||||
|
||||
def test_soft_hyphen(self):
|
||||
s = self.create_screen()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user