Repository: jesseduffield/lazygit (language: go)

Issue:
Add command to copy diff in diff mode
**Is your feature request related to a problem? Please describe.**
When I use lazygit's diff mode (shift+w), I often want to copy the diff. E.g. to share the result of multiple commits in a code review. There doesn't seem to be an easy way to do this within lazygit.

**Describe the solution you'd like**
A command to copy the current diff generated by diff mode.

**Describe alternatives you've considered**
N/A

**Additional context**
N/A

<!--
You may be able to add your desired feature with a custom command. Check out the examples here: https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium

If a custom command does what you want but you still want to see the feature built-in to lazygit, feel free to paste the custom command into the issue to help us better understand the functionality you want.

We also encourage you to put up a PR yourself! Who cares if you've never written Go before, neither did any of the existing contributors before their first lazygit PR! Check out the PR tutorial here: https://www.youtube.com/watch?v=kNavnhzZHtk&ab_channel=JesseDuffield

Also check out the contributing guide here: https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md
-->



Discussion / hints:
Is this only for diff ranges in the commits panel? If so, I'm wondering if you know about the more ad-hoc feature of selecting a range of commits using shift-arrow, and seeing the diff for those commits immediately. I use this a lot, and I find that I very rarely use the shift-W any more since we added that.

Of course, this doesn't make a difference for your feature request, because that kind of diff isn't easily copyable either, but it's very easy to make a custom command for it:
```yaml
customCommands:
    - command: 'git diff {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}} | pbcopy'
      context: "commits,subCommits"
      description: "Copy diff of selected commits to clipboard"
```
(You need a master build for the `SelectedCommitRange`, it's not released yet.)
Ah yes! I also use that feature, and meant to request it for that as well. Good to know it's possible with a custom command. In that case, I'm happy to close this, unless you want to leave it open?
Up to you. It might still be useful to have this as a feature. 

Actually I think what we should do is provide the same `y` menu that you get in the Files panel also in the Commit Files panel; the downside is that you don't get the functionality everywhere that shift-W is possible (which is also for tags and branches), and also that you need an extra "enter" keypress before you get the menu; but the advantage is that you can choose to copy the diff for all files or only for the selected file. And this would also allow copying paths and filenames, which I have sometimes missed.
> Actually I think what we should do is provide the same y menu that you get in the Files panel also in the Commit Files panel; the downside is that you don't get the functionality everywhere that shift-W is possible (which is also for tags and branches), and also that you need an extra "enter" keypress before you get the menu; but the advantage is that you can choose to copy the diff for all files or only for the selected file. And this would also allow copying paths and filenames, which I have sometimes missed.

I agree.

Fix patch:
diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md
index f162014b700..8dc8b60a811 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -56,6 +56,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 | Key | Action | Info |
 |-----|--------|-------------|
 | `` <c-o> `` | Copy path to clipboard |  |
+| `` y `` | Copy to clipboard |  |
 | `` c `` | Checkout | Checkout file. This replaces the file in your working tree with the version from the selected commit. |
 | `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
 | `` o `` | Open file | Open file in default application. |
diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md
index a1046c8dc17..2e55fdee2d8 100644
--- a/docs/keybindings/Keybindings_ja.md
+++ b/docs/keybindings/Keybindings_ja.md
@@ -134,6 +134,7 @@ If you would instead like to start an interactive rebase from the selected commi
 | Key | Action | Info |
 |-----|--------|-------------|
 | `` <c-o> `` | ファイル名をクリップボードにコピー |  |
+| `` y `` | Copy to clipboard |  |
 | `` c `` | チェックアウト | Checkout file. This replaces the file in your working tree with the version from the selected commit. |
 | `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
 | `` o `` | ファイルを開く | Open file in default application. |
diff --git a/docs/keybindings/Keybindings_ko.md b/docs/keybindings/Keybindings_ko.md
index 127ac71662a..e3c1c81aef2 100644
--- a/docs/keybindings/Keybindings_ko.md
+++ b/docs/keybindings/Keybindings_ko.md
@@ -299,6 +299,7 @@ If you would instead like to start an interactive rebase from the selected commi
 | Key | Action | Info |
 |-----|--------|-------------|
 | `` <c-o> `` | 파일명을 클립보드에 복사 |  |
+| `` y `` | 클립보드에 복사 |  |
 | `` c `` | 체크아웃 | Checkout file |
 | `` d `` | Remove | Discard this commit's changes to this file |
 | `` o `` | 파일 닫기 | Open file in default application. |
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md
index 37bacb20f35..eb940bac20c 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -129,6 +129,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 | Key | Action | Info |
 |-----|--------|-------------|
 | `` <c-o> `` | Kopieer de bestandsnaam naar het klembord |  |
+| `` y `` | Copy to clipboard |  |
 | `` c `` | Uitchecken | Bestand uitchecken |
 | `` d `` | Remove | Uitsluit deze commit zijn veranderingen aan dit bestand |
 | `` o `` | Open bestand | Open file in default application. |
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md
index 8db7a0e73ee..a3000700527 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -238,6 +238,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
 | Key | Action | Info |
 |-----|--------|-------------|
 | `` <c-o> `` | Kopiuj ścieżkę do schowka |  |
+| `` y `` | Kopiuj do schowka |  |
 | `` c `` | Przełącz | Przełącz plik. Zastępuje plik w twoim drzewie roboczym wersją z wybranego commita. |
 | `` d `` | Usuń | Odrzuć zmiany w tym pliku z tego commita. Uruchamia interaktywny rebase w tle, więc możesz otrzymać konflikt scalania, jeśli późniejszy commit również zmienia ten plik. |
 | `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
diff --git a/docs/keybindings/Keybindings_pt.md b/docs/keybindings/Keybindings_pt.md
index f256b9e7881..aa933c179db 100644
--- a/docs/keybindings/Keybindings_pt.md
+++ b/docs/keybindings/Keybindings_pt.md
@@ -135,6 +135,7 @@ Veja a documentação:
 | Key | Action | Info |
 |-----|--------|-------------|
 | `` <c-o> `` | Copy path to clipboard |  |
+| `` y `` | Copy to clipboard |  |
 | `` c `` | Verificar | Checkout file. This replaces the file in your working tree with the version from the selected commit. |
 | `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
 | `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
diff --git a/docs/keybindings/Keybindings_ru.md b/docs/keybindings/Keybindings_ru.md
index 9c17e4c6b92..28abe78eb0c 100644
--- a/docs/keybindings/Keybindings_ru.md
+++ b/docs/keybindings/Keybindings_ru.md
@@ -261,6 +261,7 @@ If you would instead like to start an interactive rebase from the selected commi
 | Key | Action | Info |
 |-----|--------|-------------|
 | `` <c-o> `` | Скопировать название файла в буфер обмена |  |
+| `` y `` | Copy to clipboard |  |
 | `` c `` | Переключить | Переключить файл |
 | `` d `` | Remove | Отменить изменения коммита в этом файле |
 | `` o `` | Открыть файл | Open file in default application. |
diff --git a/docs/keybindings/Keybindings_zh-CN.md b/docs/keybindings/Keybindings_zh-CN.md
index 790a64f5321..663dd98f057 100644
--- a/docs/keybindings/Keybindings_zh-CN.md
+++ b/docs/keybindings/Keybindings_zh-CN.md
@@ -186,6 +186,7 @@ _图例：`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
 | Key | Action | Info |
 |-----|--------|-------------|
 | `` <c-o> `` | 将文件名复制到剪贴板 |  |
+| `` y `` | 复制到剪贴板 |  |
 | `` c `` | 检出 | 检出文件 |
 | `` d `` | 删除 | 放弃对此文件的提交变更 |
 | `` o `` | 打开文件 | 使用默认程序打开该文件 |
diff --git a/docs/keybindings/Keybindings_zh-TW.md b/docs/keybindings/Keybindings_zh-TW.md
index 895a81e79fd..cb828d75749 100644
--- a/docs/keybindings/Keybindings_zh-TW.md
+++ b/docs/keybindings/Keybindings_zh-TW.md
@@ -210,6 +210,7 @@ If you would instead like to start an interactive rebase from the selected commi
 | Key | Action | Info |
 |-----|--------|-------------|
 | `` <c-o> `` | 複製檔案名稱到剪貼簿 |  |
+| `` y `` | 複製到剪貼簿 |  |
 | `` c `` | 檢出 | 檢出檔案 |
 | `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
 | `` o `` | 開啟檔案 | 使用預設軟體開啟 |
diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go
index 61dfa1d8580..0e54fb25ba9 100644
--- a/pkg/gui/controllers/commits_files_controller.go
+++ b/pkg/gui/controllers/commits_files_controller.go
@@ -41,6 +41,12 @@ func NewCommitFilesController(
 
 func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
 	bindings := []*types.Binding{
+		{
+			Key:         opts.GetKey(opts.Config.Files.CopyFileInfoToClipboard),
+			Handler:     self.openCopyMenu,
+			Description: self.c.Tr.CopyToClipboardMenu,
+			OpensMenu:   true,
+		},
 		{
 			Key:               opts.GetKey(opts.Config.CommitFiles.CheckoutCommitFile),
 			Handler:           self.withItem(self.checkout),
@@ -181,6 +187,77 @@ func (self *CommitFilesController) onClickMain(opts gocui.ViewMouseBindingOpts)
 	return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y})
 }
 
+func (self *CommitFilesController) copyDiffToClipboard(path string, toastMessage string) error {
+	from, to := self.context().GetFromAndToForDiff()
+	from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
+
+	cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, path, true)
+	diff, err := cmdObj.RunWithOutput()
+	if err != nil {
+		return err
+	}
+	if err := self.c.OS().CopyToClipboard(diff); err != nil {
+		return err
+	}
+	self.c.Toast(toastMessage)
+	return nil
+}
+
+func (self *CommitFilesController) openCopyMenu() error {
+	node := self.context().GetSelected()
+
+	copyNameItem := &types.MenuItem{
+		Label: self.c.Tr.CopyFileName,
+		OnPress: func() error {
+			if err := self.c.OS().CopyToClipboard(node.Name()); err != nil {
+				return err
+			}
+			self.c.Toast(self.c.Tr.FileNameCopiedToast)
+			return nil
+		},
+		DisabledReason: self.require(self.singleItemSelected())(),
+		Key:            'n',
+	}
+	copyPathItem := &types.MenuItem{
+		Label: self.c.Tr.CopyFilePath,
+		OnPress: func() error {
+			if err := self.c.OS().CopyToClipboard(node.Path); err != nil {
+				return err
+			}
+			self.c.Toast(self.c.Tr.FilePathCopiedToast)
+			return nil
+		},
+		DisabledReason: self.require(self.singleItemSelected())(),
+		Key:            'p',
+	}
+	copyFileDiffItem := &types.MenuItem{
+		Label: self.c.Tr.CopySelectedDiff,
+		OnPress: func() error {
+			return self.copyDiffToClipboard(node.GetPath(), self.c.Tr.FileDiffCopiedToast)
+		},
+		DisabledReason: self.require(self.singleItemSelected())(),
+		Key:            's',
+	}
+	copyAllDiff := &types.MenuItem{
+		Label: self.c.Tr.CopyAllFilesDiff,
+		OnPress: func() error {
+			return self.copyDiffToClipboard(".", self.c.Tr.AllFilesDiffCopiedToast)
+		},
+		DisabledReason: self.require(self.itemsSelected())(),
+		Key:            'a',
+	}
+
+	return self.c.Menu(types.CreateMenuOptions{
+		Title: self.c.Tr.CopyToClipboardMenu,
+		Items: []*types.MenuItem{
+			copyNameItem,
+			copyPathItem,
+			copyFileDiffItem,
+			copyAllDiff,
+		},
+	})
+}
+
 func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error {
 	self.c.LogAction(self.c.Tr.Actions.CheckoutFile)
 	if err := self.c.Git().WorkingTree.CheckoutFile(self.context().GetRef().RefName(), node.GetPath()); err != nil {


Test patch:
diff --git a/pkg/integration/tests/commit/copy_author_to_clipboard.go b/pkg/integration/tests/commit/copy_author_to_clipboard.go
index 9e182265f27..22a7311093f 100644
--- a/pkg/integration/tests/commit/copy_author_to_clipboard.go
+++ b/pkg/integration/tests/commit/copy_author_to_clipboard.go
@@ -12,8 +12,7 @@ var CopyAuthorToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
 	ExtraCmdArgs: []string{},
 	Skip:         false,
 	SetupConfig: func(config *config.AppConfig) {
-		// Include delimiters around the text so that we can assert on the entire content
-		config.GetUserConfig().OS.CopyToClipboardCmd = "echo /{{text}}/ > clipboard"
+		config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
 	},
 
 	SetupRepo: func(shell *Shell) {
@@ -36,13 +35,6 @@ var CopyAuthorToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
 
 		t.ExpectToast(Equals("Commit author copied to clipboard"))
 
-		t.Views().Files().
-			Focus().
-			Press(keys.Files.RefreshFiles).
-			Lines(
-				Contains("clipboard").IsSelected(),
-			)
-
-		t.Views().Main().Content(Contains("/John Doe <john@doe.com>/"))
+		t.FileSystem().FileContent("clipboard", Equals("John Doe <john@doe.com>"))
 	},
 })
diff --git a/pkg/integration/tests/commit/copy_tag_to_clipboard.go b/pkg/integration/tests/commit/copy_tag_to_clipboard.go
index a88148754ca..6bcd0348356 100644
--- a/pkg/integration/tests/commit/copy_tag_to_clipboard.go
+++ b/pkg/integration/tests/commit/copy_tag_to_clipboard.go
@@ -12,8 +12,7 @@ var CopyTagToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
 	ExtraCmdArgs: []string{},
 	Skip:         false,
 	SetupConfig: func(config *config.AppConfig) {
-		// Include delimiters around the text so that we can assert on the entire content
-		config.GetUserConfig().OS.CopyToClipboardCmd = "echo _{{text}}_ > clipboard"
+		config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
 	},
 
 	SetupRepo: func(shell *Shell) {
@@ -38,14 +37,6 @@ var CopyTagToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
 
 		t.ExpectToast(Equals("Commit tags copied to clipboard"))
 
-		t.Views().Files().
-			Focus().
-			Press(keys.Files.RefreshFiles).
-			Lines(
-				Contains("clipboard").IsSelected(),
-			)
-
-		t.Views().Main().Content(Contains("+_tag2"))
-		t.Views().Main().Content(Contains("+tag1_"))
+		t.FileSystem().FileContent("clipboard", Equals("tag2\ntag1"))
 	},
 })
diff --git a/pkg/integration/tests/commit/paste_commit_message.go b/pkg/integration/tests/commit/paste_commit_message.go
index 72edf72af56..ab9939e5b9f 100644
--- a/pkg/integration/tests/commit/paste_commit_message.go
+++ b/pkg/integration/tests/commit/paste_commit_message.go
@@ -10,7 +10,7 @@ var PasteCommitMessage = NewIntegrationTest(NewIntegrationTestArgs{
 	ExtraCmdArgs: []string{},
 	Skip:         false,
 	SetupConfig: func(config *config.AppConfig) {
-		config.GetUserConfig().OS.CopyToClipboardCmd = "echo {{text}} > ../clipboard"
+		config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > ../clipboard"
 		config.GetUserConfig().OS.ReadFromClipboardCmd = "cat ../clipboard"
 	},
 	SetupRepo: func(shell *Shell) {
diff --git a/pkg/integration/tests/commit/paste_commit_message_over_existing.go b/pkg/integration/tests/commit/paste_commit_message_over_existing.go
index 049d5acbd09..9f0ab259cc4 100644
--- a/pkg/integration/tests/commit/paste_commit_message_over_existing.go
+++ b/pkg/integration/tests/commit/paste_commit_message_over_existing.go
@@ -10,7 +10,7 @@ var PasteCommitMessageOverExisting = NewIntegrationTest(NewIntegrationTestArgs{
 	ExtraCmdArgs: []string{},
 	Skip:         false,
 	SetupConfig: func(config *config.AppConfig) {
-		config.GetUserConfig().OS.CopyToClipboardCmd = "echo {{text}} > ../clipboard"
+		config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > ../clipboard"
 		config.GetUserConfig().OS.ReadFromClipboardCmd = "cat ../clipboard"
 	},
 	SetupRepo: func(shell *Shell) {
diff --git a/pkg/integration/tests/diff/copy_to_clipboard.go b/pkg/integration/tests/diff/copy_to_clipboard.go
new file mode 100644
index 00000000000..88c14cd4886
--- /dev/null
+++ b/pkg/integration/tests/diff/copy_to_clipboard.go
@@ -0,0 +1,123 @@
+package diff
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+// note: this is required to simulate the clipboard during CI
+func expectClipboard(t *TestDriver, matcher *TextMatcher) {
+	defer t.Shell().DeleteFile("clipboard")
+
+	t.FileSystem().FileContent("clipboard", matcher)
+}
+
+var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "The copy menu allows to copy name and diff of selected/all files",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+		config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
+	},
+	SetupRepo: func(shell *Shell) {
+		shell.CreateDir("dir")
+		shell.CreateFileAndAdd("dir/file1", "1st line\n")
+		shell.Commit("1")
+		shell.CreateFileAndAdd("dir/file1", "1st line\n2nd line\n")
+		shell.CreateFileAndAdd("dir/file2", "file2\n")
+		shell.Commit("2")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("2").IsSelected(),
+				Contains("1"),
+			).
+			PressEnter()
+
+		t.Views().CommitFiles().
+			IsFocused().
+			Lines(
+				Contains("dir").IsSelected(),
+				Contains("file1"),
+				Contains("file2"),
+			).
+			NavigateToLine(Contains("file1")).
+			Press(keys.Files.CopyFileInfoToClipboard).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Copy to clipboard")).
+					Select(Contains("File name")).
+					Confirm().
+					Tap(func() {
+						t.ExpectToast(Equals("File name copied to clipboard"))
+						expectClipboard(t, Equals("file1"))
+					})
+			}).
+			Press(keys.Files.CopyFileInfoToClipboard).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Copy to clipboard")).
+					Select(Contains("Path")).
+					Confirm().
+					Tap(func() {
+						t.ExpectToast(Equals("File path copied to clipboard"))
+						expectClipboard(t, Equals("dir/file1"))
+					})
+			}).
+			Press(keys.Files.CopyFileInfoToClipboard).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Copy to clipboard")).
+					Select(Contains("Diff of selected file")).
+					Confirm().
+					Tap(func() {
+						t.ExpectToast(Equals("File diff copied to clipboard"))
+						expectClipboard(t,
+							Contains("diff --git a/dir/file1 b/dir/file1").Contains("+2nd line").DoesNotContain("+1st line").
+								DoesNotContain("diff --git a/dir/file2 b/dir/file2").DoesNotContain("+file2"))
+					})
+			}).
+			Press(keys.Files.CopyFileInfoToClipboard).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Copy to clipboard")).
+					Select(Contains("Diff of all files")).
+					Confirm().
+					Tap(func() {
+						t.ExpectToast(Equals("All files diff copied to clipboard"))
+						expectClipboard(t,
+							Contains("diff --git a/dir/file1 b/dir/file1").Contains("+2nd line").DoesNotContain("+1st line").
+								Contains("diff --git a/dir/file2 b/dir/file2").Contains("+file2"))
+					})
+			})
+
+		t.Views().Commits().
+			Focus().
+			// Select both commits
+			Press(keys.Universal.RangeSelectDown).
+			PressEnter()
+
+		t.Views().CommitFiles().
+			IsFocused().
+			Lines(
+				Contains("dir").IsSelected(),
+				Contains("file1"),
+				Contains("file2"),
+			).
+			NavigateToLine(Contains("file1")).
+			Press(keys.Files.CopyFileInfoToClipboard).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Copy to clipboard")).
+					Select(Contains("Diff of selected file")).
+					Confirm().
+					Tap(func() {
+						t.ExpectToast(Equals("File diff copied to clipboard"))
+						expectClipboard(t,
+							Contains("diff --git a/dir/file1 b/dir/file1").Contains("+1st line").Contains("+2nd line"))
+					})
+			})
+	},
+})
diff --git a/pkg/integration/tests/file/copy_menu.go b/pkg/integration/tests/file/copy_menu.go
index 1adb9989c97..9b96f8486d9 100644
--- a/pkg/integration/tests/file/copy_menu.go
+++ b/pkg/integration/tests/file/copy_menu.go
@@ -17,7 +17,7 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
 	ExtraCmdArgs: []string{},
 	Skip:         false,
 	SetupConfig: func(config *config.AppConfig) {
-		config.GetUserConfig().OS.CopyToClipboardCmd = "echo {{text}} > clipboard"
+		config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
 	},
 	SetupRepo: func(shell *Shell) {},
 	Run: func(t *TestDriver, keys config.KeybindingConfig) {
@@ -100,7 +100,7 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
 
 				t.ExpectToast(Equals("File name copied to clipboard"))
 
-				expectClipboard(t, Contains("unstaged_file"))
+				expectClipboard(t, Equals("1-unstaged_file"))
 			})
 
 		// Copy file path
@@ -114,7 +114,7 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
 
 				t.ExpectToast(Equals("File path copied to clipboard"))
 
-				expectClipboard(t, Contains("dir/1-unstaged_file"))
+				expectClipboard(t, Equals("dir/1-unstaged_file"))
 			})
 
 		// Selected path diff on a single (unstaged) file
diff --git a/pkg/integration/tests/misc/copy_to_clipboard.go b/pkg/integration/tests/misc/copy_to_clipboard.go
index 96b628c00ac..5b4eb731d0d 100644
--- a/pkg/integration/tests/misc/copy_to_clipboard.go
+++ b/pkg/integration/tests/misc/copy_to_clipboard.go
@@ -12,7 +12,7 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
 	ExtraCmdArgs: []string{},
 	Skip:         false,
 	SetupConfig: func(config *config.AppConfig) {
-		config.GetUserConfig().OS.CopyToClipboardCmd = "echo {{text}} > clipboard"
+		config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
 	},
 
 	SetupRepo: func(shell *Shell) {
@@ -34,13 +34,6 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
 
 		t.GlobalPress(keys.Files.RefreshFiles)
 
-		// Expect to see the clipboard file with contents
-		t.Views().Files().
-			IsFocused().
-			Lines(
-				Contains("clipboard").IsSelected(),
-			)
-
-		t.Views().Main().Content(Contains("branch-a"))
+		t.FileSystem().FileContent("clipboard", Equals("branch-a"))
 	},
 })
diff --git a/pkg/integration/tests/tag/copy_to_clipboard.go b/pkg/integration/tests/tag/copy_to_clipboard.go
index 124c94f9417..9f6b3835966 100644
--- a/pkg/integration/tests/tag/copy_to_clipboard.go
+++ b/pkg/integration/tests/tag/copy_to_clipboard.go
@@ -10,8 +10,7 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
 	ExtraCmdArgs: []string{},
 	Skip:         false,
 	SetupConfig: func(config *config.AppConfig) {
-		// Include delimiters around the text so that we can assert on the entire content
-		config.GetUserConfig().OS.CopyToClipboardCmd = "echo _{{text}}_ > clipboard"
+		config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
 	},
 	SetupRepo: func(shell *Shell) {
 		shell.EmptyCommit("one")
@@ -27,13 +26,6 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
 
 		t.ExpectToast(Equals("'super.l000ongtag' copied to clipboard"))
 
-		t.Views().Files().
-			Focus().
-			Press(keys.Files.RefreshFiles).
-			Lines(
-				Contains("clipboard").IsSelected(),
-			)
-
-		t.Views().Main().Content(Contains("super.l000ongtag"))
+		t.FileSystem().FileContent("clipboard", Equals("super.l000ongtag"))
 	},
 })
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 6a3830157f9..d0dc2a8a06a 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -167,6 +167,7 @@ var tests = []*components.IntegrationTest{
 	demo.StageLines,
 	demo.Undo,
 	demo.WorktreeCreateFromBranches,
+	diff.CopyToClipboard,
 	diff.Diff,
 	diff.DiffAndApplyPatch,
 	diff.DiffCommits,