mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
Prefer CODE_VERSION with fallback to CARGO_PKG_VERSION across codex-rs\n\n- Replace direct env!("CARGO_PKG_VERSION") with option_env!("CODE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))\n- Update clap version field, TUI upgrade banners, user agent, MCP client init, and OpenAI header\n- Verified via ./build-fast.sh
This commit is contained in:
@@ -40,17 +40,13 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "Current version in package.json: $CURRENT_VERSION"
|
||||
PUBLISHED_VERSION=$(npm view @just-every/code version 2>/dev/null || echo "0.0.0")
|
||||
echo "Latest published version: $PUBLISHED_VERSION"
|
||||
|
||||
if [ "$CURRENT_VERSION" = "$PUBLISHED_VERSION" ] || [ "$CURRENT_VERSION" \< "$PUBLISHED_VERSION" ]; then
|
||||
IFS='.' read -ra V <<< "$PUBLISHED_VERSION"
|
||||
NEW_VERSION="${V[0]}.${V[1]}.$((${V[2]} + 1))"
|
||||
else
|
||||
NEW_VERSION=$CURRENT_VERSION
|
||||
fi
|
||||
|
||||
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build-binaries:
|
||||
@@ -89,7 +85,6 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Install Rust (needed before using cargo)
|
||||
- name: Install Rust (minimal)
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -97,59 +92,24 @@ jobs:
|
||||
rustup toolchain install 1.89.0 --profile minimal --target ${{ matrix.target }}
|
||||
rustup default 1.89.0
|
||||
|
||||
# 🔧 Update workspace package version in Cargo.toml (portable)
|
||||
# Prefer [workspace.package], fallback to [package] if present
|
||||
- name: Set crate version in Cargo.toml (portable)
|
||||
shell: bash
|
||||
working-directory: codex-rs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ needs.determine-version.outputs.version }}"
|
||||
echo "Embedding version ${VERSION} into codex-rs/Cargo.toml"
|
||||
awk -v ver="$VERSION" '
|
||||
BEGIN{in_ws_pkg=0; in_pkg=0; done=0}
|
||||
/^\[workspace\.package\]/{in_ws_pkg=1; in_pkg=0}
|
||||
/^\[package\]/{in_pkg=1; in_ws_pkg=0}
|
||||
/^\[/{ if ($0 !~ /^\[(workspace\.package|package)\]/) { in_ws_pkg=0; in_pkg=0 } }
|
||||
{
|
||||
if ((in_ws_pkg || in_pkg) && !done && $0 ~ /^version[[:space:]]*=/) {
|
||||
print "version = \"" ver "\""; done=1; next
|
||||
}
|
||||
print
|
||||
}
|
||||
' Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml
|
||||
echo "New version line:" && awk '/^\[(workspace\.package|package)\]/{in=1; next} /^\[/{in=0} in && /^version[[:space:]]*=/{print; exit}' Cargo.toml || true
|
||||
|
||||
# 🔒 Keep lockfile consistent for --frozen without changing deps
|
||||
- name: Regenerate lockfile for --frozen
|
||||
shell: bash
|
||||
working-directory: codex-rs
|
||||
run: cargo generate-lockfile
|
||||
|
||||
# Cache target/ + registries (to skip linking when unchanged)
|
||||
# Keep target/ across runs so Cargo can no-op when code hasn't changed
|
||||
- name: Setup Rust Cache (target + registries)
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: v4-rust
|
||||
shared-key: codex-${{ matrix.target }}-rust-1.89-target
|
||||
prefix-key: v5-rust
|
||||
shared-key: codex-${{ matrix.target }}-toolchain-1.89
|
||||
workspaces: |
|
||||
codex-rs -> target
|
||||
cache-targets: true
|
||||
cache-workspace-crates: true
|
||||
cache-on-failure: true
|
||||
|
||||
# Stable sccache namespace per toolchain+target
|
||||
- name: Configure sccache cache keys
|
||||
shell: bash
|
||||
run: echo "SCCACHE_GHA_VERSION=${{ matrix.target }}-rust-1.89-v4" >> "$GITHUB_ENV"
|
||||
|
||||
# sccache: skip compiles when possible (doesn't skip linking)
|
||||
- name: Setup sccache (GHA backend)
|
||||
uses: mozilla-actions/[email protected]
|
||||
with:
|
||||
version: v0.10.0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Enable sccache for Rust compiles
|
||||
- name: Enable sccache
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -158,18 +118,16 @@ jobs:
|
||||
echo "SCCACHE_IDLE_TIMEOUT=1800" >> "$GITHUB_ENV"
|
||||
echo "SCCACHE_CACHE_SIZE=10G" >> "$GITHUB_ENV"
|
||||
|
||||
# ---------------------------
|
||||
# Platform-specific tuning
|
||||
# ---------------------------
|
||||
# -------- Platform tuning (minimal, proven) --------
|
||||
|
||||
# Linux GNU: mold linker + C/C++ through sccache (clang preferred)
|
||||
# Linux GNU: use mold if available; prefer system OpenSSL
|
||||
- name: Linux (gnu) tuning
|
||||
if: contains(matrix.os, 'ubuntu') && contains(matrix.target, 'gnu')
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y mold || true
|
||||
sudo apt-get install -y libssl-dev pkg-config mold || true
|
||||
if command -v clang >/dev/null 2>&1; then
|
||||
echo 'CC=sccache clang' >> "$GITHUB_ENV"
|
||||
echo 'CXX=sccache clang++' >> "$GITHUB_ENV"
|
||||
@@ -177,9 +135,10 @@ jobs:
|
||||
echo 'CC=sccache gcc' >> "$GITHUB_ENV"
|
||||
echo 'CXX=sccache g++' >> "$GITHUB_ENV"
|
||||
fi
|
||||
echo 'OPENSSL_NO_VENDOR=1' >> "$GITHUB_ENV"
|
||||
echo 'RUSTFLAGS=-Awarnings -C link-arg=-fuse-ld=mold -C debuginfo=0 -C strip=symbols -C panic=abort' >> "$GITHUB_ENV"
|
||||
|
||||
# Linux MUSL: build & link with musl-gcc to avoid glibc symbol leakage
|
||||
# Linux MUSL: reliable static build via musl-gcc (no glibc symbol leaks)
|
||||
- name: Linux (musl) tuning
|
||||
if: contains(matrix.os, 'ubuntu') && contains(matrix.target, 'musl')
|
||||
shell: bash
|
||||
@@ -187,9 +146,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools pkg-config
|
||||
# Use musl headers + libs for all C compiles (no sccache wrapper here for reliability)
|
||||
echo 'CC=musl-gcc' >> "$GITHUB_ENV"
|
||||
# Ensure rustc links with musl-gcc (avoids __isoc23_strtol glibc symbol)
|
||||
case "${{ matrix.target }}" in
|
||||
x86_64-unknown-linux-musl) echo 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc' >> "$GITHUB_ENV" ;;
|
||||
aarch64-unknown-linux-musl) echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc' >> "$GITHUB_ENV" ;;
|
||||
@@ -198,32 +155,23 @@ jobs:
|
||||
echo 'OPENSSL_STATIC=1' >> "$GITHUB_ENV"
|
||||
echo 'RUSTFLAGS=-Awarnings -C debuginfo=0 -C strip=symbols -C panic=abort' >> "$GITHUB_ENV"
|
||||
|
||||
# macOS: Homebrew LLVM + LLD; C/C++ through sccache; link via ld64.lld
|
||||
- name: macOS toolchain & linker (brew clang + lld via sccache)
|
||||
# macOS: stick with Apple toolchain to avoid brew overhead; still cache C via sccache
|
||||
- name: macOS tuning
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
brew install llvm lld || true
|
||||
LLVM_PREFIX="$(brew --prefix llvm || true)"
|
||||
LLD_PREFIX="$(brew --prefix lld || true)"
|
||||
[[ -n "${LLVM_PREFIX}" && -n "${LLD_PREFIX}" ]] || { echo "brew llvm/lld not found"; exit 1; }
|
||||
echo "${LLVM_PREFIX}/bin" >> "$GITHUB_PATH"
|
||||
echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> "$GITHUB_ENV"
|
||||
echo "CC=sccache clang" >> "$GITHUB_ENV"
|
||||
echo "CXX=sccache clang++" >> "$GITHUB_ENV"
|
||||
echo "RUSTFLAGS=-Awarnings -Clinker=clang -C link-arg=-fuse-ld=${LLD_PREFIX}/bin/ld64.lld -C debuginfo=0 -C strip=symbols -C panic=abort" >> "$GITHUB_ENV"
|
||||
echo 'CC=sccache clang' >> "$GITHUB_ENV"
|
||||
echo 'CXX=sccache clang++' >> "$GITHUB_ENV"
|
||||
echo 'RUSTFLAGS=-Awarnings -C debuginfo=0 -C strip=symbols -C panic=abort' >> "$GITHUB_ENV"
|
||||
|
||||
# Windows: lld-link + prebuilt OpenSSL via vcpkg (cached)
|
||||
- name: Cache vcpkg (Windows)
|
||||
# Windows: robust vcpkg with binary caching (x-gha)
|
||||
- name: Cache vcpkg data (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~\vcpkg\installed
|
||||
~\vcpkg\packages
|
||||
~\vcpkg\downloads
|
||||
key: vcpkg-openssl-${{ runner.os }}-x64-windows
|
||||
~\vcpkg
|
||||
key: vcpkg-root-${{ runner.os }}
|
||||
|
||||
- name: Prepare vcpkg (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -232,9 +180,9 @@ jobs:
|
||||
$Root = Join-Path $env:USERPROFILE 'vcpkg'
|
||||
if (-not (Test-Path $Root)) { New-Item -ItemType Directory -Path $Root | Out-Null }
|
||||
if (-not (Test-Path (Join-Path $Root '.git'))) {
|
||||
git -C $Root init
|
||||
git -C $Root remote remove origin 2>$null
|
||||
git -C $Root remote add origin https://github.com/microsoft/vcpkg
|
||||
if (Test-Path (Join-Path $Root 'src')) { Remove-Item -Recurse -Force (Join-Path $Root 'src') }
|
||||
git clone https://github.com/microsoft/vcpkg $Root
|
||||
} else {
|
||||
git -C $Root fetch --depth 1 origin
|
||||
git -C $Root checkout -q FETCH_HEAD
|
||||
}
|
||||
@@ -243,25 +191,26 @@ jobs:
|
||||
}
|
||||
"VCPKG_ROOT=$Root" >> $env:GITHUB_ENV
|
||||
"VCPKG_DEFAULT_TRIPLET=x64-windows" >> $env:GITHUB_ENV
|
||||
# Enable true binary caching via GitHub Actions cache service
|
||||
"VCPKG_FEATURE_FLAGS=manifests,binarycaching" >> $env:GITHUB_ENV
|
||||
"VCPKG_BINARY_SOURCES=clear;x-gha,readwrite" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Enable lld-link (Windows) + install OpenSSL via vcpkg
|
||||
- name: Windows OpenSSL via vcpkg + linker flags
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Faster linker and lean release flags
|
||||
"RUSTFLAGS=-Clinker=lld-link -C codegen-units=16 -C debuginfo=0 -C strip=symbols -C panic=abort -C link-arg=/OPT:REF -C link-arg=/OPT:ICF -C link-arg=/DEBUG:NONE" >> $env:GITHUB_ENV
|
||||
# Ensure OpenSSL from vcpkg is used (no vendoring)
|
||||
& "$env:VCPKG_ROOT\vcpkg.exe" install openssl:x64-windows
|
||||
"OPENSSL_NO_VENDOR=1" >> $env:GITHUB_ENV
|
||||
"RUSTFLAGS=-Awarnings -Clinker=lld-link -C codegen-units=16 -C debuginfo=0 -C strip=symbols -C panic=abort -C link-arg=/OPT:REF -C link-arg=/OPT:ICF -C link-arg=/DEBUG:NONE" >> $env:GITHUB_ENV
|
||||
|
||||
# Prefetch deps so --frozen works offline (needed for git deps like ratatui)
|
||||
# Prefetch deps so --frozen works even with git deps
|
||||
- name: Prefetch dependencies (git + registry)
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
run: cargo fetch --locked
|
||||
|
||||
# Pass chosen npm version into the Rust build (optional build.rs exposes it)
|
||||
# Inject the display version without touching Cargo manifests
|
||||
- name: Export CODE_VERSION for Rust build
|
||||
shell: bash
|
||||
run: echo "CODE_VERSION=${{ needs.determine-version.outputs.version }}" >> "$GITHUB_ENV"
|
||||
@@ -269,17 +218,15 @@ jobs:
|
||||
- name: Build binaries (with timings)
|
||||
shell: bash
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0" # keep off in CI; release profile + sccache benefits
|
||||
CARGO_INCREMENTAL: "0" # keep off in CI; release builds + sccache
|
||||
RUST_BACKTRACE: "1"
|
||||
run: |
|
||||
cd codex-rs
|
||||
cargo build --release --frozen --locked --timings --target ${{ matrix.target }} --bin code --bin code-tui --bin code-exec
|
||||
|
||||
- name: sccache stats (human + json)
|
||||
- name: sccache stats
|
||||
shell: bash
|
||||
run: |
|
||||
sccache --show-stats || true
|
||||
sccache --show-stats --stats-format=json || true
|
||||
run: sccache --show-stats || true
|
||||
|
||||
- name: Prepare artifacts
|
||||
shell: bash
|
||||
@@ -423,4 +370,4 @@ jobs:
|
||||
files: |
|
||||
release-assets/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
|
||||
@@ -26,7 +26,7 @@ use crate::proto::ProtoCli;
|
||||
#[clap(
|
||||
author,
|
||||
name = "code",
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
version = option_env!("CODE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")),
|
||||
// If a sub‑command is given, ignore requirements of the default args.
|
||||
subcommand_negates_reqs = true,
|
||||
// The executable is sometimes invoked via a platform‑specific name like
|
||||
|
||||
@@ -148,7 +148,9 @@ impl McpConnectionManager {
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "code-mcp-client".to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
version: option_env!("CODE_VERSION")
|
||||
.unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||
.to_owned(),
|
||||
title: Some("Codex".into()),
|
||||
},
|
||||
protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(),
|
||||
|
||||
@@ -253,9 +253,16 @@ pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: Some(
|
||||
[("version".to_string(), env!("CARGO_PKG_VERSION").to_string())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
[
|
||||
(
|
||||
"version".to_string(),
|
||||
option_env!("CODE_VERSION")
|
||||
.unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||
.to_string(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
env_http_headers: Some(
|
||||
[
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
|
||||
|
||||
pub fn get_codex_user_agent(originator: Option<&str>) -> String {
|
||||
let build_version = env!("CARGO_PKG_VERSION");
|
||||
let build_version = option_env!("CODE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
|
||||
let os_info = os_info::get();
|
||||
format!(
|
||||
"{}/{build_version} ({} {}; {}) {}",
|
||||
|
||||
@@ -138,11 +138,11 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
/// for the session. This mirrors the information shown in the TUI welcome
|
||||
/// screen.
|
||||
fn print_config_summary(&mut self, config: &Config, prompt: &str) {
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
let version = option_env!("CODE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
|
||||
ts_println!(
|
||||
self,
|
||||
"Code v{}\n--------",
|
||||
VERSION
|
||||
version
|
||||
);
|
||||
|
||||
let entries = create_config_summary_entries(config);
|
||||
|
||||
@@ -62,7 +62,9 @@ async fn main() -> Result<()> {
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "code-mcp-client".to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
version: option_env!("CODE_VERSION")
|
||||
.unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||
.to_owned(),
|
||||
title: Some("Code".to_string()),
|
||||
},
|
||||
protocol_version: MCP_SCHEMA_VERSION.to_owned(),
|
||||
|
||||
@@ -225,7 +225,9 @@ impl MessageProcessor {
|
||||
protocol_version: params.protocol_version.clone(),
|
||||
server_info: mcp_types::Implementation {
|
||||
name: "code-mcp-server".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
version: option_env!("CODE_VERSION")
|
||||
.unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||
.to_string(),
|
||||
title: Some("Code".to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -227,7 +227,7 @@ pub async fn run_main(
|
||||
#[allow(clippy::print_stderr)]
|
||||
#[cfg(not(debug_assertions))]
|
||||
if let Some(latest_version) = updates::get_upgrade_version(&config) {
|
||||
let current_version = env!("CARGO_PKG_VERSION");
|
||||
let current_version = option_env!("CODE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
|
||||
let exe = std::env::current_exe()?;
|
||||
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
|
||||
|
||||
@@ -287,7 +287,7 @@ fn run_ratatui_app(
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
|
||||
let current_version = env!("CARGO_PKG_VERSION");
|
||||
let current_version = option_env!("CODE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
|
||||
let exe = std::env::current_exe()?;
|
||||
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ fn append_markdown_with_opener_and_cwd_and_bold(
|
||||
for seg in split_text_and_fences(markdown_source) {
|
||||
match seg {
|
||||
Segment::Text(s) => {
|
||||
// Rewrite our special file citation tokens into markdown links
|
||||
// (e.g., vscode://file...). These will later be turned
|
||||
// into OSC8 hyperlinks by the markdown renderer.
|
||||
let processed = rewrite_file_citations(&s, file_opener, cwd);
|
||||
let rendered = if bold_first_sentence {
|
||||
MarkdownRenderer::render_with_bold_first_sentence(&processed)
|
||||
@@ -127,7 +130,6 @@ fn rewrite_file_citations<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
// use shared helper from `line_utils`
|
||||
|
||||
// Minimal code block splitting.
|
||||
// - Recognizes fenced blocks opened by ``` or ~~~ (allowing leading whitespace).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use regex_lite::Regex;
|
||||
|
||||
/// Custom markdown renderer with full control over spacing and styling
|
||||
pub struct MarkdownRenderer {
|
||||
@@ -397,7 +398,9 @@ impl MarkdownRenderer {
|
||||
|
||||
fn flush_current_line(&mut self) {
|
||||
if !self.current_line.is_empty() {
|
||||
self.lines.push(Line::from(self.current_line.clone()));
|
||||
// Autolink URLs and markdown links inside the accumulated spans.
|
||||
let linked = autolink_spans(std::mem::take(&mut self.current_line));
|
||||
self.lines.push(Line::from(linked));
|
||||
self.current_line.clear();
|
||||
}
|
||||
}
|
||||
@@ -416,6 +419,120 @@ impl MarkdownRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// Turn inline markdown links and bare URLs into OSC 8 hyperlinks.
|
||||
// This runs late, on the already-styled spans for a line, so it preserves
|
||||
// bold/italic styling while adding underlines for links and embedding the
|
||||
// hyperlink escape sequences in the text.
|
||||
fn autolink_spans(spans: Vec<Span<'static>>) -> Vec<Span<'static>> {
|
||||
// Patterns: markdown [label](target), explicit http(s) URLs, and plain domains.
|
||||
// Keep conservative to avoid false positives.
|
||||
static MD_LINK_RE: once_cell::sync::OnceCell<Regex> = once_cell::sync::OnceCell::new();
|
||||
static EXPL_URL_RE: once_cell::sync::OnceCell<Regex> = once_cell::sync::OnceCell::new();
|
||||
static DOMAIN_RE: once_cell::sync::OnceCell<Regex> = once_cell::sync::OnceCell::new();
|
||||
let md_re = MD_LINK_RE
|
||||
.get_or_init(|| Regex::new(r"\[([^\]]+)\]\(([^)\s]+)\)").unwrap());
|
||||
let url_re = EXPL_URL_RE
|
||||
.get_or_init(|| Regex::new(r"(?i)\bhttps?://[^\s)]+").unwrap());
|
||||
let dom_re = DOMAIN_RE.get_or_init(|| {
|
||||
// e.g., apps.shopify.com or foo.example.io/path?x=1
|
||||
Regex::new(r"\b([a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+(?:/[\w\-./?%&=#]*)?)")
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let mut out: Vec<Span<'static>> = Vec::with_capacity(spans.len());
|
||||
for s in spans {
|
||||
let text = s.content.clone();
|
||||
let mut cursor = 0usize;
|
||||
let mut changed = false;
|
||||
|
||||
// Scan left-to-right, preferring markdown links first (explicit intent),
|
||||
// then explicit URLs, then bare domains.
|
||||
while cursor < text.len() {
|
||||
let after = &text[cursor..];
|
||||
|
||||
// 1) markdown links
|
||||
if let Some(m) = md_re.find(after) {
|
||||
let start = cursor + m.start();
|
||||
let end = cursor + m.end();
|
||||
// Push any plain prefix
|
||||
if cursor < start {
|
||||
let mut span = s.clone();
|
||||
span.content = text[cursor..start].to_string().into();
|
||||
out.push(span);
|
||||
}
|
||||
let caps = md_re.captures(&text[start..end]).unwrap();
|
||||
let label = caps.get(1).unwrap().as_str();
|
||||
let target = caps.get(2).unwrap().as_str();
|
||||
out.push(hyperlink_span(label, target, &s));
|
||||
cursor = end;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2) explicit http(s) URLs
|
||||
if let Some(m) = url_re.find(after) {
|
||||
let start = cursor + m.start();
|
||||
let end = cursor + m.end();
|
||||
if cursor < start {
|
||||
let mut span = s.clone();
|
||||
span.content = text[cursor..start].to_string().into();
|
||||
out.push(span);
|
||||
}
|
||||
let url = &text[start..end];
|
||||
out.push(hyperlink_span(url, url, &s));
|
||||
cursor = end;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3) bare domain (assume https)
|
||||
if let Some(m) = dom_re.find(after) {
|
||||
let start = cursor + m.start();
|
||||
let end = cursor + m.end();
|
||||
if cursor < start {
|
||||
let mut span = s.clone();
|
||||
span.content = text[cursor..start].to_string().into();
|
||||
out.push(span);
|
||||
}
|
||||
let dom = &text[start..end];
|
||||
// Skip if it obviously looks like an email or already in a link
|
||||
if !dom.contains('@') {
|
||||
let target = format!("https://{dom}");
|
||||
out.push(hyperlink_span(dom, &target, &s));
|
||||
cursor = end;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// No more matches
|
||||
break;
|
||||
}
|
||||
|
||||
if changed {
|
||||
if cursor < text.len() {
|
||||
let mut span = s.clone();
|
||||
span.content = text[cursor..].to_string().into();
|
||||
out.push(span);
|
||||
}
|
||||
} else {
|
||||
out.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn hyperlink_span(label: &str, target: &str, base: &Span<'static>) -> Span<'static> {
|
||||
// Use OSC 8 with BEL terminators (widely supported; matches existing code).
|
||||
let content = format!(
|
||||
"\u{1b}]8;;{target}\u{7}{label}\u{1b}]8;;\u{7}"
|
||||
);
|
||||
let mut style = base.style;
|
||||
style.add_modifier.insert(Modifier::UNDERLINED);
|
||||
Span::styled(content, style)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn get_upgrade_version(config: &Config) -> Option<String> {
|
||||
});
|
||||
|
||||
info.and_then(|info| {
|
||||
let current_version = env!("CARGO_PKG_VERSION");
|
||||
let current_version = option_env!("CODE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
|
||||
if is_newer(&info.latest_version, current_version).unwrap_or(false) {
|
||||
Some(info.latest_version)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user