use super::*;
use crate::config;
use crate::ui::theme;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SettingsTab {
General,
Theme,
Layout,
Keys,
Modules,
Integrations,
Language,
}
impl SettingsTab {
pub const ALL: [SettingsTab; 7] = [
SettingsTab::General,
SettingsTab::Theme,
SettingsTab::Layout,
SettingsTab::Keys,
SettingsTab::Modules,
SettingsTab::Integrations,
SettingsTab::Language,
];
pub fn icon(self) -> &'static str {
match self {
SettingsTab::General => "◈",
SettingsTab::Theme => "◑",
SettingsTab::Layout => "▦",
SettingsTab::Keys => "⌨",
SettingsTab::Modules => "❏",
SettingsTab::Integrations => "⌁",
SettingsTab::Language => "⊕",
}
}
pub fn label(self, cat: &crate::i18n::Catalog) -> &'static str {
match self {
SettingsTab::General => cat.tab_general,
SettingsTab::Theme => cat.tab_theme,
SettingsTab::Layout => cat.tab_layout,
SettingsTab::Keys => cat.tab_keys,
SettingsTab::Modules => cat.tab_modules,
SettingsTab::Integrations => cat.tab_agents,
SettingsTab::Language => cat.tab_language,
}
}
fn index(self) -> usize {
Self::ALL.iter().position(|t| *t == self).unwrap_or(0)
}
fn from_index(i: usize) -> SettingsTab {
Self::ALL[i % Self::ALL.len()]
}
}
pub struct SettingsUi {
pub tab: SettingsTab,
pub cursor: usize,
pub capturing: bool,
}
#[derive(Clone)]
pub enum LayoutRow {
SidebarWidth,
ColGap,
RowGap,
Scrollback,
PaneTitles,
ResumeWs,
#[cfg(windows)]
Shell,
LeftVisible,
RightVisible,
RightWidth,
Dock(DockKind),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum GeneralRow {
FileOpen,
FilesShowHidden,
ShiftEnter,
CheckUpdates,
SoundDone,
SoundBlocked,
TestSound,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ModuleRow {
Module(usize),
Setting(usize, usize),
}
const MODULE_SETTING_MAX: usize = 512;
impl App {
pub fn general_rows(&self) -> Vec<GeneralRow> {
vec![
GeneralRow::FileOpen,
GeneralRow::FilesShowHidden,
GeneralRow::ShiftEnter,
GeneralRow::CheckUpdates,
GeneralRow::SoundDone,
GeneralRow::SoundBlocked,
GeneralRow::TestSound,
]
}
pub fn general_section_start(&self) -> usize {
4
}
pub fn layout_rows(&self) -> Vec<LayoutRow> {
let mut v = vec![
LayoutRow::SidebarWidth,
LayoutRow::ColGap,
LayoutRow::RowGap,
LayoutRow::Scrollback,
LayoutRow::PaneTitles,
LayoutRow::ResumeWs,
];
#[cfg(windows)]
v.push(LayoutRow::Shell);
v.push(LayoutRow::LeftVisible);
v.push(LayoutRow::RightVisible);
v.push(LayoutRow::RightWidth);
for k in self.available_docks() {
v.push(LayoutRow::Dock(k));
}
v
}
pub fn dock_section_start(&self) -> usize {
#[cfg(windows)]
{
7
}
#[cfg(not(windows))]
{
6
}
}
pub fn open_settings(&mut self) {
self.settings = Some(SettingsUi {
tab: SettingsTab::General,
cursor: 0,
capturing: false,
});
}
pub fn close_settings(&mut self) {
self.settings = None;
self.module_setting_edit = None;
}
pub fn open_changelog(&mut self) {
self.changelog_open = true;
self.changelog_scroll = 0;
}
pub fn settings_rows(&self, tab: SettingsTab) -> usize {
match tab {
SettingsTab::General => self.general_rows().len(),
SettingsTab::Theme => theme::THEMES.len(),
SettingsTab::Layout => self.layout_rows().len(),
SettingsTab::Keys => crate::app::Cmd::ALL.len() + crate::app::key_reference_rows(),
SettingsTab::Modules => self.module_rows().len(),
SettingsTab::Integrations => crate::integration::AGENTS.len(),
SettingsTab::Language => crate::i18n::LANGS.len(),
}
}
pub fn handle_settings_key(&mut self, key: KeyEvent) {
let Some(&SettingsUi {
tab,
cursor,
capturing,
}) = self.settings.as_ref()
else {
return;
};
if capturing {
if key.code != KeyCode::Esc {
if let (Some(cmd), Some(s)) = (Self::keys_cmd_at(cursor), keys::key_string(&key)) {
self.rebind(cmd, s);
}
}
if let Some(ui) = self.settings.as_mut() {
ui.capturing = false;
}
return;
}
match key.code {
KeyCode::Esc => self.close_settings(),
KeyCode::Tab => self.settings_set_tab(SettingsTab::from_index(tab.index() + 1)),
KeyCode::BackTab => self.settings_set_tab(SettingsTab::from_index(
tab.index() + SettingsTab::ALL.len() - 1,
)),
KeyCode::Up => self.settings_move(-1),
KeyCode::Down => self.settings_move(1),
KeyCode::Left => self.settings_adjust(cursor, -1),
KeyCode::Right => self.settings_adjust(cursor, 1),
KeyCode::Enter | KeyCode::Char(' ') => self.settings_activate(cursor),
KeyCode::Backspace | KeyCode::Delete if tab == SettingsTab::Keys => {
if let Some(cmd) = Self::keys_cmd_at(cursor) {
self.reset_binding(cmd);
}
}
KeyCode::Char(c) if ('1'..='7').contains(&c) => {
self.settings_set_tab(SettingsTab::from_index(c as usize - '1' as usize));
}
_ => {}
}
}
pub fn handle_settings_click(&mut self, c: u16, r: u16) {
let hit = |rect: Rect| c >= rect.x && c < rect.right() && r >= rect.y && r < rect.bottom();
if self.settings_close_rect.is_some_and(hit) {
self.close_settings();
return;
}
if self.settings_modal_rect.is_some_and(|m| !hit(m)) {
self.close_settings();
return;
}
if let Some((tab, _)) = self
.settings_tab_rects
.iter()
.find(|(_, rect)| hit(*rect))
.copied()
{
self.settings_set_tab(tab);
return;
}
if let Some((i, delta, _)) = self
.settings_arrow_rects
.iter()
.find(|(_, _, rect)| hit(*rect))
.copied()
{
if let Some(ui) = self.settings.as_mut() {
ui.cursor = i;
}
self.settings_adjust(i, delta);
return;
}
if let Some((i, _)) = self
.settings_ctl_rects
.iter()
.find(|(_, rect)| hit(*rect))
.map(|(i, rect)| (*i, *rect))
{
let tab = self.settings.as_ref().map(|u| u.tab);
if let Some(ui) = self.settings.as_mut() {
ui.cursor = i;
}
let is_slider = match tab {
Some(SettingsTab::Layout) => matches!(
self.layout_rows().get(i),
Some(LayoutRow::SidebarWidth)
| Some(LayoutRow::RightWidth)
| Some(LayoutRow::Dock(_))
),
Some(SettingsTab::General) => {
self.general_rows().get(i) == Some(&GeneralRow::FileOpen)
}
Some(SettingsTab::Modules) => self.module_row_is_slider(i),
_ => false,
};
if !is_slider {
self.settings_activate(i);
}
}
}
fn settings_set_tab(&mut self, tab: SettingsTab) {
let cursor = match tab {
SettingsTab::Theme => theme_cursor(&self.config.theme),
SettingsTab::Language => lang_cursor(&self.config.language),
_ => 0,
};
if let Some(ui) = self.settings.as_mut() {
ui.tab = tab;
ui.cursor = cursor;
}
}
pub fn settings_scroll(&mut self, dir: i32) {
self.settings_move(dir * 3);
}
fn settings_move(&mut self, delta: i32) {
let Some(&SettingsUi { tab, cursor, .. }) = self.settings.as_ref() else {
return;
};
let rows = self.settings_rows(tab);
if rows == 0 {
return;
}
let new = (cursor as i32 + delta).clamp(0, rows as i32 - 1) as usize;
if let Some(ui) = self.settings.as_mut() {
ui.cursor = new;
}
if tab == SettingsTab::Theme {
self.apply_theme(theme::THEMES[new]);
} else if tab == SettingsTab::Language {
self.apply_language(crate::i18n::LANGS[new]);
}
}
fn settings_adjust(&mut self, cursor: usize, delta: i32) {
let Some(tab) = self.settings.as_ref().map(|u| u.tab) else {
return;
};
match tab {
SettingsTab::Theme | SettingsTab::Language => self.settings_move(delta),
SettingsTab::Layout => self.adjust_layout(cursor, delta),
SettingsTab::General => self.adjust_general(cursor, delta),
SettingsTab::Keys => {} SettingsTab::Integrations => self.settings_activate(cursor),
SettingsTab::Modules => self.toggle_module(cursor, Some(delta)),
}
}
fn settings_activate(&mut self, cursor: usize) {
let Some(tab) = self.settings.as_ref().map(|u| u.tab) else {
return;
};
match tab {
SettingsTab::Theme => {
self.apply_theme(theme::THEMES[cursor.min(theme::THEMES.len() - 1)])
}
SettingsTab::Language => {
self.apply_language(crate::i18n::LANGS[cursor.min(crate::i18n::LANGS.len() - 1)])
}
SettingsTab::Layout => self.activate_layout(cursor),
SettingsTab::General => match self.general_rows().get(cursor).copied() {
Some(GeneralRow::TestSound) => self.test_sound(),
_ => self.adjust_general(cursor, 1),
},
SettingsTab::Keys => {
if Self::keys_cmd_at(cursor).is_some() {
if let Some(ui) = self.settings.as_mut() {
ui.capturing = true;
}
}
}
SettingsTab::Integrations => self.install_integration(cursor),
SettingsTab::Modules => self.toggle_module(cursor, None),
}
}
fn keys_cmd_at(cursor: usize) -> Option<crate::app::Cmd> {
crate::app::Cmd::ALL.get(cursor).copied()
}
pub fn module_rows(&self) -> Vec<ModuleRow> {
let mut v = Vec::new();
for (mi, m) in self.modules.modules.iter().enumerate() {
v.push(ModuleRow::Module(mi));
if m.enabled && m.warning.is_none() {
v.extend((0..m.manifest.settings.len()).map(|si| ModuleRow::Setting(mi, si)));
}
}
v
}
fn module_row_is_slider(&self, i: usize) -> bool {
use crate::module::manifest::SettingKind;
let Some(ModuleRow::Setting(mi, si)) = self.module_rows().get(i).copied() else {
return false;
};
self.modules
.modules
.get(mi)
.and_then(|m| m.manifest.settings.get(si))
.is_some_and(|s| matches!(s.kind, SettingKind::Number | SettingKind::Enum))
}
fn toggle_module(&mut self, cursor: usize, delta: Option<i32>) {
match self.module_rows().get(cursor).copied() {
Some(ModuleRow::Module(mi)) => {
if let Some(m) = self.modules.modules.get(mi) {
let (id, on) = (m.id.clone(), !m.enabled);
let _ = self.module_set_enabled(&id, on);
self.clamp_settings_cursor();
}
}
Some(ModuleRow::Setting(mi, si)) => self.adjust_module_setting(mi, si, delta),
None => {}
}
}
fn adjust_module_setting(&mut self, mi: usize, si: usize, delta: Option<i32>) {
use crate::module::manifest::SettingKind;
let Some((id, spec)) = self.modules.modules.get(mi).and_then(|m| {
m.manifest
.settings
.get(si)
.map(|s| (m.id.clone(), s.clone()))
}) else {
return;
};
let current = crate::module::settings::get(
&self.modules.find(&id).unwrap().manifest.clone(),
&id,
&spec.key,
)
.unwrap_or_else(|| spec.default_value());
if spec.kind == SettingKind::String {
self.module_setting_edit = Some(ModuleSettingEdit {
module_id: id,
key: spec.key.clone(),
title: spec.title.clone(),
buffer: if spec.secret {
String::new()
} else {
current.as_str().unwrap_or_default().to_string()
},
secret: spec.secret,
});
return;
}
let step = delta.unwrap_or(1) as i64;
let next = crate::module::settings::stepped(&spec, ¤t, step);
if let Err(e) = self.module_set_setting(&id, &spec.key, next) {
self.show_toast(e);
}
}
pub fn handle_module_setting_key(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Esc => self.module_setting_edit = None,
KeyCode::Enter => {
if let Some(e) = self.module_setting_edit.take() {
let v = Value::String(e.buffer);
if let Err(err) = self.module_set_setting(&e.module_id, &e.key, v) {
self.show_toast(err);
}
}
}
KeyCode::Backspace => {
if let Some(e) = self.module_setting_edit.as_mut() {
e.buffer.pop();
}
}
KeyCode::Char(c) => {
if let Some(e) = self.module_setting_edit.as_mut() {
if e.buffer.chars().count() < MODULE_SETTING_MAX {
e.buffer.push(c);
}
}
}
_ => {}
}
}
fn clamp_settings_cursor(&mut self) {
let Some(tab) = self.settings.as_ref().map(|u| u.tab) else {
return;
};
let max = self.settings_rows(tab).saturating_sub(1);
if let Some(ui) = self.settings.as_mut() {
ui.cursor = ui.cursor.min(max);
}
}
fn apply_theme(&mut self, name: &str) {
self.config.theme = name.to_string();
self.theme = theme::by_name(name);
if self.downsample {
self.theme = self.theme.to_256();
}
config::save(&self.config);
}
fn apply_language(&mut self, code: &str) {
self.config.language = code.to_string();
self.catalog = crate::i18n::by_code(code);
config::save(&self.config);
}
fn adjust_layout(&mut self, cursor: usize, delta: i32) {
let Some(row) = self.layout_rows().get(cursor).cloned() else {
return;
};
match row {
LayoutRow::SidebarWidth => {
let w = (self.sidebars.left.width as i32 + 2 * delta)
.clamp(SIDEBAR_WIDTH_MIN as i32, SIDEBAR_WIDTH_MAX as i32)
as u16;
self.set_side_width(Side::Left, w);
}
LayoutRow::RightWidth => {
let w = (self.sidebars.right.width as i32 + 2 * delta)
.clamp(SIDEBAR_WIDTH_MIN as i32, SIDEBAR_WIDTH_MAX as i32)
as u16;
self.set_side_width(Side::Right, w);
}
LayoutRow::ColGap => {
self.config.layout.col_gap ^= 1;
self.apply_gaps();
}
LayoutRow::RowGap => {
self.config.layout.row_gap ^= 1;
self.apply_gaps();
}
LayoutRow::Scrollback => {
let step = config::SCROLLBACK_STEP as i64;
let next = (self.config.layout.scrollback as i64 + step * delta as i64)
.clamp(config::SCROLLBACK_MIN as i64, config::SCROLLBACK_MAX as i64)
as usize;
self.config.layout.scrollback = next;
self.apply_scrollback();
config::save(&self.config);
}
LayoutRow::PaneTitles => {
self.config.layout.show_titles = !self.config.layout.show_titles;
config::save(&self.config);
}
LayoutRow::ResumeWs => {
self.config.layout.resume_in_new_workspace =
!self.config.layout.resume_in_new_workspace;
config::save(&self.config);
}
#[cfg(windows)]
LayoutRow::Shell => self.cycle_shell(delta),
LayoutRow::LeftVisible => {
self.sidebars.left.visible = !self.sidebars.left.visible;
self.save_sidebars();
}
LayoutRow::RightVisible => {
self.sidebars.right.visible = !self.sidebars.right.visible;
self.save_sidebars();
}
LayoutRow::Dock(kind) => {
if delta <= -1 {
self.move_dock(&kind, Side::Left);
} else if delta == 1 {
self.move_dock(&kind, Side::Right);
} else {
self.unmount_dock(&kind);
}
}
}
}
fn activate_layout(&mut self, cursor: usize) {
match self.layout_rows().get(cursor).cloned() {
Some(LayoutRow::Dock(kind)) => match self.sidebars.side_of(&kind) {
Some(Side::Left) => self.move_dock(&kind, Side::Right),
Some(Side::Right) => self.unmount_dock(&kind),
None => self.move_dock(&kind, Side::Left),
},
_ => self.adjust_layout(cursor, 1),
}
}
fn cycle_file_open(&mut self, delta: i32) {
let mut opts: Vec<String> = vec![config::FILE_OPEN_READONLY.to_string()];
opts.extend(self.editors.iter().map(|(cmd, _)| cmd.clone()));
let n = opts.len() as i32;
if n == 0 {
return;
}
let cur = opts
.iter()
.position(|o| *o == self.config.layout.file_open)
.unwrap_or(0) as i32;
let next = (((cur + delta) % n + n) % n) as usize;
self.config.layout.file_open = opts[next].clone();
config::save(&self.config);
}
fn cycle_shift_enter(&mut self, delta: i32) {
let opts = config::SHIFT_ENTER_CHOICES;
let n = opts.len() as i32;
let cur = opts
.iter()
.position(|(k, _, _)| *k == self.config.layout.shift_enter)
.unwrap_or(0) as i32;
let next = (((cur + delta) % n + n) % n) as usize;
self.config.layout.shift_enter = opts[next].0.to_string();
config::save(&self.config);
}
pub fn shift_enter_label(&self) -> String {
config::SHIFT_ENTER_CHOICES
.iter()
.find(|(k, _, _)| *k == self.config.layout.shift_enter)
.map(|(_, label, _)| label.to_string())
.unwrap_or_else(|| self.config.layout.shift_enter.clone())
}
pub fn file_open_label(&self) -> String {
let choice = &self.config.layout.file_open;
if choice == config::FILE_OPEN_READONLY {
return "read-only".to_string();
}
self.editors
.iter()
.find(|(cmd, _)| cmd == choice)
.map(|(_, label)| label.clone())
.unwrap_or_else(|| choice.clone())
}
#[cfg(windows)]
fn cycle_shell(&mut self, delta: i32) {
let choices = crate::platform::shell_choices();
let n = choices.len() as i32;
let cur = choices
.iter()
.position(|(k, _)| *k == self.config.shell)
.unwrap_or(0) as i32;
let next = (((cur + delta) % n + n) % n) as usize;
self.config.shell = choices[next].0.to_string();
config::save(&self.config);
}
fn apply_gaps(&mut self) {
crate::layout::set_gaps(self.config.layout.col_gap, self.config.layout.row_gap);
config::save(&self.config);
}
fn apply_scrollback(&mut self) {
let lines = self.config.scrollback();
for pane in self.panes.values() {
pane.set_scrollback(lines);
}
}
fn adjust_general(&mut self, cursor: usize, delta: i32) {
match self.general_rows().get(cursor).copied() {
Some(GeneralRow::FileOpen) => self.cycle_file_open(delta),
Some(GeneralRow::FilesShowHidden) => self.toggle_files_hidden(),
Some(GeneralRow::ShiftEnter) => self.cycle_shift_enter(delta),
Some(GeneralRow::CheckUpdates) => {
self.config.check_updates = !self.config.check_updates;
config::save(&self.config);
}
Some(GeneralRow::SoundDone) => {
self.config.notifications.sound_on_done = !self.config.notifications.sound_on_done;
config::save(&self.config);
}
Some(GeneralRow::SoundBlocked) => {
self.config.notifications.sound_on_blocked =
!self.config.notifications.sound_on_blocked;
config::save(&self.config);
}
Some(GeneralRow::TestSound) => {}
None => {}
}
}
fn test_sound(&mut self) {
self.pending_sound = true;
}
fn install_integration(&mut self, cursor: usize) {
if let Some(agent) = crate::integration::AGENTS.get(cursor) {
if crate::integration::is_installed(agent) {
let _ = crate::integration::uninstall(agent);
} else {
let _ = crate::integration::install(agent);
}
}
}
}
fn theme_cursor(name: &str) -> usize {
let name = theme::canonical(name);
theme::THEMES.iter().position(|n| *n == name).unwrap_or(0)
}
fn lang_cursor(code: &str) -> usize {
crate::i18n::LANGS
.iter()
.position(|c| *c == code)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn general_tab_toggles_sounds_and_tests_the_chime() {
let _env = crate::persist::test_env("general-tab");
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = crate::app::App::new(80, 24, tx).unwrap();
app.open_settings();
if let Some(ui) = app.settings.as_mut() {
ui.tab = SettingsTab::General;
}
assert_eq!(app.settings_rows(SettingsTab::General), 7);
let rows = app.general_rows();
assert_eq!(rows[0], GeneralRow::FileOpen, "file-open leads the tab");
let done = rows
.iter()
.position(|r| *r == GeneralRow::SoundDone)
.unwrap();
let blocked = rows
.iter()
.position(|r| *r == GeneralRow::SoundBlocked)
.unwrap();
let test = rows
.iter()
.position(|r| *r == GeneralRow::TestSound)
.unwrap();
app.settings_activate(done);
assert!(app.config.notifications.sound_on_done, "toggles done");
app.settings_activate(blocked);
assert!(app.config.notifications.sound_on_blocked, "toggles blocked");
assert!(!app.pending_sound);
app.settings_adjust(test, 1);
assert!(!app.pending_sound, "‹ › on the Test row does not ring");
app.settings_activate(test);
assert!(app.pending_sound, "the Test row rings the chime");
}
#[test]
fn general_tab_renders_file_open_then_a_notify_section() {
use ratatui::{backend::TestBackend, Terminal};
let _env = crate::persist::test_env("general-render");
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = crate::app::App::new(120, 40, tx).unwrap();
app.editors = vec![("vim".into(), "vim".into())];
app.open_settings();
if let Some(ui) = app.settings.as_mut() {
ui.tab = SettingsTab::General;
}
let mut term = Terminal::new(TestBackend::new(120, 40)).unwrap();
term.draw(|f| crate::ui::render(f, &mut app)).unwrap();
let buf = term.backend().buffer();
let text: Vec<String> = (0..buf.area.height)
.map(|r| {
(0..buf.area.width)
.map(|c| buf.cell((c, r)).map(|x| x.symbol()).unwrap_or(" "))
.collect::<String>()
})
.collect();
let all = text.join("\n");
if std::env::var("SHOW_UI").is_ok() {
println!("{all}");
}
assert!(all.contains("General"), "the General tab is in the strip");
assert!(all.contains("Open files with"), "file-open row drawn");
assert!(all.contains("read-only"), "its current value drawn");
assert!(all.contains("Notify"), "the notifications section divider");
let row_of = |needle: &str| text.iter().position(|l| l.contains(needle));
let (fo, div, snd) = (
row_of("Open files with"),
row_of("Notify"),
row_of("Test sound"),
);
assert!(fo < div && div < snd, "file-open → divider → sounds");
let (fo, div) = (fo.unwrap(), div.unwrap());
assert!(div >= fo + 2, "a blank gap sits above the section divider");
}
#[test]
fn general_replaces_the_notifications_tab() {
assert_eq!(
SettingsTab::ALL[0],
SettingsTab::General,
"General is first"
);
assert_eq!(SettingsTab::ALL[1], SettingsTab::Theme, "before Theme");
assert_eq!(SettingsTab::ALL.len(), 7, "still seven tabs");
}
#[test]
fn general_file_open_cycles_through_editors() {
let _env = crate::persist::test_env("file-open-cycle");
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = crate::app::App::new(80, 24, tx).unwrap();
app.editors = vec![("vim".into(), "vim".into()), ("nano".into(), "nano".into())];
app.open_settings();
if let Some(ui) = app.settings.as_mut() {
ui.tab = SettingsTab::General;
}
let idx = app
.general_rows()
.iter()
.position(|r| *r == GeneralRow::FileOpen)
.expect("the General tab has a file-open row");
assert_eq!(app.config.layout.file_open, "readonly", "starts read-only");
app.settings_adjust(idx, 1);
assert_eq!(app.config.layout.file_open, "vim");
app.settings_adjust(idx, 1);
assert_eq!(app.config.layout.file_open, "nano");
app.settings_adjust(idx, 1);
assert_eq!(
app.config.layout.file_open, "readonly",
"wraps back to read-only"
);
app.settings_adjust(idx, -1);
assert_eq!(
app.config.layout.file_open, "nano",
"steps backward with wrap"
);
}
#[test]
fn general_shift_enter_cycles_and_drives_the_bytes() {
let _env = crate::persist::test_env("shift-enter-cycle");
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = crate::app::App::new(80, 24, tx).unwrap();
app.open_settings();
if let Some(ui) = app.settings.as_mut() {
ui.tab = SettingsTab::General;
}
let idx = app
.general_rows()
.iter()
.position(|r| *r == GeneralRow::ShiftEnter)
.expect("the General tab has a Shift+Enter row");
assert_eq!(app.config.layout.shift_enter, "esc-cr", "starts at ESC CR");
assert_eq!(app.config.shift_enter_bytes(), b"\x1b\r");
app.settings_adjust(idx, 1);
assert_eq!(app.config.layout.shift_enter, "lf", "steps to LF");
assert_eq!(app.config.shift_enter_bytes(), b"\n");
app.settings_adjust(idx, -1);
app.settings_adjust(idx, -1);
let last = config::SHIFT_ENTER_CHOICES.last().unwrap().0;
assert_eq!(app.config.layout.shift_enter, last, "wraps to the last");
}
}