fix(terminal): simplify layout and fix box alignment
- remove ASCII art logo, use styled text header instead - fix box_top width calculation for titled boxes - add note about character width gotchas to blog post - add new hero image for terminal UI blog post
This commit is contained in:
parent
3a5e5b05bf
commit
81fe8f0d2b
BIN
src/assets/curl-terminal.png
Normal file
BIN
src/assets/curl-terminal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 MiB |
@ -2,7 +2,7 @@
|
||||
title: "Building a Terminal UI for Your Website with Rust and Cloudflare Workers"
|
||||
description: "How we built a curl-able ANSI terminal interface for nicholai.work using Rust compiled to WebAssembly, running on Cloudflare's edge network. Browsers get the normal site, terminals get ASCII art."
|
||||
pubDate: 2026-01-20
|
||||
heroImage: "../../assets/workbench.avif"
|
||||
heroImage: "../../assets/curl-terminal.png"
|
||||
featured: true
|
||||
category: "Development"
|
||||
tags: ["Rust", "Cloudflare Workers", "WebAssembly", "Terminal UI", "Edge Computing"]
|
||||
@ -117,6 +117,12 @@ combine these with color codes and you get clean, bordered sections:
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### a note on character widths
|
||||
|
||||
we learned this the hard way: **full-block characters (`█`) and double-line box drawing (`╔`, `║`, `═`) have inconsistent display widths across terminals**. some terminals render them as double-width, others as single-width. this causes alignment issues.
|
||||
|
||||
the solution: use single-line box drawing characters (`┌`, `─`, `│`) for borders - they're reliably single-width everywhere. if you want ASCII art logos, half-block characters (`▄`, `▀`) are also safe. we ended up ditching the fancy ASCII art logo entirely and going with styled text, which works perfectly across all terminals.
|
||||
|
||||
## the worker entry point
|
||||
|
||||
the cloudflare `worker` crate provides macros for handling requests:
|
||||
|
||||
@ -21,20 +21,25 @@ pub fn horizontal_line(width: usize) -> String {
|
||||
|
||||
/// Render a box top border with optional title
|
||||
pub fn box_top(width: usize, title: Option<&str>) -> String {
|
||||
let inner_width = width - 2;
|
||||
let inner_width = width - 2; // subtract corners
|
||||
|
||||
match title {
|
||||
Some(t) => {
|
||||
let title_colored = color(&format!(" {} ", t), Colors::RED);
|
||||
let title_len = t.len() + 2; // account for spaces
|
||||
let line_len = inner_width.saturating_sub(title_len);
|
||||
dim(&format!(
|
||||
"{}{}{}{}",
|
||||
Box::TOP_LEFT,
|
||||
Box::HORIZONTAL,
|
||||
title_colored,
|
||||
format!("{}{}", dim(&horizontal_line(line_len)), dim(&Box::TOP_RIGHT.to_string()))
|
||||
))
|
||||
// Title format: ┌─ Title ───────┐
|
||||
// We need: corner + dash + space + title + space + dashes + corner
|
||||
let title_display = format!(" {} ", t);
|
||||
let title_visible_len = title_display.len(); // all ASCII, so len() works
|
||||
// Subtract: 1 for the dash before title, title_visible_len for title
|
||||
let remaining_dashes = inner_width.saturating_sub(1 + title_visible_len);
|
||||
|
||||
format!(
|
||||
"{}{}{}{}{}",
|
||||
dim(&Box::TOP_LEFT.to_string()),
|
||||
dim(&Box::HORIZONTAL.to_string()),
|
||||
color(&title_display, Colors::RED),
|
||||
dim(&horizontal_line(remaining_dashes)),
|
||||
dim(&Box::TOP_RIGHT.to_string())
|
||||
)
|
||||
}
|
||||
None => dim(&format!(
|
||||
"{}{}{}",
|
||||
@ -74,7 +79,8 @@ pub fn box_empty(width: usize) -> String {
|
||||
box_row("", width)
|
||||
}
|
||||
|
||||
/// Strip ANSI codes and return visible length
|
||||
/// Strip ANSI codes and return visible display width
|
||||
/// Accounts for double-width unicode characters (block chars, CJK, etc)
|
||||
fn strip_ansi_len(s: &str) -> usize {
|
||||
let mut len = 0;
|
||||
let mut in_escape = false;
|
||||
@ -87,13 +93,40 @@ fn strip_ansi_len(s: &str) -> usize {
|
||||
in_escape = false;
|
||||
}
|
||||
} else {
|
||||
len += 1;
|
||||
len += char_width(c);
|
||||
}
|
||||
}
|
||||
|
||||
len
|
||||
}
|
||||
|
||||
/// Get the display width of a character
|
||||
/// Half-blocks are single-width, full-blocks vary by terminal
|
||||
pub fn char_width(c: char) -> usize {
|
||||
match c {
|
||||
// Half-block characters - reliably single-width
|
||||
'▀' | '▄' | '▌' | '▐' => 1,
|
||||
// Single-line box drawing (what we use for borders) - single width
|
||||
'┌' | '┐' | '└' | '┘' | '│' | '─' | '├' | '┤' | '┬' | '┴' | '┼' => 1,
|
||||
// Most other characters are single width
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get display width of a string (accounting for ANSI codes and double-width chars)
|
||||
pub fn display_width(s: &str) -> usize {
|
||||
strip_ansi_len(s)
|
||||
}
|
||||
|
||||
/// Pad a string with spaces on the right to reach target display width
|
||||
pub fn pad_to_width(text: &str, target_width: usize) -> String {
|
||||
let current_width = display_width(text);
|
||||
if current_width >= target_width {
|
||||
return text.to_string();
|
||||
}
|
||||
format!("{}{}", text, " ".repeat(target_width - current_width))
|
||||
}
|
||||
|
||||
/// Center text within a given width
|
||||
pub fn center(text: &str, width: usize) -> String {
|
||||
let visible_len = strip_ansi_len(text);
|
||||
|
||||
@ -2,15 +2,7 @@ use super::colors::{bold_color, color, dim, Colors};
|
||||
use super::content::{Experience, NavItem, SiteContent, Skill, EXPERIENCES, NAV_ITEMS, SKILLS};
|
||||
use super::layout::{box_bottom, box_empty, box_row, box_top, center};
|
||||
|
||||
const WIDTH: usize = 67;
|
||||
|
||||
/// ASCII art logo for "VISUAL ALCHEMIST"
|
||||
const LOGO: &str = r#"██╗ ██╗██╗███████╗██╗ ██╗ █████╗ ██╗
|
||||
██║ ██║██║██╔════╝██║ ██║██╔══██╗██║
|
||||
██║ ██║██║███████╗██║ ██║███████║██║
|
||||
╚██╗ ██╔╝██║╚════██║██║ ██║██╔══██║██║
|
||||
╚████╔╝ ██║███████║╚██████╔╝██║ ██║███████╗
|
||||
╚═══╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝"#;
|
||||
const WIDTH: usize = 78;
|
||||
|
||||
/// Render the complete terminal UI
|
||||
pub fn render() -> String {
|
||||
@ -36,14 +28,11 @@ fn render_header() -> String {
|
||||
lines.push(box_top(WIDTH, None));
|
||||
lines.push(box_empty(WIDTH));
|
||||
|
||||
// Logo
|
||||
for line in LOGO.lines() {
|
||||
let centered = center(&color(line, Colors::RED), WIDTH - 4);
|
||||
lines.push(box_row(¢ered, WIDTH));
|
||||
}
|
||||
|
||||
// Subtitle
|
||||
lines.push(box_row(¢er(&bold_color("ALCHEMIST", Colors::RED), WIDTH - 4), WIDTH));
|
||||
// Title
|
||||
lines.push(box_row(
|
||||
¢er(&bold_color("VISUAL ALCHEMIST", Colors::RED), WIDTH - 4),
|
||||
WIDTH,
|
||||
));
|
||||
lines.push(box_empty(WIDTH));
|
||||
|
||||
// Name and title
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user