From e36cd17dd1a8040ee8c020aebbbde46e3268997e Mon Sep 17 00:00:00 2001 From: socksprox Date: Tue, 27 Oct 2026 18:06:53 +0100 Subject: [PATCH 1/2] Books: page click events work, run_command ones included Two gaps left every link in a written book dead. view_pages only parsed compound page components, so the list form ComponentSerialization also takes (and servers behind ViaVersion send) fell back to plain text and lost its click events. And run_command was never parsed into a ClickEvent, so even a parsed page's command links did nothing. The book's click handler now follows Screen.defaultHandleClickEvent: open_url through the link flow, run_command sent unsigned (client mods first, as clickCommandAction), suggest_command into the chat box, copy_to_clipboard, with chat's own click match gaining run_command the same way. Co-Authored-By: Devin (SWE-2) <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- crates/horizon-client-core/src/component.rs | 1 + crates/horizon-client-core/src/text.rs | 5 +++++ crates/horizon-client/src/game/books.rs | 20 +++++++++++++++++--- crates/horizon-client/src/game/chat.rs | 15 +++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/horizon-client-core/src/component.rs b/crates/horizon-client-core/src/component.rs index afb68fa98..0b2ca918a 100644 --- a/crates/horizon-client-core/src/component.rs +++ b/crates/horizon-client-core/src/component.rs @@ -182,6 +182,7 @@ fn object_style(o: &serde_json::Map) -> Style { }) }) .map(crate::text::ClickEvent::OpenUrl), + Some("run_command") => text("command").map(crate::text::ClickEvent::RunCommand), // ClickEvent.ChangePage.CODEC: ExtraCodecs.POSITIVE_INT. Some("change_page") => e .get("page") diff --git a/crates/horizon-client-core/src/text.rs b/crates/horizon-client-core/src/text.rs index 3a0bf301b..cf99a94be 100644 --- a/crates/horizon-client-core/src/text.rs +++ b/crates/horizon-client-core/src/text.rs @@ -46,6 +46,8 @@ pub enum ClickEvent { OpenUrl(String), /// `ClickEvent.ChangePage`: a book's page, counted from 1. ChangePage(i32), + /// `ClickEvent.RunCommand`: the command the click sends (`/` allowed). + RunCommand(String), SuggestCommand(String), CopyToClipboard(String), /// A local file to open with the system handler. @@ -535,6 +537,9 @@ fn style_json(style: &Style) -> Vec<(String, Value)> { let event = match click { ClickEvent::OpenUrl(url) => json!({ "action": "open_url", "url": url }), ClickEvent::ChangePage(page) => json!({ "action": "change_page", "page": page }), + ClickEvent::RunCommand(command) => { + json!({ "action": "run_command", "command": command }) + } ClickEvent::SuggestCommand(command) => { json!({ "action": "suggest_command", "command": command }) } diff --git a/crates/horizon-client/src/game/books.rs b/crates/horizon-client/src/game/books.rs index 07d65c5a4..4c1a03129 100644 --- a/crates/horizon-client/src/game/books.rs +++ b/crates/horizon-client/src/game/books.rs @@ -76,9 +76,11 @@ fn view_pages(data: Option<&StackData>) -> Vec { .iter() .enumerate() .map(|(i, page)| match tagged.then(|| &w.page_tags[i]) { - Some(tag @ horizon_world::nbt::Tag::Compound(_)) => { - horizon_client::entity_nbt::name_text(tag) - } + // A component object, or a component list (`ComponentSerialization` + // takes both; servers behind ViaVersion send the list form). + Some( + tag @ (horizon_world::nbt::Tag::Compound(_) | horizon_world::nbt::Tag::List(_)), + ) => horizon_client::entity_nbt::name_text(tag), _ => Text::literal(page), }) .collect() @@ -137,6 +139,18 @@ impl Game<'_> { } } ClickEvent::OpenUrl(url) => self.click_url(url), + ClickEvent::RunCommand(command) => self.click_command(&command), + ClickEvent::SuggestCommand(command) => { + // defaultHandleClickEvent: the chat screen takes it over. + if self.player.is_some() { + self.close_book(); + self.open_chat(&command); + } + } + ClickEvent::CopyToClipboard(value) => { + use horizon_client::clipboard::Clipboard; + self.clipboard.set(&value); + } _ => {} } } diff --git a/crates/horizon-client/src/game/chat.rs b/crates/horizon-client/src/game/chat.rs index 9fbc5cd88..f19410545 100644 --- a/crates/horizon-client/src/game/chat.rs +++ b/crates/horizon-client/src/game/chat.rs @@ -764,6 +764,7 @@ impl Game<'_> { super::packs::open_folder(std::path::Path::new(&path)); } Some(ClickEvent::OpenUrl(url)) => self.click_url(url), + Some(ClickEvent::RunCommand(command)) => self.click_command(&command), Some(ClickEvent::SuggestCommand(command)) => { if let Some(screen) = self.ui.chat_screen.as_mut() { screen.suggest_command(&m, &c, &command); @@ -773,6 +774,20 @@ impl Game<'_> { Some(ClickEvent::ChangePage(_)) | None => {} } } + /// `Screen.clickCommandAction`: a client mod's command first, then the + /// server (unsigned) or the local one runs it, without the chat history + /// a typed line would get. + pub(super) fn click_command(&mut self, command: &str) { + let command = command.strip_prefix('/').unwrap_or(command); + if command.is_empty() || self.mod_command(command) { + return; + } + if self.remote.is_some() { + self.send_chat(&format!("/{command}")); + } else if !self.local_server_mod_command(command) { + self.run_command(command); + } + } /// `Screen.clickUrlAction`: nothing with Web Links off, else the link /// opens, after the confirmation unless Prompt on Links is off. pub(super) fn click_url(&mut self, url: String) { From 46c84a8b2413978de2a57301b7126b03b58b5720 Mon Sep 17 00:00:00 2001 From: Ludovic Marie <136255825+ludovic111@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:27:03 +0100 Subject: [PATCH 2/2] Merge main into #2891: main has run_command click events since Keep main's ClickEvent::RunCommand and click_command (now pub(super) for the book screen) instead of the branch's copies. Co-Authored-By: Claude Opus 5.5 --- crates/horizon-client-core/src/component.rs | 1 - crates/horizon-client-core/src/text.rs | 5 ----- crates/horizon-client/src/game/chat.rs | 17 +---------------- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/crates/horizon-client-core/src/component.rs b/crates/horizon-client-core/src/component.rs index dbedc0513..248c78617 100644 --- a/crates/horizon-client-core/src/component.rs +++ b/crates/horizon-client-core/src/component.rs @@ -183,7 +183,6 @@ fn object_style(o: &serde_json::Map) -> Style { }) }) .map(crate::text::ClickEvent::OpenUrl), - Some("run_command") => text("command").map(crate::text::ClickEvent::RunCommand), // ClickEvent.ChangePage.CODEC: ExtraCodecs.POSITIVE_INT. Some("change_page") => e .get("page") diff --git a/crates/horizon-client-core/src/text.rs b/crates/horizon-client-core/src/text.rs index 4fcd0fad5..243a3ae3c 100644 --- a/crates/horizon-client-core/src/text.rs +++ b/crates/horizon-client-core/src/text.rs @@ -46,8 +46,6 @@ pub enum ClickEvent { OpenUrl(String), /// `ClickEvent.ChangePage`: a book's page, counted from 1. ChangePage(i32), - /// `ClickEvent.RunCommand`: the command the click sends (`/` allowed). - RunCommand(String), SuggestCommand(String), /// `ClickEvent.RunCommand`: the command, with or without its `/`. RunCommand(String), @@ -550,9 +548,6 @@ fn style_json(style: &Style) -> Vec<(String, Value)> { let event = match click { ClickEvent::OpenUrl(url) => json!({ "action": "open_url", "url": url }), ClickEvent::ChangePage(page) => json!({ "action": "change_page", "page": page }), - ClickEvent::RunCommand(command) => { - json!({ "action": "run_command", "command": command }) - } ClickEvent::SuggestCommand(command) => { json!({ "action": "suggest_command", "command": command }) } diff --git a/crates/horizon-client/src/game/chat.rs b/crates/horizon-client/src/game/chat.rs index ff25d9a34..4ed7a0571 100644 --- a/crates/horizon-client/src/game/chat.rs +++ b/crates/horizon-client/src/game/chat.rs @@ -780,7 +780,6 @@ impl Game<'_> { super::packs::open_folder(std::path::Path::new(&path)); } Some(ClickEvent::OpenUrl(url)) => self.click_url(url), - Some(ClickEvent::RunCommand(command)) => self.click_command(&command), Some(ClickEvent::SuggestCommand(command)) => { if let Some(screen) = self.ui.chat_screen.as_mut() { screen.suggest_command(&m, &c, &command); @@ -791,20 +790,6 @@ impl Game<'_> { Some(ClickEvent::ChangePage(_)) | None => {} } } - /// `Screen.clickCommandAction`: a client mod's command first, then the - /// server (unsigned) or the local one runs it, without the chat history - /// a typed line would get. - pub(super) fn click_command(&mut self, command: &str) { - let command = command.strip_prefix('/').unwrap_or(command); - if command.is_empty() || self.mod_command(command) { - return; - } - if self.remote.is_some() { - self.send_chat(&format!("/{command}")); - } else if !self.local_server_mod_command(command) { - self.run_command(command); - } - } /// `Screen.clickUrlAction`: nothing with Web Links off, else the link /// opens, after the confirmation unless Prompt on Links is off. pub(super) fn click_url(&mut self, url: String) { @@ -874,7 +859,7 @@ impl Game<'_> { /// first, as `ClientPacketListener.sendUnattendedCommand` lets client /// commands take it. (Vanilla asks first when the command fails to /// parse; here the server answers it.) - fn click_command(&mut self, command: &str) { + pub(super) fn click_command(&mut self, command: &str) { let command = command.strip_prefix('/').unwrap_or(command); if self.mod_command(command) { return;