diff --git a/crates/ely_browser_core/src/navigation.rs b/crates/ely_browser_core/src/navigation.rs index 4141ab0..a56031e 100644 --- a/crates/ely_browser_core/src/navigation.rs +++ b/crates/ely_browser_core/src/navigation.rs @@ -112,6 +112,14 @@ pub(crate) fn rename_tab_group_name(command: &str) -> Option<&str> { ) } +pub(crate) fn tab_group_color_hex(command: &str) -> Option { + let value = command_argument( + command, + &["set-tab-group-color ", "set tab group color ", "tab-group-color ", "tab group color "], + )?; + parse_color_hex(value) +} + pub(crate) fn split_group_name(command: &str) -> Option<&str> { command_argument( command, @@ -142,6 +150,15 @@ fn command_argument<'a>(command: &'a str, prefixes: &[&str]) -> Option<&'a str> None } +fn parse_color_hex(value: &str) -> Option { + let value = value.trim().strip_prefix('#').unwrap_or(value.trim()); + if value.len() != 6 || !value.as_bytes().iter().all(u8::is_ascii_hexdigit) { + return None; + } + + u32::from_str_radix(value, 16).ok() +} + pub(crate) fn space_icon(name: &str) -> String { name.chars().next().map_or_else(String::new, |value| value.to_string()) } diff --git a/crates/ely_browser_core/src/state/commands.rs b/crates/ely_browser_core/src/state/commands.rs index 801fcbd..e163cec 100644 --- a/crates/ely_browser_core/src/state/commands.rs +++ b/crates/ely_browser_core/src/state/commands.rs @@ -11,8 +11,8 @@ use crate::{ move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name, note_body, notes_url, plugin_detail_url, plugins_url, reading_list_url, reading_progress_percent, rename_tab_group_name, search_url, settings_page_url, settings_url, shortcut_settings_url, - space_icon, split_group_name, switch_profile_name, sync_status_url, tab_group_name, - tab_note_body, task_manager_url, + space_icon, split_group_name, switch_profile_name, sync_status_url, tab_group_color_hex, + tab_group_name, tab_note_body, task_manager_url, }, }; @@ -149,6 +149,9 @@ impl BrowserCore { if let Some(name) = rename_tab_group_name(command) { return Ok(self.rename_active_tab_group(name)?.is_some()); } + if let Some(color_hex) = tab_group_color_hex(command) { + return Ok(self.set_active_tab_group_color(color_hex)?.is_some()); + } if let Some(body) = tab_note_body(command) { self.save_active_tab_note(body)?; return Ok(true); diff --git a/crates/ely_browser_core/src/state/tab_groups.rs b/crates/ely_browser_core/src/state/tab_groups.rs index 5b0bedc..52f30be 100644 --- a/crates/ely_browser_core/src/state/tab_groups.rs +++ b/crates/ely_browser_core/src/state/tab_groups.rs @@ -100,6 +100,24 @@ impl BrowserCore { group.rename(name).map_err(CoreError::from) } + pub fn set_active_tab_group_color(&mut self, color_hex: u32) -> Result, CoreError> { + let Some(group_id) = self.active_tab_group_id()? else { + return Ok(None); + }; + self.set_tab_group_color(&group_id, color_hex)?; + Ok(Some(())) + } + + pub fn set_tab_group_color( + &mut self, + group_id: &TabGroupId, + color_hex: u32, + ) -> Result<(), CoreError> { + let group = self.tab_group_mut(group_id)?; + group.set_color_hex(color_hex); + Ok(()) + } + pub fn discard_active_tab_group(&mut self) -> Result, CoreError> { let Some(group_id) = self.active_tab_group_id()? else { return Ok(None); diff --git a/crates/ely_browser_core/tests/tab_group_commands.rs b/crates/ely_browser_core/tests/tab_group_commands.rs index c4af996..268ad37 100644 --- a/crates/ely_browser_core/tests/tab_group_commands.rs +++ b/crates/ely_browser_core/tests/tab_group_commands.rs @@ -39,6 +39,56 @@ fn rename_tab_group_command_preserves_query_without_active_group() -> Result<(), Ok(()) } +#[test] +fn tab_group_color_command_updates_active_group_color() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + core.group_active_tab("Docs")?; + + core.set_command_query(">tab-group-color #9FC9A2"); + let intent = core.submit_command()?; + let snapshot = core.snapshot()?; + let group = snapshot.tab_groups.first().ok_or("missing tab group")?; + + assert_eq!(intent, Some(CommandIntent::Command("tab-group-color #9FC9A2".to_string()))); + assert_eq!(snapshot.command_query, ""); + assert_eq!(group.color_hex(), 0x9fc9a2); + Ok(()) +} + +#[test] +fn tab_group_color_command_preserves_query_without_active_group() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + + core.set_command_query(">tab-group-color #9FC9A2"); + let intent = core.submit_command()?; + let snapshot = core.snapshot()?; + + assert_eq!(intent, Some(CommandIntent::Command("tab-group-color #9FC9A2".to_string()))); + assert_eq!(snapshot.command_query, ">tab-group-color #9FC9A2"); + assert!(snapshot.tab_groups.is_empty()); + Ok(()) +} + +#[test] +fn tab_group_color_command_preserves_query_for_invalid_hex() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let group_id = core.group_active_tab("Docs")?; + + core.set_command_query(">tab-group-color orange"); + let intent = core.submit_command()?; + let snapshot = core.snapshot()?; + let group = snapshot + .tab_groups + .iter() + .find(|group| group.id() == &group_id) + .ok_or("missing tab group")?; + + assert_eq!(intent, Some(CommandIntent::Command("tab-group-color orange".to_string()))); + assert_eq!(snapshot.command_query, ">tab-group-color orange"); + assert_eq!(group.color_hex(), 0xf54e00); + Ok(()) +} + fn tab_group_id<'a>( snapshot: &'a ely_browser_core::BrowserSnapshot, tab_id: &ely_domain::TabId, diff --git a/docs/ui-shell.md b/docs/ui-shell.md index 9c04e5a..7f3e64d 100644 --- a/docs/ui-shell.md +++ b/docs/ui-shell.md @@ -103,6 +103,21 @@ Group rename updates the active group row in place: └──────────────────────────────┴───────────────────────────────────────────────┘ ``` +Group color updates the active group accent: + +```text +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ELY Browser [ >tab-group-color #9FC9A2.................. ] [pin] [*] [+] │ +├──────────────────────────────┬───────────────────────────────────────────────┤ +│ Tabs │ example.com │ +│ [folder] Docs │ https://example.com/b │ +│ 2 tabs - Expanded │ │ +│ color #9FC9A2 │ Group Docs │ +│ example.com │ │ +│ example.com │ │ +└──────────────────────────────┴───────────────────────────────────────────────┘ +``` + Group sleep applies the sleeping state to every tab in the active group: ```text