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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
|
use crossterm::event::{KeyCode, KeyModifiers};
mod io;
mod keybinds;
mod model;
mod sound;
mod theme;
pub use self::{
io::{
config_diagnostic_summary, config_dir, config_path, load_live_config,
remove_keybinding_config_sections, remove_section_key, state_dir, upsert_section_bool,
upsert_section_value,
},
keybinds::{
format_key_combo, normalize_key_combo, terminal_key_matches_combo, ActionKeybinds,
BindingConfig, CommandKeybindConfig, CustomCommandAction, CustomCommandKeybind,
IndexedKeybind, Keybinds, LiveKeybindConfig,
},
model::{
validated_sidebar_bounds, AgentPanelSortConfig, Config, ConfigReloadReport,
ConfigReloadStatus, KeysConfig, NewTerminalCwdConfig, ShellModeConfig,
ToastClipboardPosition, ToastConfig, ToastDelivery, ToastHerdrPosition,
UpdateChannelConfig, MAX_TOAST_DELAY_SECONDS,
},
sound::SoundConfig,
theme::{parse_color, CustomThemeColors, ThemeConfig},
};
pub(crate) use self::io::upsert_top_level_bool;
pub(crate) use self::keybinds::parse_key_combo;
pub const CONFIG_PATH_ENV_VAR: &str = "HERDR_CONFIG_PATH";
pub const DEFAULT_SCROLLBACK_LIMIT_BYTES: usize = 10_000_000;
pub const DEFAULT_MOUSE_SCROLL_LINES: usize = 3;
pub const DEFAULT_MOBILE_WIDTH_THRESHOLD: u16 = 64;
#[cfg(test)]
pub(crate) fn app_dir_name() -> &'static str {
io::app_dir_name()
}
#[cfg(test)]
pub(crate) fn test_config_env_lock() -> &'static std::sync::Mutex<()> {
static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
LOCK.get_or_init(|| std::sync::Mutex::new(()))
}
impl Config {
pub fn should_show_onboarding(&self) -> bool {
self.onboarding.unwrap_or(true)
}
pub fn prefix_key(&self) -> (KeyCode, KeyModifiers) {
self.validated_keybinds().1
}
/// Parsed keybinds for Herdr actions.
pub fn keybinds(&self) -> Keybinds {
self.validated_keybinds().3
}
pub fn collect_diagnostics(&self) -> Vec<String> {
let (prefix_diag, _, keybind_diags, _) = self.validated_keybinds();
prefix_diag
.into_iter()
.chain(keybind_diags)
.chain(self.remote_image_paste_key().err())
.chain(self.ui.sound.diagnostics())
.collect()
}
pub(crate) fn remote_image_paste_key(&self) -> Result<Option<(KeyCode, KeyModifiers)>, String> {
let raw = self.keys.remote_image_paste.trim();
if raw.is_empty() {
return Ok(None);
}
parse_key_combo(raw).map(Some).ok_or_else(|| {
format!("invalid keybinding: keys.remote_image_paste = {raw:?}; disabling binding")
})
}
pub fn live_keybinds(&self) -> Result<LiveKeybindConfig, Vec<String>> {
let (prefix_diag, prefix, keybind_diags, keybinds) = self.validated_keybinds();
let diagnostics: Vec<String> = prefix_diag.into_iter().chain(keybind_diags).collect();
if diagnostics.is_empty() {
Ok(LiveKeybindConfig { prefix, keybinds })
} else {
Err(diagnostics)
}
}
pub(crate) fn local_keybindings_profile_toml(&self) -> Result<String, toml::ser::Error> {
let mut keys = self.keys.clone();
keys.command.clear();
#[derive(serde::Serialize)]
struct KeysProfile {
keys: KeysConfig,
}
toml::to_string_pretty(&KeysProfile { keys })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_keybindings_profile_includes_defaults_and_excludes_commands() {
let config: Config = toml::from_str(
r#"
[keys]
prefix = "ctrl+a"
new_tab = "prefix+t"
[[keys.command]]
key = "prefix+g"
command = "lazygit"
"#,
)
.unwrap();
let profile = config.local_keybindings_profile_toml().unwrap();
assert!(profile.contains("[keys]"));
assert!(profile.contains("prefix = \"ctrl+a\""));
assert!(profile.contains("new_tab = \"prefix+t\""));
assert!(profile.contains("next_tab = \"prefix+n\""));
assert!(!profile.contains("lazygit"));
assert!(!profile.contains("command ="));
assert!(!profile.contains("[[keys.command]]"));
}
#[test]
fn remote_image_paste_key_defaults_to_ctrl_v() {
let config = Config::default();
assert_eq!(
config.remote_image_paste_key().unwrap(),
Some((KeyCode::Char('v'), KeyModifiers::CONTROL))
);
}
#[test]
fn remote_image_paste_key_can_be_disabled() {
let config: Config = toml::from_str("[keys]\nremote_image_paste = ''\n").unwrap();
assert_eq!(config.remote_image_paste_key().unwrap(), None);
}
}
|