diff options
| author | kangal-bot <kangal@herdr.dev> | 2026-05-27 15:09:31 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-27 15:09:31 +0300 |
| commit | 858c36974bad616e9e6c1897882b8c9a97396bec (patch) | |
| tree | 63a6b332599596708dc7a66cead14af474755fd2 /src | |
| parent | 9a89e3dc181bdf35101c2066bea114394afb93e2 (diff) | |
feat: configure mobile width threshold (#317)
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/mod.rs | 29 | ||||
| -rw-r--r-- | src/app/state.rs | 2 | ||||
| -rw-r--r-- | src/config.rs | 1 | ||||
| -rw-r--r-- | src/config/model.rs | 13 | ||||
| -rw-r--r-- | src/main.rs | 4 | ||||
| -rw-r--r-- | src/ui.rs | 20 | ||||
| -rw-r--r-- | src/ui/mobile.rs | 5 |
7 files changed, 68 insertions, 6 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs index cad09a71..12fb704e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -478,6 +478,7 @@ impl App { sidebar_width, sidebar_min_width, sidebar_max_width, + mobile_width_threshold: config.ui.mobile_width_threshold, sidebar_width_source, sidebar_width_auto: false, sidebar_collapsed: false, @@ -1079,6 +1080,7 @@ impl App { } self.state.sidebar_min_width = config.ui.sidebar_min_width; self.state.sidebar_max_width = config.ui.sidebar_max_width; + self.state.mobile_width_threshold = config.ui.mobile_width_threshold; // Re-clamp the live width to the new bounds. No source guard — bounds // always apply, including to widths owned by Persisted or Manual. self.state.sidebar_width = self @@ -1778,6 +1780,10 @@ mod tests { // Default bounds. assert_eq!(app.state.sidebar_min_width, 18); assert_eq!(app.state.sidebar_max_width, 36); + assert_eq!( + app.state.mobile_width_threshold, + crate::config::DEFAULT_MOBILE_WIDTH_THRESHOLD + ); // Manually set a width and flip the source so the existing // sidebar_width-only-when-config-owned guard does NOT update it. @@ -1818,6 +1824,29 @@ mod tests { } #[test] + fn reload_config_updates_mobile_width_threshold() { + let _guard = config_env_lock().lock().unwrap(); + let path = temp_config_path("reload-config-mobile-width-threshold"); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path); + + let mut app = test_app(); + assert_eq!( + app.state.mobile_width_threshold, + crate::config::DEFAULT_MOBILE_WIDTH_THRESHOLD + ); + + std::fs::write(&path, "[ui]\nmobile_width_threshold = 96\n").unwrap(); + let report = app.reload_config(); + + assert_eq!(report.status, crate::config::ConfigReloadStatus::Applied); + assert_eq!(app.state.mobile_width_threshold, 96); + + std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR); + let _ = std::fs::remove_dir_all(path.parent().unwrap()); + } + + #[test] fn app_new_falls_back_to_default_bounds_on_inverted_config() { let mut config = Config::default(); config.ui.sidebar_min_width = 50; diff --git a/src/app/state.rs b/src/app/state.rs index de1db052..7c47973c 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1119,6 +1119,7 @@ pub struct AppState { pub sidebar_width: u16, pub sidebar_min_width: u16, pub sidebar_max_width: u16, + pub mobile_width_threshold: u16, pub sidebar_width_source: SidebarWidthSource, pub sidebar_width_auto: bool, pub sidebar_collapsed: bool, @@ -1424,6 +1425,7 @@ impl AppState { sidebar_width: 26, sidebar_min_width: 18, sidebar_max_width: 36, + mobile_width_threshold: crate::config::DEFAULT_MOBILE_WIDTH_THRESHOLD, sidebar_width_source: SidebarWidthSource::ConfigDefault, sidebar_width_auto: false, sidebar_collapsed: false, diff --git a/src/config.rs b/src/config.rs index d874455e..b4fcf1ef 100644 --- a/src/config.rs +++ b/src/config.rs @@ -30,6 +30,7 @@ pub(crate) use self::io::upsert_top_level_bool; 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 { diff --git a/src/config/model.rs b/src/config/model.rs index d4830df1..ac304973 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -3,8 +3,8 @@ use std::num::NonZeroUsize; use serde::{Deserialize, Deserializer, Serialize}; use super::{ - BindingConfig, CommandKeybindConfig, SoundConfig, ThemeConfig, DEFAULT_MOUSE_SCROLL_LINES, - DEFAULT_SCROLLBACK_LIMIT_BYTES, + BindingConfig, CommandKeybindConfig, SoundConfig, ThemeConfig, DEFAULT_MOBILE_WIDTH_THRESHOLD, + DEFAULT_MOUSE_SCROLL_LINES, DEFAULT_SCROLLBACK_LIMIT_BYTES, }; #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)] @@ -259,6 +259,8 @@ pub struct UiConfig { pub sidebar_min_width: u16, /// Maximum sidebar width (columns) when expanded. Default: 36. pub sidebar_max_width: u16, + /// Terminal width at or below which Herdr uses the mobile single-column layout. Default: 64. + pub mobile_width_threshold: u16, /// Capture mouse input for Herdr's mouse UI. Default: true. pub mouse_capture: bool, /// Force a full host-terminal redraw when the outer terminal regains focus. Default: true. @@ -420,6 +422,7 @@ impl Default for UiConfig { sidebar_width: 26, sidebar_min_width: 18, sidebar_max_width: 36, + mobile_width_threshold: DEFAULT_MOBILE_WIDTH_THRESHOLD, mouse_capture: true, redraw_on_focus_gained: true, mouse_scroll_lines: None, @@ -642,15 +645,21 @@ cjk_ime_agents = ["claude", "codex"] let default_config = Config::default(); assert_eq!(default_config.ui.sidebar_min_width, 18); assert_eq!(default_config.ui.sidebar_max_width, 36); + assert_eq!( + default_config.ui.mobile_width_threshold, + DEFAULT_MOBILE_WIDTH_THRESHOLD + ); let toml = r#" [ui] sidebar_min_width = 12 sidebar_max_width = 80 +mobile_width_threshold = 96 "#; let config: Config = toml::from_str(toml).unwrap(); assert_eq!(config.ui.sidebar_min_width, 12); assert_eq!(config.ui.sidebar_max_width, 80); + assert_eq!(config.ui.mobile_width_threshold, 96); } #[test] diff --git a/src/main.rs b/src/main.rs index 0d42effe..5f20d3a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -178,6 +178,10 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # Maximum sidebar width when expanded (columns) # sidebar_max_width = 36 +# Terminal width at or below which Herdr uses the mobile single-column layout. +# Increase this for foldables, tablets, or wide phone terminals. +# mobile_width_threshold = 64 + # Capture mouse input for Herdr's mouse UI. # Set false to let the terminal handle normal clicks, such as Cmd-clicking URLs. # Pane apps like lazygit and btop can still receive mouse when they request it. @@ -170,7 +170,7 @@ fn compute_view_internal( resize_panes: bool, cell_size: crate::kitty_graphics::HostCellSize, ) { - if is_mobile_width(area) { + if is_mobile_width(area, app.mobile_width_threshold) { compute_mobile_view(app, terminal_runtimes, area, resize_panes, cell_size); return; } @@ -544,6 +544,24 @@ mod tests { } #[test] + fn configured_mobile_width_threshold_controls_layout_switch() { + let mut app = crate::app::state::AppState::test_new(); + app.workspaces = vec![Workspace::test_new("one")]; + app.active = Some(0); + app.selected = 0; + app.mode = Mode::Terminal; + + compute_view(&mut app, Rect::new(0, 0, 80, 20)); + assert_eq!(app.view.layout, ViewLayout::Desktop); + + app.mobile_width_threshold = 90; + compute_view(&mut app, Rect::new(0, 0, 80, 20)); + assert_eq!(app.view.layout, ViewLayout::Mobile); + assert_eq!(app.view.mobile_header_rect, Rect::new(0, 0, 80, 2)); + assert_eq!(app.view.terminal_area, Rect::new(0, 2, 80, 18)); + } + + #[test] fn product_announcement_renders_above_config_diagnostic() { let mut app = crate::app::state::AppState::test_new(); app.workspaces = vec![Workspace::test_new("one")]; diff --git a/src/ui/mobile.rs b/src/ui/mobile.rs index 2de79d23..c1dd57b6 100644 --- a/src/ui/mobile.rs +++ b/src/ui/mobile.rs @@ -14,7 +14,6 @@ use crate::detect::AgentState; use crate::layout::PaneId; use crate::terminal::TerminalRuntimeRegistry; -pub(crate) const MOBILE_WIDTH_THRESHOLD: u16 = 64; const SWITCH_BUTTON_WIDTH: u16 = 10; #[derive(Debug, Clone, Copy, Default)] @@ -42,8 +41,8 @@ pub(crate) enum MobileSwitcherTarget { Menu(usize), } -pub(crate) fn is_mobile_width(area: Rect) -> bool { - area.width > 0 && area.width <= MOBILE_WIDTH_THRESHOLD +pub(crate) fn is_mobile_width(area: Rect, threshold: u16) -> bool { + area.width > 0 && area.width <= threshold } pub(crate) fn compute_mobile_header_hit_areas(_app: &AppState, area: Rect) -> MobileHeaderHitAreas { |
