From f7a67a6950b9477965c879989277cf5ce44541c7 Mon Sep 17 00:00:00 2001 From: David Abram Date: Fri, 16 Oct 2026 10:46:07 +0200 Subject: [PATCH] cli: Add linked site prompt to help banner Update the help banner with box-drawing SCE art and a linked sce.crocoder.dev visit line, while preserving plain rendering when color is disabled. Document the banner and hyperlink behavior in the CLI context. --- cli/src/command_surface.rs | 28 ++++++++++++++++++++-------- cli/src/services/style.rs | 30 ++++++++++++++++++++++++++++++ context/cli/cli-command-surface.md | 2 +- context/cli/styling-service.md | 1 + 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/cli/src/command_surface.rs b/cli/src/command_surface.rs index 85d0a3601..f47cc52eb 100644 --- a/cli/src/command_surface.rs +++ b/cli/src/command_surface.rs @@ -1,16 +1,20 @@ use std::fmt::Write; use crate::{cli_schema, services}; -use services::style::{banner_with_gradient, command_name, heading}; +use services::style::{banner_link, banner_with_gradient, command_name, heading}; const SCE_BANNER_LINES: &[&str] = &[ - r" ______ ______ ________ ", - r".' ____ \ .' ___ ||_ __ | ", - r"| (___ \_|/ .' \_| | |_ \_| ", - r" _.____`. | | | _| _ ", - r"| \____) |\ `.___.'\ _| |__/ | ", - r" \______.' `.____ .'|________| ", + "███████╗ ██████╗███████╗", + "██╔════╝██╔════╝██╔════╝", + "███████╗██║ █████╗", + "╚════██║██║ ██╔══╝", + "███████║╚██████╗███████╗", + "╚══════╝ ╚═════╝╚══════╝", ]; +const SCE_BANNER_VISIT_PREFIX: &str = "Visit "; +const SCE_BANNER_VISIT_LINK_TEXT: &str = "sce.crocoder.dev"; +const SCE_BANNER_VISIT_LINK_URL: &str = "https://sce.crocoder.dev"; +const SCE_BANNER_VISIT_SUFFIX: &str = " to learn more."; const HELP_COMMAND_NAME: &str = "help"; const HELP_COMMAND_PURPOSE: &str = "Show help for the current CLI surface"; @@ -130,10 +134,18 @@ fn push_section(out: &mut String, section: &str) { out.push_str(section); } +fn sce_banner() -> String { + format!( + "{}\n{SCE_BANNER_VISIT_PREFIX}{}{SCE_BANNER_VISIT_SUFFIX}", + banner_with_gradient(SCE_BANNER_LINES), + banner_link(SCE_BANNER_VISIT_LINK_TEXT, SCE_BANNER_VISIT_LINK_URL) + ) +} + pub fn help_text() -> String { let mut output = String::new(); - push_section(&mut output, &banner_with_gradient(SCE_BANNER_LINES)); + push_section(&mut output, &sce_banner()); push_blank_line(&mut output); push_blank_line(&mut output); diff --git a/cli/src/services/style.rs b/cli/src/services/style.rs index 329f22ffb..903c884eb 100644 --- a/cli/src/services/style.rs +++ b/cli/src/services/style.rs @@ -257,6 +257,18 @@ fn is_help_placeholder(token: &str) -> bool { .any(|ch| ch.is_ascii_uppercase() || matches!(ch, '<' | '>' | '[' | ']')) } +#[must_use] +pub fn banner_link(text: &str, url: &str) -> String { + banner_link_with_color_policy(text, url, supports_color()) +} + +#[must_use] +pub(crate) fn banner_link_with_color_policy(text: &str, url: &str, color_enabled: bool) -> String { + style_if(text, color_enabled, |s| { + format!("\x1b]8;;{url}\x1b\\{}\x1b]8;;\x1b\\", s.cyan().underline()) + }) +} + /// Gradient color endpoints for the ASCII art banner. /// Right side: cyan (0, 255, 255), Left side: magenta (255, 0, 255). const GRADIENT_COLOR_RIGHT: (u8, u8, u8) = (0, 255, 255); @@ -359,6 +371,24 @@ mod tests { assert!(styled.contains("SCE-ERR-RUNTIME")); } + #[test] + fn banner_link_wraps_cyan_underlined_text_in_hyperlink_when_color_enabled() { + let styled = + banner_link_with_color_policy("sce.crocoder.dev", "https://sce.crocoder.dev", true); + assert_eq!( + styled, + "\x1b]8;;https://sce.crocoder.dev\x1b\\\x1b[4m\x1b[36msce.crocoder.dev\x1b[39m\x1b[0m\x1b]8;;\x1b\\" + ); + } + + #[test] + fn banner_link_is_plain_when_color_disabled() { + assert_eq!( + banner_link_with_color_policy("sce.crocoder.dev", "https://sce.crocoder.dev", false), + "sce.crocoder.dev" + ); + } + #[test] fn error_code_is_plain_when_color_disabled() { assert_eq!( diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index ac4d73b5d..403828c63 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -85,7 +85,7 @@ Non-interactive target setup never prompts. A missing config receives explicit ` `sce`, `sce help`, and `sce --help` now render a top-level help surface that starts with an ASCII art "SCE" banner followed by a slim command list: - the banner uses a per-column right-to-left color gradient (cyan on the right, magenta on the left) when stdout color is enabled, and renders as plain ASCII when color is disabled (non-TTY or `NO_COLOR`) -- the banner is rendered by `command_surface::help_text()` calling `style::banner_with_gradient(SCE_BANNER_LINES)` before the heading +- the banner is the box-drawing "SCE" art from `SCE_BANNER_LINES`, rendered by `command_surface::help_text()` via `sce_banner()`, which applies `style::banner_with_gradient` and adds a `Visit sce.crocoder.dev to learn more.` line where `sce.crocoder.dev` is cyan, underlined, and an OSC 8 hyperlink to `https://sce.crocoder.dev` (`style::banner_link`); all of these render as plain text when color is disabled - the visible real-command rows are sourced from `cli_schema::TOP_LEVEL_COMMANDS`, so top-level purpose text and help visibility are defined once for both help rendering and known-command classification - the visible command list is `help`, `auth`, `config`, `setup`, `doctor`, `sync`, `version`, and `completion` - top-level help omits implemented/placeholder labels diff --git a/context/cli/styling-service.md b/context/cli/styling-service.md index de0d43f0a..3f163c8a8 100644 --- a/context/cli/styling-service.md +++ b/context/cli/styling-service.md @@ -45,6 +45,7 @@ The CLI styling service in `cli/src/services/style.rs` provides deterministic te - `banner_with_gradient(lines: &[&str]) -> String` - Applies a per-column right-to-left color gradient (cyan on the right, magenta on the left) to ASCII art banner lines when color is enabled; returns plain ASCII when color is disabled; spaces in the banner are left unstyled to avoid trailing-space ANSI artifacts - `banner_with_gradient_with_color_policy(lines: &[&str], color_enabled: bool) -> String` - Internal variant accepting an explicit color policy flag for testability +- `banner_link(text: &str, url: &str) -> String` - Cyan, underlined text wrapped in an OSC 8 terminal hyperlink to `url`; plain text without the hyperlink when color is disabled ### Policy