mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-28 08:55:06 +08:00
feat(terminal): copy rejoins the lines Claude Code wrapped itself (#1141)
* feat(terminal): copy rejoins the lines Claude Code wrapped itself Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(terminal): keep copy_unwrap fixtures LF on Windows checkouts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(settings): build the join-wrapped row with its Selection & Clipboard siblings --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
17 files changed
+771
-1
No files matched your search
@@ -3,3 +3,6 @@
|
||||
# release time. Say so explicitly so no future line-ending rule can rewrite it.
|
||||
assets/windows/conpty/**/*.dll binary
|
||||
assets/windows/conpty/**/*.exe binary
|
||||
|
||||
# include_str! fixtures are compared byte-for-byte; autocrlf would add \r on Windows.
|
||||
src/terminal/view/fixtures/*.txt -text
|
||||
@@ -381,6 +381,10 @@ pub struct Config {
|
||||
#[serde(default, deserialize_with = "de_lenient")]
|
||||
pub mouse_zoom_modifier: MouseZoomModifier,
|
||||
pub clipboard_trim_trailing_spaces: bool,
|
||||
/// In a Claude Code pane, copy joins the rows Claude wrapped itself, so a
|
||||
/// paragraph pastes as one line. ⌘⌥C and Copy Raw copy the rows as is.
|
||||
#[serde(default)]
|
||||
pub copy_join_wrapped: bool,
|
||||
pub copy_on_select: bool,
|
||||
/// Optional HTTP/SOCKS proxy for tty7's *own* update checks and release
|
||||
/// downloads, and (HTTP only) the mobile gateway's relay; when set it is
|
||||
@@ -854,6 +858,7 @@ impl Default for Config {
|
||||
mouse_reporting: true,
|
||||
mouse_zoom_modifier: MouseZoomModifier::default(),
|
||||
clipboard_trim_trailing_spaces: false,
|
||||
copy_join_wrapped: false,
|
||||
copy_on_select: false,
|
||||
http_proxy: None,
|
||||
smart_select: true,
|
||||
|
||||
@@ -145,6 +145,7 @@ their id from the file name. [More about themes →](/customization/themes)
|
||||
| `word_separators` | string | see below | Characters that end a word. Used when smart selection is off. |
|
||||
| `copy_on_select` | bool | `false` | |
|
||||
| `clipboard_trim_trailing_spaces` | bool | `false` | |
|
||||
| `copy_join_wrapped` | bool | `false` | In a Claude Code pane, copy rejoins the lines Claude wrapped at the pane's edge, so a paragraph pastes as one line; other panes copy as shown. ⌘⌥C and Copy Raw copy the rows as shown. |
|
||||
| `macos_option_as_alt` | bool | `false` | <kbd>⌥</kbd>+key sends the escape chord instead of typing a special character. |
|
||||
| `keybindings` | object | `{}` | `{"SplitRight": "cmd-d"}`. [Syntax →](/customization/keybindings) |
|
||||
| `keybinding_preset` | string | `"default"` | Or `"tmux"`. |
|
||||
|
||||
+11
-1
@@ -35,6 +35,8 @@ use crate::core::shell_quote::quote_for_shell;
|
||||
use crate::daemon::protocol::{RemoteContext, ShellSpec};
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
|
||||
mod copy_unwrap;
|
||||
|
||||
pub(super) const GRID_PAD_X: f32 = 8.;
|
||||
pub(super) const GRID_PAD_Y: f32 = 4.;
|
||||
|
||||
@@ -2638,6 +2640,10 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
|
||||
if m.platform && m.alt && !m.control && ks.key == "c" && self.copy_raw(cx) {
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
if m.platform && !m.control && !m.alt {
|
||||
match self.handle_cmd_shortcut(ks, window, cx) {
|
||||
CmdKey::Consumed => {
|
||||
@@ -3554,7 +3560,11 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
pub fn copy_selection(&mut self, cx: &mut Context<Self>) {
|
||||
let text = self.terminal.term.lock().selection_to_string();
|
||||
self.copy_selection_as(false, cx);
|
||||
}
|
||||
|
||||
fn copy_selection_as(&mut self, raw: bool, cx: &mut Context<Self>) {
|
||||
let text = copy_unwrap::selection_text(self, raw, cx);
|
||||
if let Some(mut text) = text {
|
||||
if cx.global::<Config>().clipboard_trim_trailing_spaces {
|
||||
text = trim_trailing_spaces(&text);
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
//! Copy rejoins the rows a program wrapped itself. Claude Code (Ink) wraps at
|
||||
//! the pane's width by writing CRLF plus the block's indent, so the grid never
|
||||
//! sets WRAPLINE on those rows and a plain copy keeps every break. The rule
|
||||
//! was measured on 16 captures of Claude panes; `fixtures/` holds three.
|
||||
|
||||
use alacritty_terminal::Term;
|
||||
use alacritty_terminal::grid::Dimensions;
|
||||
use alacritty_terminal::index::{Column, Line, Point};
|
||||
use alacritty_terminal::term::cell::Flags;
|
||||
use gpui::Context;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use super::TerminalView;
|
||||
use crate::core::cli_agent::CLIAgent;
|
||||
use crate::core::config::Config;
|
||||
|
||||
/// One line as the copy sees it: the grid's rows up to the next one without
|
||||
/// WRAPLINE, read from column 0 whatever the selection's start.
|
||||
struct Row {
|
||||
text: String,
|
||||
/// Where the last physical row's text ends, in cells.
|
||||
end: usize,
|
||||
}
|
||||
|
||||
/// What `copy_selection` copies: the selection with Claude's wrapped rows
|
||||
/// joined, in a Claude pane with `copy_join_wrapped` on and `raw` off. Every
|
||||
/// other program's output is copied as the grid holds it: `ls` columns,
|
||||
/// separators and an editor's own wraps all look like Claude's.
|
||||
pub(super) fn selection_text(
|
||||
view: &TerminalView,
|
||||
raw: bool,
|
||||
cx: &Context<TerminalView>,
|
||||
) -> Option<String> {
|
||||
let join = !raw
|
||||
&& cx.global::<Config>().copy_join_wrapped
|
||||
&& view.terminal.foreground_agent() == Some(CLIAgent::Claude);
|
||||
let term = view.terminal.term.lock();
|
||||
if join {
|
||||
joined(&term)
|
||||
} else {
|
||||
term.selection_to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The selection's text with app-wrapped rows joined. Any doubt (block
|
||||
/// selection, a row count that doesn't line up) copies raw.
|
||||
fn joined<T>(term: &Term<T>) -> Option<String> {
|
||||
let raw = term.selection_to_string()?;
|
||||
let Some(range) = term.selection.as_ref().and_then(|s| s.to_range(term)) else {
|
||||
return Some(raw);
|
||||
};
|
||||
if range.is_block {
|
||||
return Some(raw);
|
||||
}
|
||||
let rows = grid_rows(term, range.start.line, range.end.line);
|
||||
Some(rejoin(&raw, &rows, term.columns()))
|
||||
}
|
||||
|
||||
/// `raw` split at its newlines, which must be one per row boundary.
|
||||
fn rejoin(raw: &str, rows: &[Row], cols: usize) -> String {
|
||||
// A Lines selection ends in a newline its rows don't account for.
|
||||
let (body, tail) = match raw.strip_suffix('\n') {
|
||||
Some(body) if body.split('\n').count() == rows.len() => (body, "\n"),
|
||||
_ => (raw, ""),
|
||||
};
|
||||
let parts: Vec<&str> = body.split('\n').collect();
|
||||
if parts.len() != rows.len() {
|
||||
return raw.to_string();
|
||||
}
|
||||
apply(&parts, &joins(rows, cols)) + tail
|
||||
}
|
||||
|
||||
fn grid_rows<T>(term: &Term<T>, first: Line, last: Line) -> Vec<Row> {
|
||||
let cols = term.columns();
|
||||
let mut rows = Vec::new();
|
||||
let mut text = String::new();
|
||||
let mut line = first;
|
||||
while line <= last {
|
||||
let grid_row = &term.grid()[line];
|
||||
text += &term.bounds_to_string(
|
||||
Point::new(line, Column(0)),
|
||||
Point::new(line, Column(cols - 1)),
|
||||
);
|
||||
let wraps = grid_row[Column(cols - 1)].flags.contains(Flags::WRAPLINE);
|
||||
if !wraps || line == last {
|
||||
let end = (0..cols)
|
||||
.rev()
|
||||
.find(|&c| {
|
||||
let cell = &grid_row[Column(c)];
|
||||
cell.c != ' ' && !cell.flags.contains(Flags::LEADING_WIDE_CHAR_SPACER)
|
||||
})
|
||||
.map_or(0, |c| {
|
||||
c + 1 + grid_row[Column(c)].flags.contains(Flags::WIDE_CHAR) as usize
|
||||
});
|
||||
rows.push(Row {
|
||||
text: std::mem::take(&mut text).trim_end_matches('\n').to_string(),
|
||||
end,
|
||||
});
|
||||
}
|
||||
line += 1;
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
/// What goes between two rows.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Sep {
|
||||
Newline,
|
||||
Space,
|
||||
/// Ink broke inside a word: a row that is one word, or wide (CJK) text,
|
||||
/// which wraps between any two characters.
|
||||
Nothing,
|
||||
}
|
||||
|
||||
/// For each boundary between `rows[i]` and `rows[i + 1]`, what goes there.
|
||||
fn joins(rows: &[Row], cols: usize) -> Vec<Sep> {
|
||||
let mut out = Vec::with_capacity(rows.len().saturating_sub(1));
|
||||
// (leading spaces, text column) of the row that opened the current run.
|
||||
let mut opened: Option<(usize, usize)> = None;
|
||||
let mut fenced = false;
|
||||
// The indent of a `⎿` tool result whose rows we are inside.
|
||||
let mut tool: Option<usize> = None;
|
||||
for pair in rows.windows(2) {
|
||||
let (a, b) = (&pair[0], &pair[1]);
|
||||
let a_trim = a.text.trim();
|
||||
if a_trim.starts_with("```") {
|
||||
fenced = !fenced;
|
||||
}
|
||||
if !a_trim.is_empty() {
|
||||
if tool.is_some_and(|t| lead(&a.text) <= t) {
|
||||
tool = None;
|
||||
}
|
||||
if a_trim.starts_with('⎿') {
|
||||
tool = Some(lead(&a.text));
|
||||
}
|
||||
}
|
||||
let (lo, hi) = *opened.get_or_insert_with(|| (lead(&a.text), text_col(&a.text)));
|
||||
let b_trim = b.text.trim();
|
||||
let word = cells(b_trim.split(' ').next().unwrap_or(""));
|
||||
// Ink breaks one cell early when the next word would land exactly on
|
||||
// the edge, so a word that just fits still counts as not fitting.
|
||||
// A word too long for any row tells nothing about where `a` ended.
|
||||
let full = a.end + 1 + word >= cols && lead(&b.text) + word <= cols;
|
||||
let join = full
|
||||
&& !fenced
|
||||
&& tool.is_none()
|
||||
&& !b_trim.is_empty()
|
||||
&& b_trim
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii() || c.is_alphabetic())
|
||||
&& !a_trim.ends_with('…')
|
||||
&& (lo..=hi).contains(&lead(&b.text))
|
||||
&& text_col(&b.text) == lead(&b.text)
|
||||
&& !structural(&a.text)
|
||||
&& !structural(&b.text);
|
||||
let wide = |c: Option<char>| c.is_some_and(|c| c.width() == Some(2));
|
||||
// A one-word row that fills the row is a word Ink cut in two: its hard
|
||||
// breaks fill to the edge, its word wraps stop short of it.
|
||||
let one_word = a.end >= cols && !a.text[text_byte(&a.text)..].trim_end().contains(' ');
|
||||
out.push(if !join {
|
||||
opened = None;
|
||||
Sep::Newline
|
||||
} else if one_word || (wide(a_trim.chars().last()) && wide(b_trim.chars().next())) {
|
||||
Sep::Nothing
|
||||
} else {
|
||||
Sep::Space
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn apply(parts: &[&str], seps: &[Sep]) -> String {
|
||||
let mut out = parts[0].to_string();
|
||||
for (part, &sep) in parts[1..].iter().zip(seps) {
|
||||
if sep == Sep::Newline {
|
||||
out.push('\n');
|
||||
out.push_str(part);
|
||||
continue;
|
||||
}
|
||||
out.truncate(out.trim_end().len());
|
||||
if sep == Sep::Space {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push_str(part.trim_start());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Cells `text` takes in the grid, which sizes each char on its own: VS16
|
||||
/// adds nothing, so `⚠️` is one cell, not the two `str` width gives it.
|
||||
fn cells(text: &str) -> usize {
|
||||
text.chars().map(|c| c.width().unwrap_or(0)).sum()
|
||||
}
|
||||
|
||||
fn lead(text: &str) -> usize {
|
||||
text.len() - text.trim_start_matches(' ').len()
|
||||
}
|
||||
|
||||
/// Where a row's text starts once a list or block marker is skipped: `- `,
|
||||
/// `1. `, or a run of symbols and a space (`⏺ `, `⚠️ `, `◯ `).
|
||||
fn text_col(text: &str) -> usize {
|
||||
let at = text_byte(text);
|
||||
lead(text) + cells(&text[lead(text)..at])
|
||||
}
|
||||
|
||||
/// The byte where a row's text starts, past its indent and any marker.
|
||||
fn text_byte(text: &str) -> usize {
|
||||
let indent = lead(text);
|
||||
let rest = &text[indent..];
|
||||
let Some((marker, _)) = rest.split_once(' ') else {
|
||||
return indent;
|
||||
};
|
||||
let digits = marker.trim_end_matches(['.', ')']);
|
||||
let is_marker = matches!(marker, "-" | "*" | "+" | "•")
|
||||
|| (marker.len() > digits.len()
|
||||
&& (1..=3).contains(&digits.len())
|
||||
&& digits.bytes().all(|b| b.is_ascii_digit()))
|
||||
|| (!marker.is_empty()
|
||||
&& marker
|
||||
.chars()
|
||||
.all(|c| !c.is_ascii() && !c.is_alphanumeric()));
|
||||
if is_marker {
|
||||
indent + marker.len() + 1
|
||||
} else {
|
||||
indent
|
||||
}
|
||||
}
|
||||
|
||||
/// Box drawing, block elements, a raw table row, or a prompt: never joined.
|
||||
fn structural(text: &str) -> bool {
|
||||
let t = text.trim();
|
||||
text.chars().any(|c| ('\u{2500}'..='\u{259f}').contains(&c))
|
||||
|| ["|", "$ ", "> ", "❯ ", "```", "⎿"]
|
||||
.iter()
|
||||
.any(|p| t.starts_with(p))
|
||||
}
|
||||
|
||||
impl TerminalView {
|
||||
/// ⌘⌥C and Copy Raw: Copy, with the grid's rows as it holds them.
|
||||
pub(crate) fn copy_raw(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
let editor_selection = self.input_active() && self.cmd.selected_text().is_some();
|
||||
if editor_selection || !self.has_selection() {
|
||||
return self.copy_contextual(false, cx);
|
||||
}
|
||||
self.copy_selection_as(true, cx);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use alacritty_terminal::event::VoidListener;
|
||||
use alacritty_terminal::index::Side;
|
||||
use alacritty_terminal::selection::{Selection, SelectionType};
|
||||
|
||||
/// Rows the way `tty7 capture --plain` prints them; a capture line longer
|
||||
/// than the pane was soft-wrapped, so its last row is the remainder.
|
||||
fn unwrap(text: &str, cols: usize) -> String {
|
||||
let rows: Vec<Row> = text
|
||||
.split('\n')
|
||||
.map(|p| {
|
||||
let w = cells(p.trim_end());
|
||||
Row {
|
||||
text: p.to_string(),
|
||||
end: if w > cols { (w - 1) % cols + 1 } else { w },
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
rejoin(text, &rows, cols)
|
||||
}
|
||||
|
||||
fn term(cols: usize, lines: usize, input: &str) -> Term<VoidListener> {
|
||||
let mut term = Term::new(
|
||||
alacritty_terminal::term::Config::default(),
|
||||
&crate::terminal::size::TermSize::new(cols, lines),
|
||||
VoidListener,
|
||||
);
|
||||
alacritty_terminal::vte::ansi::Processor::<alacritty_terminal::vte::ansi::StdSyncHandler>::new()
|
||||
.advance(&mut term, input.as_bytes());
|
||||
term
|
||||
}
|
||||
|
||||
fn select(
|
||||
term: &mut Term<VoidListener>,
|
||||
ty: SelectionType,
|
||||
from: (i32, usize),
|
||||
to: (i32, usize),
|
||||
) {
|
||||
let mut sel = Selection::new(ty, Point::new(Line(from.0), Column(from.1)), Side::Left);
|
||||
sel.update(Point::new(Line(to.0), Column(to.1)), Side::Right);
|
||||
term.selection = Some(sel);
|
||||
}
|
||||
|
||||
const C111: &str = include_str!("fixtures/copy_unwrap_111.txt");
|
||||
const C172: &str = include_str!("fixtures/copy_unwrap_172.txt");
|
||||
const C_CODE: &str = include_str!("fixtures/copy_unwrap_code.txt");
|
||||
|
||||
fn has_line(out: &str, line: &str) -> bool {
|
||||
out.lines().any(|l| l == line)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped_prose_joins() {
|
||||
let out = unwrap(C111, 111);
|
||||
assert!(has_line(
|
||||
&out,
|
||||
"⏺ #84 is now based on the new patch-stack main-niu, and GitHub reports it can merge cleanly. Its code is the same as what passed review: the only change is that its notes moved into the hotkey-window feature page. Its tests pass locally."
|
||||
));
|
||||
assert!(has_line(
|
||||
&out,
|
||||
"※ recap: The goal is for tty7 to show the hotkey window on launch, instead of creating a new workspace, when it's your only workspace. That's PR #84, which passed review and is waiting on CI. Next, it merges and tty7 reloads on its own, and I'll check your sessions survive. (disable recaps in /config)"
|
||||
));
|
||||
// A system line Claude continues at column 0.
|
||||
assert!(has_line(
|
||||
&out,
|
||||
"⏺ Background command \"Merge #84 once CI passes (network-error tolerant), then reload tty7\" completed (exit code 0)"
|
||||
));
|
||||
let out = unwrap(C172, 172);
|
||||
assert!(has_line(
|
||||
&out,
|
||||
"※ recap: Goal: clean up shuck findings in your dotfiles, most-used first; the shell startup files are done and committed locally. Next, pick Z1, Z2 or Z3 for the no-op `unset GLOBAL_RCS` line, and say \"push\" for the 19 unpushed commits. (disable recaps in /config)"
|
||||
));
|
||||
// `&&/||` in prose is not a table.
|
||||
assert!(out.contains(
|
||||
"fixing &&/|| logic into proper if/else, correcting a comment-continuation bug, adding"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reported_paragraph_joins() {
|
||||
let rows = "⏺ If the Theme field won't change or the popup comes back after they pick it, send me a screenshot. That would\n be a bug.";
|
||||
assert_eq!(
|
||||
unwrap(rows, 111),
|
||||
"⏺ If the Theme field won't change or the popup comes back after they pick it, send me a screenshot. That would be a bug."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_items_keep_their_newlines() {
|
||||
let out = unwrap(C111, 111);
|
||||
assert!(out.contains(" Here's how to try it:\n 1. Close every workspace except the hotkey one.\n 2. Quit tty7 and open it again.\n"));
|
||||
let bullets = " - Rule location: the rule is .claude/rules/app-layout.md, plus its Cursor copy and a pointer from the backend\n skill.\n - What it says: nothing new goes under <app>/services/, and other apps import only <app>.public and\n <app>.models.";
|
||||
assert_eq!(
|
||||
unwrap(bullets, 111),
|
||||
" - Rule location: the rule is .claude/rules/app-layout.md, plus its Cursor copy and a pointer from the backend skill.\n - What it says: nothing new goes under <app>/services/, and other apps import only <app>.public and <app>.models."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_tables_and_tool_output_never_join() {
|
||||
let out = unwrap(C111, 111);
|
||||
// A box drawn round a summary.
|
||||
assert!(out.contains(" │ joins when ... the line runs to the right edge (the next word wouldn't\n │ have fit)"));
|
||||
// A tool call's command and its char-wrapped output stay as the grid has them.
|
||||
assert!(out.contains("tasks/befky\n 62qi.output;"));
|
||||
assert!(out.contains(
|
||||
"https://github.com/NorthIsUp/tty7/actions/runs/369394730\n 36/job/110627346251"
|
||||
));
|
||||
assert!(out.contains("panicked at crates/tty7\n -core/src/host/server.rs"));
|
||||
let fenced = "```\nlet joined = rows.iter().map(|r| r.text.as_str()).collect::<Vec<_>>().join(\" \"); // a long line of code\nthat continues\n```";
|
||||
let code_width = cells(fenced.lines().nth(1).unwrap());
|
||||
assert_eq!(unwrap(fenced, code_width), fenced);
|
||||
let table = "| a long table cell that runs right up to the edge of the pane, wider than the |\nrest |";
|
||||
assert_eq!(unwrap(table, 80), table);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_short_row_before_an_indented_row_does_not_join() {
|
||||
let out = unwrap(C111, 111);
|
||||
assert!(out.contains(" Here's how to try it:\n 1."));
|
||||
let rows = " Still open on your side: rotate the key.\n Then rerun the push.";
|
||||
assert_eq!(unwrap(rows, 111), rows);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_narrow_pane_joins_the_same() {
|
||||
// The reported paragraph and a list item, as Ink wraps them at 52 columns.
|
||||
let rows = "⏺ If the Theme field won't change or the popup comes\n back after they pick it, send me a screenshot.\n That would be a bug.\n\n 1. Close every workspace except the hotkey one,\n then quit tty7 and open it again.\n 2. Quit tty7.";
|
||||
assert_eq!(
|
||||
unwrap(rows, 52),
|
||||
"⏺ If the Theme field won't change or the popup comes back after they pick it, send me a screenshot. That would be a bug.\n\n 1. Close every workspace except the hotkey one, then quit tty7 and open it again.\n 2. Quit tty7."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_grid_selection_joins_from_a_mid_row_start() {
|
||||
let soft = "a".repeat(45);
|
||||
let mut term = term(
|
||||
40,
|
||||
6,
|
||||
&format!(
|
||||
"⏺ If the Theme field won't change or\r\n the popup comes back after they pick\r\n it.\r\n{soft}"
|
||||
),
|
||||
);
|
||||
select(&mut term, SelectionType::Simple, (0, 2), (4, 39));
|
||||
assert_eq!(
|
||||
joined(&term).unwrap(),
|
||||
format!(
|
||||
"If the Theme field won't change or the popup comes back after they pick it.\n{soft}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_text_joins_without_a_space() {
|
||||
// Ten CJK chars fill 20 cells; Ink wraps between any two.
|
||||
let mut term = term(20, 3, "这是一个很长的中文句\r\n继续写完。");
|
||||
select(&mut term, SelectionType::Simple, (0, 0), (1, 19));
|
||||
assert_eq!(joined(&term).unwrap(), "这是一个很长的中文句继续写完。");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_on_one_side_only_keeps_the_space() {
|
||||
let emoji = format!(" {} 🎉\n Next steps", "w".repeat(36));
|
||||
assert_eq!(
|
||||
unwrap(&emoji, 42),
|
||||
format!(" {} 🎉 Next steps", "w".repeat(36))
|
||||
);
|
||||
let mixed = format!(" {} 詳しくは\n README を", "w".repeat(30));
|
||||
assert_eq!(
|
||||
unwrap(&mixed, 42),
|
||||
format!(" {} 詳しくは README を", "w".repeat(30))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_selection_copies_raw() {
|
||||
let mut term = term(
|
||||
40,
|
||||
3,
|
||||
"⏺ If the Theme field won't change or\r\n the popup comes back",
|
||||
);
|
||||
select(&mut term, SelectionType::Block, (0, 0), (1, 39));
|
||||
assert_eq!(joined(&term), term.selection_to_string());
|
||||
assert!(joined(&term).unwrap().contains("or\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lines_selection_keeps_its_trailing_newline() {
|
||||
let mut term = term(
|
||||
40,
|
||||
3,
|
||||
"⏺ If the Theme field won't change or\r\n the popup comes back",
|
||||
);
|
||||
select(&mut term, SelectionType::Lines, (0, 0), (1, 0));
|
||||
assert_eq!(
|
||||
joined(&term).unwrap(),
|
||||
"⏺ If the Theme field won't change or the popup comes back\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_that_dont_line_up_with_the_text_copy_raw() {
|
||||
let rows = [Row {
|
||||
text: "x".repeat(40),
|
||||
end: 40,
|
||||
}];
|
||||
let raw = format!("{}\nmore", "x".repeat(40));
|
||||
assert_eq!(rejoin(&raw, &rows, 40), raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_one_word_row_at_the_edge_joins_without_a_space() {
|
||||
let url = "https://github.com/NorthIsUp/tty7/actions/runs/36939473036";
|
||||
let (head, tail) = url.split_at(40);
|
||||
let rows = format!(" {head}\n {tail} is the run.");
|
||||
assert_eq!(unwrap(&rows, 42), format!(" {url} is the run."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_vs16_emoji_is_one_cell() {
|
||||
assert_eq!(cells("⚠️"), 1);
|
||||
// 2 + "⚠️ " (2 cells) + 37 = 41 cells: the next word would not fit at 44.
|
||||
let rows = format!(" ⚠️ {}\n more words", "w".repeat(37));
|
||||
assert_eq!(
|
||||
unwrap(&rows, 44),
|
||||
format!(" ⚠️ {} more words", "w".repeat(37))
|
||||
);
|
||||
assert_eq!(unwrap(&rows, 48), rows);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wrapped_code_line_joins_and_separate_code_lines_dont() {
|
||||
let out = unwrap(C_CODE, 111);
|
||||
assert!(has_line(
|
||||
&out,
|
||||
" ! themes env version history sampleorg/tty/testing | head -1; cd ~/src/sampleorg/Sample_1/.worktrees/sm-12345-tty-set-run/src/sampl && .venv/bin/python ~/tmp/set_sync_testing.py --apply"
|
||||
));
|
||||
assert!(out.contains(" ! cd /Users/adam/src/tty7-mdfix && mise run reload\n"));
|
||||
assert!(out.contains(
|
||||
" # themes/palette.py: drop `import random`\n from colour.utils.random import get_random_string\n ...\n"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_status_bar_never_joins() {
|
||||
let rows = " #14 #15 #16 #17 #18 #19 #20 #21 #24 #25 #27 #28 #29 #30 #31 #32 #33 #34 #35 #36 #37 #38 #39 #40 #41 #42 …\n ⏵⏵ auto mode on (shift+tab to cycle) · ← 19 agents";
|
||||
assert_eq!(unwrap(rows, 111), rows);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
⏺ #84 is now based on the new patch-stack main-niu, and GitHub reports it can merge cleanly. Its code is the
|
||||
same as what passed review: the only change is that its notes moved into the hotkey-window feature page. Its
|
||||
tests pass locally.
|
||||
|
||||
Once CI passes, it merges on its own and tty7 reloads on the new main-niu. That reload will be the first run
|
||||
of the restructured branch, so I'll check that your sessions survive it.
|
||||
|
||||
✻ Crunched for 28m 10s · done 4:13 PM
|
||||
|
||||
※ recap: The goal is for tty7 to show the hotkey window on launch, instead of creating a new workspace, when
|
||||
it's your only workspace. That's PR #84, which passed review and is waiting on CI. Next, it merges and tty7
|
||||
reloads on its own, and I'll check your sessions survive. (disable recaps in /config)
|
||||
|
||||
⏺ Background command "Merge #84 once CI passes (network-error tolerant), then reload tty7" completed (exit code
|
||||
0)
|
||||
|
||||
⏺ Bash(tail -8 /private/tmp/claude-501/-Users-adam-src-clawmux/f14681bd-4bf7-4808-b727-0cb6ed1a8c5b/tasks/befky
|
||||
62qi.output; tty7 status 2>&1 | head -5; tty7 agents 2>&…)
|
||||
⎿ build & test (aarch64-apple-darwin) fail 6m8s https://github.com/NorthIsUp/tty
|
||||
7/actions/runs/36939473036
|
||||
/job/110627383842
|
||||
host boundary pass 5s https://github.com/NorthIsUp/tty7/actions/runs/369394730
|
||||
36/job/110627346251
|
||||
… +13 lines (ctrl+o to expand)
|
||||
⎿ Allowed by auto mode classifier
|
||||
|
||||
⏺ Bash(gh api --allow-escape-sequences repos/NorthIsUp/tty7/actions/jobs/110627383842/logs 2>&1 | sed
|
||||
's/\x1b\[[0-9;]*m//g' | grep -E 'FAILED|panicked at|error\[|^.{29…)
|
||||
⎿ test host::server::pool_tests::closing_the_pool_drops_queued_work ... FAILED
|
||||
thread 'host::server::pool_tests::closing_the_pool_drops_queued_work' (43435) panicked at crates/tty7
|
||||
-core/src/host/server.rs:2419:9:
|
||||
… +6 lines (ctrl+o to expand)
|
||||
|
||||
⏺ The CI failure is in an unrelated thread-pool test that #84 doesn't touch and main's own CI passes, so it
|
||||
looks flaky. I'm re-running the failed job.
|
||||
|
||||
⏺ Bash(cd ~/src/tty7 && git diff ed406c19 24f7c1eb --stat | tail -2; gh run rerun 36939473036 -R NorthIsUp/tty7
|
||||
--failed && echo rerun-started)
|
||||
⎿ src/ui/windows.rs | 46 ++++++++-
|
||||
6 files changed, 255 insertions(+), 23 deletions(-)
|
||||
rerun-started
|
||||
⎿ Allowed by auto mode classifier
|
||||
|
||||
⏺ Bash(cd ~/src/tty7 && sleep 60; while :; do out=$(gh pr checks 84 -R NorthIsUp/tty7 --required 2>&1); rc=$?;
|
||||
if echo "$out" | grep -qiE 'pending|no checks|queued|err…)
|
||||
⎿ Running in the background (↓ to manage)
|
||||
⎿ Allowed by auto mode classifier
|
||||
|
||||
⏺ #84 isn't merged yet. Its CI failed in a tty7-core test (closing_the_pool_drops_queued_work) that #84 doesn't
|
||||
touch: the PR only changes 6 files in the hotkey window and window code. main-niu's own CI passes, so this
|
||||
looks like a flaky test. I've re-run the failed job. If it passes, #84 merges and tty7 reloads, and I'll
|
||||
check your 24 panes are still there afterwards.
|
||||
|
||||
If it fails again, I'll treat that test as a real problem and investigate it instead of re-running.
|
||||
|
||||
✻ Crunched for 2m 42s · done 4:22 PM
|
||||
|
||||
※ recap: I'm finishing the fix that opens the hotkey window instead of a blank workspace when it's your only
|
||||
workspace. Its CI hit an unrelated flaky test, so I re-ran it; once it passes, it merges and tty7 reloads on
|
||||
its own. (disable recaps in /config)
|
||||
|
||||
⏺ Background command "Wait for #84's rerun, merge on green, reload and confirm panes survived" completed (exit
|
||||
code 0)
|
||||
|
||||
Read 1 file (ctrl+o to expand)
|
||||
|
||||
⏺ ✅ #84 is merged, and tty7 has reloaded on the restructured main-niu. All 24 of your panes came through the
|
||||
reload. The re-run passed, so the earlier CI failure was a flaky test.
|
||||
|
||||
Here's how to try it:
|
||||
1. Close every workspace except the hotkey one.
|
||||
2. Quit tty7 and open it again.
|
||||
|
||||
It should open straight into the hotkey window without making a new workspace. Clicking the Dock icon should
|
||||
do the same.
|
||||
|
||||
✻ Sautéed for 27s · done 4:45 PM
|
||||
|
||||
※ recap: The goal was making tty7 restore your agents reliably. That's all merged and live now (#72–#84),
|
||||
including the hotkey window opening on its own when it's your only workspace. Next: quit and reopen tty7 to
|
||||
confirm the spinners and that it opens straight into the hotkey window.
|
||||
|
||||
❯ [Image #47] when coppying this i'd like it to not have a hard newline on the wrap
|
||||
|
||||
what coppies:
|
||||
|
||||
If the Theme field won't change or the popup comes back after they pick it, send me a screenshot. That would
|
||||
be a bug.
|
||||
|
||||
what should copy:
|
||||
|
||||
If the Theme field won't change or the popup comes back after they pick it, send me a screenshot. That would
|
||||
be a bug.
|
||||
⎿ [Image #47]
|
||||
|
||||
⏺ Agent(Unwrap app-wrapped lines on copy)
|
||||
⎿ Backgrounded agent (↓ to manage · ctrl+o to expand)
|
||||
|
||||
⏺ The newline comes from Claude Code itself. It wraps long lines by writing a real line break plus an indent,
|
||||
so to tty7 they look like two separate lines. Your terminal's own soft-wrap tracking never sees it.
|
||||
|
||||
I've started a subagent on the fix. On copy, tty7 will rejoin lines Claude wrapped:
|
||||
|
||||
┌──────────────────────────────
|
||||
│ 📦 copy joins Claude's wrapped lines
|
||||
├──────────────────────────────
|
||||
│ joins when ... the line runs to the right edge (the next word wouldn't
|
||||
│ have fit) and the next line continues at the same indent
|
||||
│ never joins .. code, tables, box drawing, prompts, list items
|
||||
│ setting ...... "Join wrapped lines when copying", on for you
|
||||
│ (off by default upstream)
|
||||
│ raw copy ..... a Copy Raw command in ⌘P, for the exact text
|
||||
│ 🧪 tuned on captures from your Claude tabs: prose, bullets, code, tables
|
||||
└──────────────────────────────
|
||||
|
||||
The subagent only reads your tabs to build test cases and doesn't type into them or take focus. Once the PR
|
||||
is up, I'll review it, merge it after CI passes, turn the setting on in your config, and reload tty7. Expect
|
||||
about an hour.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
⏺ I've laid out 12 fixes across the dotfiles, each to be its own commit — removing stray shebangs, fixing &&/|| logic into proper if/else, correcting a comment-continuation
|
||||
bug, adding a default in slow-cmd, using exact-match in path-fixup, scoping the loop in 10-focus-events, and adding suppression comments for the three intentional cases.
|
||||
I'll start committing these one at a time now.
|
||||
|
||||
✻ Cogitated for 23m 19s · done Wednesday 5:00 PM
|
||||
|
||||
※ recap: Goal: clean up shuck findings in your dotfiles, most-used first; the shell startup files are done and committed locally. Next, pick Z1, Z2 or Z3 for the no-op
|
||||
`unset GLOBAL_RCS` line, and say "push" for the 19 unpushed commits. (disable recaps in /config)
|
||||
|
||||
❯ continue
|
||||
|
||||
⏺ Continuing without changing GLOBAL_RCS and without pushing, since you haven't answered either. The next most-run code is the shared helpers that the scripts I've already
|
||||
fixed call into: argparse, usage2, log-*, subcommander, scale-it. Ranking the remaining flagged files by how many places call them:
|
||||
|
||||
⏺ Bash(git status --short; python3 - <<'EOF'
|
||||
import collections,os,re,subprocess…)
|
||||
⎿ Error: Exit code 1
|
||||
M home/.claude/settings.json
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 2, in <module>
|
||||
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/claude-501/sk3.txt'
|
||||
|
||||
⏺ Temp files were cleared since the last session. Regenerating the findings list and rerunning the ranking:
|
||||
|
||||
⏺ Bash(mkdir -p /tmp/claude-501 && cd /Users/adam/src/dotfiles && git log --oneline -1 && cmd=$(grep -o "shuck check --output-format concise[^\"]*" hk.pkl); MISE_QUIET…)
|
||||
⎿ Running in the background (↓ to manage)
|
||||
⎿ Loaded home/.claude/CLAUDE.md
|
||||
⎿ Loaded 5 skills from home/.claude/skills
|
||||
⎿ 5 skills available
|
||||
|
||||
⏺ The findings scan plus caller ranking is still running in the background. It went over the 5-minute foreground limit, probably because shuck is slower on a cold cache.
|
||||
I'll start on the top-ranked helper when it finishes.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
You didn't pick between U1 and U2, so I did U1: rewrote the PR title and body. The new title is feat(infra)!:
|
||||
source the app colours from themes set at boot.
|
||||
|
||||
The testing sync is a config-files write, which auto-mode won't let me do. It prints the rollback revision
|
||||
first, then key names only:
|
||||
|
||||
! themes env version history sampleorg/tty/testing | head -1; cd
|
||||
~/src/sampleorg/Sample_1/.worktrees/sm-12345-tty-set-run/src/sampl && .venv/bin/python
|
||||
~/tmp/set_sync_testing.py --apply
|
||||
|
||||
⏺ Background command "Rebuild and reload dev app with both fixes" was stopped because the system is running low
|
||||
on memory
|
||||
|
||||
⏺ Rebuild the dev app yourself when you're ready:
|
||||
|
||||
! cd /Users/adam/src/tty7-mdfix && mise run reload
|
||||
|
||||
Fixes for #1 and #2 (about 15 minutes with a test):
|
||||
|
||||
# themes/palette.py: drop `import random`
|
||||
from colour.utils.random import get_random_string
|
||||
...
|
||||
return get_random_string(6, allowed_chars="0123456789")
|
||||
|
||||
# settings/base.py
|
||||
PALETTE_SIZE_LIMITS = {"accent_colour": "1/55s/key,10/m/ip"}
|
||||
@@ -6800,6 +6800,11 @@ impl Tty7App {
|
||||
});
|
||||
}
|
||||
}
|
||||
CopyRaw => {
|
||||
if let Some(leaf) = self.focused_leaf(window, cx) {
|
||||
leaf.update(cx, |view, cx| view.copy_raw(cx));
|
||||
}
|
||||
}
|
||||
CutText => {
|
||||
if let Some(leaf) = self.focused_leaf(window, cx) {
|
||||
leaf.update(cx, |view, cx| {
|
||||
@@ -7128,6 +7133,9 @@ impl Tty7App {
|
||||
L10nKey::SettingsTrimTrailingSpaces => {
|
||||
self.set_clipboard_trim(defaults.clipboard_trim_trailing_spaces, cx)
|
||||
}
|
||||
L10nKey::SettingsCopyJoinWrapped => {
|
||||
self.update_config(cx, |c| c.copy_join_wrapped = defaults.copy_join_wrapped)
|
||||
}
|
||||
L10nKey::SettingsCopyOnSelect => self.set_copy_on_select(defaults.copy_on_select, cx),
|
||||
L10nKey::SettingsSmartSelection => self.set_smart_select(defaults.smart_select, cx),
|
||||
L10nKey::SettingsPromptEditor => self.set_prompt_editor(defaults.prompt_editor, cx),
|
||||
|
||||
@@ -2340,6 +2340,14 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::GitHubShowFullText => "Show full text",
|
||||
L10nKey::GitHubShowHiddenComments => "Show {count} more comments",
|
||||
L10nKey::GitHubShowAllReviewers => "Show all {count} reviewers",
|
||||
L10nKey::CmdCopyRaw => "Copy Raw",
|
||||
L10nKey::SettingsSearchCopyJoinWrappedKeywords => {
|
||||
"clipboard copy wrap unwrap join lines claude raw"
|
||||
}
|
||||
L10nKey::SettingsCopyJoinWrapped => "Join wrapped lines when copying",
|
||||
L10nKey::SettingsCopyJoinWrappedDesc => {
|
||||
"In a Claude Code pane, rejoin the lines Claude wrapped at the pane's edge, so a paragraph pastes as one line. ⌘⌥C or Copy Raw copies them as shown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2411,6 +2411,14 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::GitHubShowFullText => "全文を表示",
|
||||
L10nKey::GitHubShowHiddenComments => "ほか {count} 件のコメントを表示",
|
||||
L10nKey::GitHubShowAllReviewers => "{count} 人のレビュアーをすべて表示",
|
||||
L10nKey::CmdCopyRaw => "そのままコピー",
|
||||
L10nKey::SettingsSearchCopyJoinWrappedKeywords => {
|
||||
"クリップボード コピー 折り返し 行 結合 clipboard copy wrap unwrap join lines claude raw"
|
||||
}
|
||||
L10nKey::SettingsCopyJoinWrapped => "コピー時に折り返し行をつなげる",
|
||||
L10nKey::SettingsCopyJoinWrappedDesc => {
|
||||
"Claude Code のペインで、Claude が画面端で折り返した行をつなげ、段落を 1 行として貼り付けます。⌘⌥C または「そのままコピー」で表示どおりにコピーします"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1855,6 +1855,10 @@ l10n_keys! {
|
||||
GitHubShowFullText,
|
||||
GitHubShowHiddenComments,
|
||||
GitHubShowAllReviewers,
|
||||
CmdCopyRaw,
|
||||
SettingsCopyJoinWrapped,
|
||||
SettingsCopyJoinWrappedDesc,
|
||||
SettingsSearchCopyJoinWrappedKeywords,
|
||||
}
|
||||
|
||||
/// The source control strings that are translated but not yet displayed.
|
||||
|
||||
@@ -2435,6 +2435,14 @@ pub fn translate_ru(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::GitHubShowFullText => "Показать полный текст",
|
||||
L10nKey::GitHubShowHiddenComments => "Показать ещё комментарии ({count})",
|
||||
L10nKey::GitHubShowAllReviewers => "Показать всех рецензентов ({count})",
|
||||
L10nKey::CmdCopyRaw => "Копировать как есть",
|
||||
L10nKey::SettingsSearchCopyJoinWrappedKeywords => {
|
||||
"буфер обмена копирование перенос склеить строки claude clipboard copy wrap unwrap join lines raw"
|
||||
}
|
||||
L10nKey::SettingsCopyJoinWrapped => "Склеивать перенесённые строки при копировании",
|
||||
L10nKey::SettingsCopyJoinWrappedDesc => {
|
||||
"В панели Claude Code склеивать строки, которые Claude перенёс у края панели, чтобы абзац вставлялся одной строкой. ⌘⌥C или «Копировать как есть» копируют их как на экране"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2185,6 +2185,14 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::GitHubShowFullText => "展开全文",
|
||||
L10nKey::GitHubShowHiddenComments => "显示另外 {count} 条评论",
|
||||
L10nKey::GitHubShowAllReviewers => "显示全部 {count} 位审查人",
|
||||
L10nKey::CmdCopyRaw => "原样复制",
|
||||
L10nKey::SettingsSearchCopyJoinWrappedKeywords => {
|
||||
"剪贴板 复制 折行 合并 clipboard copy wrap unwrap join lines claude raw"
|
||||
}
|
||||
L10nKey::SettingsCopyJoinWrapped => "复制时合并折行",
|
||||
L10nKey::SettingsCopyJoinWrappedDesc => {
|
||||
"在 Claude Code 窗格中,将 Claude 在窗格边缘折行的内容重新合并,段落粘贴为一行。⌘⌥C 或“原样复制”按显示内容复制"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ pub enum CommandKind {
|
||||
FindNext,
|
||||
FindPrevious,
|
||||
CopyText,
|
||||
CopyRaw,
|
||||
CutText,
|
||||
PasteText,
|
||||
SelectAllText,
|
||||
@@ -248,6 +249,7 @@ impl CommandKind {
|
||||
FindNext => "find-next",
|
||||
FindPrevious => "find-previous",
|
||||
CopyText => "copy",
|
||||
CopyRaw => "copy-raw",
|
||||
CutText => "cut",
|
||||
PasteText => "paste",
|
||||
SelectAllText => "select-all",
|
||||
@@ -326,6 +328,7 @@ impl CommandKind {
|
||||
|spec: &str| -> Option<String> { cfg!(target_os = "macos").then(|| spec.to_string()) };
|
||||
match self {
|
||||
CopyText => return inline("secondary-c"),
|
||||
CopyRaw => return inline("secondary-alt-c"),
|
||||
CutText => return inline("secondary-x"),
|
||||
PasteText => return inline("secondary-v"),
|
||||
SelectAllText => return inline("secondary-a"),
|
||||
@@ -440,6 +443,7 @@ impl CommandKind {
|
||||
ToggleDiffViewMode => "ToggleDiffViewMode",
|
||||
QuickOpenFile => "QuickOpenFile",
|
||||
CopyText
|
||||
| CopyRaw
|
||||
| CutText
|
||||
| PasteText
|
||||
| SelectAllText
|
||||
@@ -765,6 +769,7 @@ impl Item {
|
||||
Item::localized(L10nKey::CmdFindNext, FindNext),
|
||||
Item::localized(L10nKey::CmdFindPrevious, FindPrevious),
|
||||
Item::localized(L10nKey::CmdCopy, CopyText),
|
||||
Item::localized(L10nKey::CmdCopyRaw, CopyRaw),
|
||||
Item::localized(L10nKey::CmdCut, CutText),
|
||||
Item::localized(L10nKey::CmdPaste, PasteText),
|
||||
Item::localized(L10nKey::CmdSelectAll, SelectAllText),
|
||||
@@ -928,6 +933,7 @@ mod tests {
|
||||
CommandKind::SplitRight,
|
||||
CommandKind::ClearTerminal,
|
||||
CommandKind::CopyText,
|
||||
CommandKind::CopyRaw,
|
||||
CommandKind::CutText,
|
||||
CommandKind::PasteText,
|
||||
CommandKind::SelectAllText,
|
||||
|
||||
@@ -539,6 +539,11 @@ fn settings_search_entries() -> &'static [SearchEntry] {
|
||||
title: SettingsTrimTrailingSpaces,
|
||||
keywords: SettingsSearchTrimTrailingSpacesKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: KeyboardMouse,
|
||||
title: SettingsCopyJoinWrapped,
|
||||
keywords: SettingsSearchCopyJoinWrappedKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: Ssh,
|
||||
title: SettingsHosts,
|
||||
@@ -845,6 +850,7 @@ impl SearchEntry {
|
||||
L10nKey::SettingsSmoothScroll => "smooth_scroll",
|
||||
L10nKey::SettingsMouseZoom => "mouse_zoom_modifier",
|
||||
L10nKey::SettingsTrimTrailingSpaces => "clipboard_trim_trailing_spaces",
|
||||
L10nKey::SettingsCopyJoinWrapped => "copy_join_wrapped",
|
||||
L10nKey::SettingsCopyOnSelect => "copy_on_select",
|
||||
L10nKey::SettingsSmartSelection => "smart_select",
|
||||
L10nKey::SettingsPromptEditor => "prompt_editor",
|
||||
@@ -936,6 +942,7 @@ impl SearchEntry {
|
||||
L10nKey::SettingsSmartSelection => t(L10nKey::SettingsSmartSelectionDesc),
|
||||
L10nKey::SettingsCopyOnSelect => t(L10nKey::SettingsCopyOnSelectDesc),
|
||||
L10nKey::SettingsTrimTrailingSpaces => t(L10nKey::SettingsTrimTrailingSpacesDesc),
|
||||
L10nKey::SettingsCopyJoinWrapped => t(L10nKey::SettingsCopyJoinWrappedDesc),
|
||||
L10nKey::SettingsVerifyHostKeys => t(L10nKey::SettingsVerifyHostKeysDesc),
|
||||
L10nKey::SettingsStartupWindow => t(L10nKey::SettingsStartupWindowDesc),
|
||||
L10nKey::SettingsRememberWindowSize => t(L10nKey::SettingsRememberWindowSizeDesc),
|
||||
@@ -1033,6 +1040,7 @@ impl SearchEntry {
|
||||
L10nKey::SettingsTrimTrailingSpaces => {
|
||||
cfg.clipboard_trim_trailing_spaces != defaults.clipboard_trim_trailing_spaces
|
||||
}
|
||||
L10nKey::SettingsCopyJoinWrapped => cfg.copy_join_wrapped != defaults.copy_join_wrapped,
|
||||
L10nKey::SettingsCopyOnSelect => cfg.copy_on_select != defaults.copy_on_select,
|
||||
L10nKey::SettingsSmartSelection => cfg.smart_select != defaults.smart_select,
|
||||
L10nKey::SettingsPromptEditor => cfg.prompt_editor != defaults.prompt_editor,
|
||||
|
||||
@@ -1233,6 +1233,11 @@ impl Tty7App {
|
||||
let trim = self.settings_switch("term-clip-trim", clip_trim, cx, |this, on, _, cx| {
|
||||
this.set_clipboard_trim(on, cx)
|
||||
});
|
||||
let join_wrapped = cx.global::<Config>().copy_join_wrapped;
|
||||
let join =
|
||||
self.settings_switch("copy-join-wrapped", join_wrapped, cx, |this, on, _, cx| {
|
||||
this.update_config(cx, |c| c.copy_join_wrapped = on)
|
||||
});
|
||||
// Ctrl only earns a cell where it is a different key from the platform
|
||||
// modifier: off macOS the two are the same key. A config that names
|
||||
// `ctrl` there still highlights it, in the one cell that means it.
|
||||
@@ -1335,6 +1340,12 @@ impl Tty7App {
|
||||
trim,
|
||||
cx,
|
||||
),
|
||||
self.settings_row(
|
||||
t(L10nKey::SettingsCopyJoinWrapped),
|
||||
t(L10nKey::SettingsCopyJoinWrappedDesc),
|
||||
join,
|
||||
cx,
|
||||
),
|
||||
]
|
||||
.map(IntoElement::into_any_element),
|
||||
cx,
|
||||
|
||||
Reference in new issue
Block a user