1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use super::*;
impl App {
/// The path to copy. In Preview it is the preview target; in Tree it is the entry selected in the tree.
pub(super) fn copy_target(&self) -> Option<PathBuf> {
// Git 変更ハブ表示中は、ツリーの選択ではなく**変更ファイル**のパスをコピー対象にする。
#[cfg(feature = "git")]
if self.surface() == crate::keymap::Surface::GitChanges {
return self.git_view_selected();
}
match self.tab.mode {
Mode::Preview => self.tab.preview_path.clone(),
Mode::Tree => self
.tab
.entries
.get(self.tab.selected)
.map(|e| e.path.clone()),
}
}
/// Copy the selected path to the clipboard according to the kind, and show the result in flash (FR-6).
pub fn copy_path(&mut self, kind: CopyKind) {
let Some(path) = self.copy_target() else {
self.flash = Some(tr(self.lang, crate::i18n::Msg::NoCopyTarget).into());
return;
};
let text = copy_text(&path, &self.tab.open_dir, kind);
match set_clipboard(&text) {
Ok(()) => {
self.flash = Some(format!(
"{}{text}",
tr(self.lang, crate::i18n::Msg::CopiedPrefix)
))
}
Err(e) => {
self.flash = Some(format!(
"{}{e}",
tr(self.lang, crate::i18n::Msg::CopyFailed)
))
}
}
}
/// Test-only: the exact string `copy_path(kind)` would place on the clipboard for the current
/// copy target (tree selection / preview path / git-changes selection). `None` when there is no
/// target — the same gate as `copy_path`. Lets E2E assert copy values without the (headless-flaky)
/// clipboard round-trip.
#[cfg(test)]
pub fn copy_string_for(&self, kind: CopyKind) -> Option<String> {
let path = self.copy_target()?;
Some(copy_text(&path, &self.tab.open_dir, kind))
}
/// Test-only: the exact `@path#L..` string that `preview_copy_selection_ref` (`Y`) would copy.
#[cfg(test)]
pub fn selection_ref_string(&self) -> Option<String> {
self.preview_selection_ref_text()
}
/// Get metadata for the selected commit in log/graph/detail. detail uses the already-loaded data, while log/graph
/// fetch `commit_meta` from the selected commit id. None for non-commits (uncommitted rows, etc.) or out-of-scope surfaces.
#[cfg(feature = "git")]
pub(super) fn current_commit_meta(&self) -> Option<crate::git::CommitMeta> {
use crate::keymap::Surface;
match self.surface() {
Surface::GitDetail => self.tab.git_detail_meta.clone(),
Surface::GitLog => {
let id = self.git_log_selected_id()?;
crate::git::commit_meta(&self.tab.root, &id)
}
Surface::GitGraph => {
let id = self
.git_graph_selected_row()
.and_then(|r| r.commit.clone())?;
crate::git::commit_meta(&self.tab.root, &id)
}
_ => None,
}
}
/// git log/graph/detail: copy the selected commit's info (short/full hash, subject, full message, author, date)
/// to the clipboard. If there is no commit, notify via flash.
#[cfg(feature = "git")]
pub fn git_copy(&mut self, kind: GitCopyKind) {
let Some(meta) = self.current_commit_meta() else {
self.flash = Some(tr(self.lang, crate::i18n::Msg::NoCommitToCopy).into());
return;
};
let text = match kind {
GitCopyKind::ShortHash => meta.short,
GitCopyKind::FullHash => meta.id,
GitCopyKind::Subject => meta.message.lines().next().unwrap_or("").to_string(),
GitCopyKind::Message => meta.message,
GitCopyKind::Author => meta.author,
GitCopyKind::Date => meta.date,
};
self.set_clipboard_flash(&text);
}
/// branches view: copy the selected branch name to the clipboard.
#[cfg(feature = "git")]
pub fn git_copy_branch_name(&mut self) {
let Some(b) = self.git_branch_selected() else {
self.flash = Some(tr(self.lang, crate::i18n::Msg::NoCopyTarget).into());
return;
};
let name = b.name;
self.set_clipboard_flash(&name);
}
/// Write to the clipboard and flash success/failure (common processing for copy operations). On success, shows a one-line preview
/// (multi-line content such as a full message is rounded to the first line + `…` so the footer does not overflow).
pub(super) fn set_clipboard_flash(&mut self, text: &str) {
match set_clipboard(text) {
Ok(()) => {
let first = text.lines().next().unwrap_or("");
let mut preview: String = first.chars().take(60).collect();
if first.chars().count() > 60 || text.lines().nth(1).is_some() {
preview.push('…');
}
self.flash = Some(format!(
"{}{preview}",
tr(self.lang, crate::i18n::Msg::CopiedPrefix)
));
}
Err(e) => {
self.flash = Some(format!(
"{}{e}",
tr(self.lang, crate::i18n::Msg::CopyFailed)
))
}
}
}
}