use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use crossterm::event::{KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
use ratatui::layout::{Position, Rect};
use ratatui::style::{Color, Modifier};
use ratatui::Terminal;
use serde_json::{json, Value};
use crate::app::Editor;
use crate::config;
use crate::config_io::DirectoryContext;
use crate::model::filesystem::{FileSystem, StdFileSystem};
const DEFAULT_SIZE: (u16, u16) = (140, 44);
pub fn build_editor(cols: u16, rows: u16, files: &[PathBuf]) -> Result<Editor> {
let dir_context = DirectoryContext::from_system()?;
let working_dir = std::env::current_dir().unwrap_or_default();
let cfg = config::Config::load_with_layers(&dir_context, &working_dir);
let fs: Arc<dyn FileSystem + Send + Sync> = Arc::new(StdFileSystem);
let mut editor = Editor::with_working_dir(
cfg,
cols,
rows,
Some(working_dir),
dir_context,
true, crate::view::color_support::ColorCapability::TrueColor,
fs,
)?;
editor.load_init_script_async(true);
editor.fire_plugins_loaded_hook();
editor.suppress_chrome_cells = true;
for f in files {
if let Err(e) = editor.open_file(f) {
eprintln!("open_file {f:?} failed: {e}");
}
}
Ok(editor)
}
pub fn apply_step(editor: &mut Editor, step: &Value) {
if let Some(s) = step.get("type").and_then(|t| t.as_str()) {
for ch in s.chars() {
apply_key(editor, &json!({ "key": ch.to_string() }));
}
} else if step.get("key").is_some() {
apply_key(editor, step);
} else if step.get("kind").is_some() {
apply_mouse(editor, step);
} else if let Some(name) = step.get("action").and_then(|a| a.as_str()) {
if let Some(act) =
crate::input::keybindings::Action::from_str(name, &std::collections::HashMap::new())
{
if let Err(e) = editor.handle_action(act) {
eprintln!("[webui] action error: {e}");
}
}
}
if let Err(e) = crate::app::editor_tick(editor, || Ok(())) {
eprintln!("[webui] editor_tick error: {e}");
}
}
pub fn scene_value(editor: &mut Editor, cols: u16, rows: u16) -> Value {
scene_json(editor, cols, rows)
}
pub fn render_tui_cells(editor: &mut Editor, cols: u16, rows: u16) -> String {
let prev = editor.suppress_chrome_cells;
editor.suppress_chrome_cells = false;
let (buf, _) = render_to_buffer(editor, cols, rows);
editor.suppress_chrome_cells = prev;
let mut out = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
out.push_str(buf.cell((x, y)).map(|c| c.symbol()).unwrap_or(" "));
}
out.push('\n');
}
out
}
pub fn run(addr: &str, files: &[PathBuf]) -> Result<()> {
let (mut cols, mut rows) = DEFAULT_SIZE;
let mut editor = build_editor(cols, rows, files)?;
let listener = TcpListener::bind(addr)?;
eprintln!("fresh web bridge on http://{addr} (real render pipeline, no mocks)");
let html_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../web-ui/index.html");
for stream in listener.incoming() {
let mut stream = match stream {
Ok(s) => s,
Err(_) => continue,
};
if let Err(e) = handle_conn(
&mut stream,
&mut editor,
html_path,
&mut cols,
&mut rows,
files,
) {
eprintln!("conn error: {e}");
}
}
Ok(())
}
fn handle_conn(
stream: &mut TcpStream,
editor: &mut Editor,
html_path: &str,
cols: &mut u16,
rows: &mut u16,
files: &[PathBuf],
) -> Result<()> {
let mut reader = BufReader::new(stream.try_clone()?);
let mut request_line = String::new();
if reader.read_line(&mut request_line)? == 0 {
return Ok(());
}
let mut it = request_line.split_whitespace();
let method = it.next().unwrap_or("");
let path = it.next().unwrap_or("/");
let mut content_length = 0usize;
loop {
let mut line = String::new();
if reader.read_line(&mut line)? == 0 || line == "\r\n" || line == "\n" {
break;
}
if let Some(v) = line.to_ascii_lowercase().strip_prefix("content-length:") {
content_length = v.trim().parse().unwrap_or(0);
}
}
let mut body = vec![0u8; content_length];
if content_length > 0 {
reader.read_exact(&mut body)?;
}
match (method, path) {
("GET", "/") => {
let html = std::fs::read_to_string(html_path)
.unwrap_or_else(|_| "<h1>web-ui/index.html not found</h1>".into());
respond(
stream,
"200 OK",
"text/html; charset=utf-8",
html.as_bytes(),
)
}
("GET", "/favicon.ico") => respond(stream, "204 No Content", "image/x-icon", b""),
("GET", "/state") => {
let s = tick_scene(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
("POST", "/key") => {
let v: Value = serde_json::from_slice(&body).unwrap_or(json!({}));
apply_key(editor, &v);
let s = tick_scene(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
("POST", "/mouse") => {
let v: Value = serde_json::from_slice(&body).unwrap_or(json!({}));
apply_mouse(editor, &v);
let s = tick_scene(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
("POST", "/action") => {
let v: Value = serde_json::from_slice(&body).unwrap_or(json!({}));
if let Some(name) = v.get("action").and_then(|a| a.as_str()) {
let args: std::collections::HashMap<String, Value> = v
.get("args")
.and_then(|a| a.as_object())
.map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default();
if let Some(act) = crate::input::keybindings::Action::from_str(name, &args) {
if let Err(e) = editor.handle_action(act) {
eprintln!("[webui] action error: {e}");
}
}
}
let s = tick_scene(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
("POST", "/widget") => {
let v: Value = serde_json::from_slice(&body).unwrap_or(json!({}));
match v.get("surface").and_then(|s| s.as_str()) {
Some("toolbar") => {
if let Some(key) = v.get("key").and_then(|k| k.as_str()) {
editor.toggle_overlay_toolbar_widget(key);
}
}
Some("panel") => {
let plugin = v.get("plugin").and_then(|p| p.as_str()).unwrap_or("");
let panel_id = v.get("panelId").and_then(|p| p.as_u64()).unwrap_or(0);
if let Some(idx) = v.get("hitIndex").and_then(|i| i.as_u64()) {
editor.deliver_widget_hit_by_index(plugin, panel_id, idx as usize);
}
}
_ => {}
}
let s = tick_scene(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
("POST", "/settings") => {
let v: Value = serde_json::from_slice(&body).unwrap_or(json!({}));
let a = v.get("a").and_then(|x| x.as_u64()).unwrap_or(0) as usize;
let bb = v.get("b").and_then(|x| x.as_u64()).unwrap_or(0) as usize;
let dbl = v.get("double").and_then(|x| x.as_bool()).unwrap_or(false);
use crate::view::settings::SettingsHit as H;
let kind = v.get("kind").and_then(|k| k.as_str()).unwrap_or("");
if kind == "entryItem" {
editor.entry_dialog_select_item(a);
let s = tick_scene(editor, *cols, *rows).to_string();
return respond(stream, "200 OK", "application/json", s.as_bytes());
}
if kind == "entryButton" {
let btn = v.get("button").and_then(|x| x.as_str()).unwrap_or("cancel");
editor.entry_dialog_activate_button(btn);
let s = tick_scene(editor, *cols, *rows).to_string();
return respond(stream, "200 OK", "application/json", s.as_bytes());
}
let hit = match kind {
"category" => Some(H::Category(a)),
"categoryDisclosure" => Some(H::CategoryDisclosure(a)),
"categorySection" => Some(H::CategorySection(a, bb)),
"item" => Some(H::Item(a)),
"controlToggle" => Some(H::ControlToggle(a)),
"controlDropdown" => Some(H::ControlDropdown(a)),
"controlDropdownOption" => Some(H::ControlDropdownOption(a, bb)),
"controlDecrement" => Some(H::ControlDecrement(a)),
"controlIncrement" => Some(H::ControlIncrement(a)),
"controlText" => Some(H::ControlText(a)),
"controlMapRow" => Some(H::ControlMapRow(a, bb)),
"controlMapAddNew" => Some(H::ControlMapAddNew(a)),
"controlTextListRow" => Some(H::ControlTextListRow(a, bb)),
"controlDualListAvailable" => Some(H::ControlDualListAvailable(a, bb)),
"controlDualListIncluded" => Some(H::ControlDualListIncluded(a, bb)),
"controlDualListAdd" => Some(H::ControlDualListAdd(a)),
"controlDualListRemove" => Some(H::ControlDualListRemove(a)),
"controlDualListMoveUp" => Some(H::ControlDualListMoveUp(a)),
"controlDualListMoveDown" => Some(H::ControlDualListMoveDown(a)),
"controlInherit" => Some(H::ControlInherit(a)),
"searchResult" => Some(H::SearchResult(a)),
"save" => Some(H::SaveButton),
"cancel" => Some(H::CancelButton),
"reset" => Some(H::ResetButton),
"layer" => Some(H::LayerButton),
"edit" => Some(H::EditButton),
"clearCategory" => Some(H::ClearCategoryButton),
_ => None,
};
if let Some(hit) = hit {
editor.dispatch_settings_hit(hit, 0, dbl);
}
let s = tick_scene(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
("POST", "/kbedit") => {
let v: Value = serde_json::from_slice(&body).unwrap_or(json!({}));
if let Some(a) = v.get("a").and_then(|x| x.as_u64()) {
editor.kbedit_select_display_row(a as usize);
}
let s = tick_scene(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
("POST", "/resize") => {
let v: Value = serde_json::from_slice(&body).unwrap_or(json!({}));
if let Some(c) = v.get("cols").and_then(|x| x.as_u64()) {
*cols = (c as u16).clamp(20, 400);
}
if let Some(r) = v.get("rows").and_then(|x| x.as_u64()) {
*rows = (r as u16).clamp(8, 200);
}
editor.resize(*cols, *rows);
let s = tick_scene(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
("POST", "/step") => {
let v: Value = serde_json::from_slice(&body).unwrap_or(json!({}));
apply_step(editor, &v);
let s = scene_json(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
("POST", "/reset") => {
(*cols, *rows) = DEFAULT_SIZE;
match build_editor(*cols, *rows, files) {
Ok(e) => *editor = e,
Err(err) => eprintln!("reset failed: {err}"),
}
let s = scene_json(editor, *cols, *rows).to_string();
respond(stream, "200 OK", "application/json", s.as_bytes())
}
_ => respond(stream, "404 Not Found", "text/plain", b"not found"),
}
}
fn respond(stream: &mut TcpStream, status: &str, ctype: &str, body: &[u8]) -> Result<()> {
let header = format!(
"HTTP/1.1 {status}\r\nContent-Type: {ctype}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
stream.write_all(header.as_bytes())?;
stream.write_all(body)?;
stream.flush()?;
Ok(())
}
fn render_to_buffer(editor: &mut Editor, cols: u16, rows: u16) -> (Buffer, Option<(u16, u16)>) {
use ratatui::backend::Backend;
let backend = TestBackend::new(cols, rows);
let mut terminal = Terminal::new(backend).expect("terminal");
terminal.draw(|frame| editor.render(frame)).expect("draw");
let buf = terminal.backend().buffer().clone();
let cursor = terminal
.backend_mut()
.get_cursor_position()
.ok()
.map(|p| (p.x, p.y));
(buf, cursor)
}
fn rect_json(r: Rect) -> Value {
json!({ "x": r.x, "y": r.y, "w": r.width, "h": r.height })
}
fn cells_json(buf: &Buffer, r: Rect) -> Value {
let mut rows = Vec::with_capacity(r.height as usize);
for y in r.y..r.y.saturating_add(r.height) {
let mut runs: Vec<Value> = Vec::new();
let mut cur_text = String::new();
let mut cur_fg: Option<String> = None;
let mut cur_bg: Option<String> = None;
let mut cur_mods = Modifier::empty();
let mut flush = |runs: &mut Vec<Value>,
text: &mut String,
fg: &Option<String>,
bg: &Option<String>,
m: Modifier| {
if !text.is_empty() {
runs.push(json!({
"t": text,
"fg": fg, "bg": bg,
"b": m.contains(Modifier::BOLD),
"i": m.contains(Modifier::ITALIC),
"u": m.contains(Modifier::UNDERLINED),
"r": m.contains(Modifier::REVERSED),
}));
text.clear();
}
};
for x in r.x..r.x.saturating_add(r.width) {
let Some(cell) = buf.cell(Position::new(x, y)) else {
continue;
};
let fg = color_css(cell.fg);
let bg = color_css(cell.bg);
let m = cell.modifier;
if !cur_text.is_empty() && (fg != cur_fg || bg != cur_bg || m != cur_mods) {
flush(&mut runs, &mut cur_text, &cur_fg, &cur_bg, cur_mods);
}
cur_fg = fg;
cur_bg = bg;
cur_mods = m;
cur_text.push_str(cell.symbol());
}
flush(&mut runs, &mut cur_text, &cur_fg, &cur_bg, cur_mods);
rows.push(Value::Array(runs));
}
Value::Array(rows)
}
fn tick_scene(editor: &mut Editor, cols: u16, rows: u16) -> Value {
if let Err(e) = crate::app::editor_tick(editor, || Ok(())) {
eprintln!("[webui] editor_tick error: {e}");
}
scene_json(editor, cols, rows)
}
fn scene_json(editor: &mut Editor, cols: u16, rows: u16) -> Value {
let (buf, cursor) = render_to_buffer(editor, cols, rows);
let w = buf.area.width;
let h = buf.area.height;
let popups = serde_json::to_value(editor.popups_view()).unwrap_or_else(|_| json!([]));
let menu_view = serde_json::to_value(editor.menu_view()).unwrap_or_else(|_| json!({}));
let get = |k: &str| menu_view.get(k).cloned().unwrap_or(Value::Null);
let menus = get("menus");
let menu_open = get("menuOpen");
let menu_highlight = get("menuHighlight");
let submenu_path = get("submenuPath");
let dropdown = get("dropdown");
let layout = editor.active_layout();
let content = layout.editor_content_area.unwrap_or(Rect::new(0, 0, w, h));
let menubar_rect = (content.y > 0).then(|| Rect::new(0, 0, w, content.y));
let panes: Vec<Value> = layout
.split_areas
.iter()
.map(
|(leaf, bufid, content_rect, scrollbar_rect, thumb_s, thumb_e)| {
let tb = editor.tab_bar_view(*leaf);
let gw = editor
.leaf_gutter_width(*leaf, *bufid)
.min(content_rect.width);
let gutter_rect =
Rect::new(content_rect.x, content_rect.y, gw, content_rect.height);
let text_rect = Rect::new(
content_rect.x + gw,
content_rect.y,
content_rect.width - gw,
content_rect.height,
);
json!({
"leaf": leaf.0 .0,
"buffer": bufid.0,
"content": rect_json(*content_rect),
"gutterWidth": gw,
"gutter": if gw > 0 { cells_json(&buf, gutter_rect) } else { Value::Null },
"cells": cells_json(&buf, text_rect),
"tabBar": serde_json::to_value(tb.bar).unwrap_or(Value::Null),
"tabs": serde_json::to_value(tb.tabs).unwrap_or_else(|_| json!([])),
"vscroll": rect_json(*scrollbar_rect),
"thumbStart": thumb_s,
"thumbEnd": thumb_e,
})
},
)
.collect();
let separators: Vec<Value> = layout
.separator_areas
.iter()
.map(|(_id, dir, x, y, len)| {
json!({
"vertical": matches!(dir, crate::model::event::SplitDirection::Vertical),
"x": x, "y": y, "len": len,
})
})
.collect();
let file_explorer = serde_json::to_value(editor.file_explorer_view()).unwrap_or(Value::Null);
let statusbar = serde_json::to_value(editor.status_view()).unwrap_or(Value::Null);
let mut palette = serde_json::to_value(editor.palette_view()).unwrap_or(Value::Null);
if let Some(pv) = palette.get("previewRect").cloned() {
let u = |k: &str| pv.get(k).and_then(|x| x.as_u64()).unwrap_or(0) as u16;
let pr = Rect::new(u("x"), u("y"), u("w"), u("h"));
if pr.width > 0 && pr.height > 0 {
let cells = cells_json(&buf, pr);
if let Some(obj) = palette.as_object_mut() {
obj.insert("previewCells".to_string(), cells);
}
}
}
let trust_dialog = serde_json::to_value(editor.trust_dialog_view()).unwrap_or(Value::Null);
let widgets = serde_json::to_value(editor.widgets_view()).unwrap_or(Value::Null);
let context_menu = serde_json::to_value(editor.context_menu_view()).unwrap_or(Value::Null);
let aux_modal = serde_json::to_value(editor.aux_modals_view()).unwrap_or(Value::Null);
let keybinding_editor =
serde_json::to_value(editor.keybinding_editor_view()).unwrap_or(Value::Null);
let settings = serde_json::to_value(editor.settings_view()).unwrap_or(Value::Null);
let theme = {
let t = editor.theme.read().unwrap();
json!({
"name": t.name,
"bg": color_css(t.editor_bg),
"fg": color_css(t.editor_fg),
"accent": color_css(t.cursor),
"muted": color_css(t.line_number_fg),
"selectionBg": color_css(t.selection_bg),
"menuBg": color_css(t.menu_bg),
"menuFg": color_css(t.menu_fg),
"menuHi": color_css(t.menu_highlight_bg),
"popupBg": color_css(t.popup_bg),
"popupFg": color_css(t.popup_text_fg),
"border": color_css(t.popup_border_fg),
"statusBg": color_css(t.status_bar_bg),
"statusFg": color_css(t.status_bar_fg),
"tabActiveBg": color_css(t.tab_active_bg),
})
};
let regions = json!({
"menubar": menubar_rect.map(rect_json),
"menus": menus,
"menuOpen": menu_open,
"menuHighlight": menu_highlight,
"submenuPath": submenu_path,
"dropdown": dropdown,
"statusbar": statusbar,
"fileExplorer": file_explorer,
"panes": panes,
"separators": separators,
"popups": popups,
"palette": palette,
"trustDialog": trust_dialog,
"widgets": widgets,
"contextMenu": context_menu,
"auxModal": aux_modal,
"keybindingEditor": keybinding_editor,
"settings": settings,
"cursor": cursor.map(|(x, y)| json!({ "x": x, "y": y })),
"poll": json!({
"active": editor.active_window().animations.is_active()
|| editor.active_window().has_active_lsp_progress()
|| editor.next_periodic_redraw_deadline().is_some(),
}),
});
json!({ "w": w, "h": h, "regions": regions, "theme": theme })
}
fn apply_key(editor: &mut Editor, v: &Value) {
let key = v.get("key").and_then(|k| k.as_str()).unwrap_or("");
let ctrl = v.get("ctrl").and_then(|b| b.as_bool()).unwrap_or(false);
let alt = v.get("alt").and_then(|b| b.as_bool()).unwrap_or(false);
let meta = v.get("meta").and_then(|b| b.as_bool()).unwrap_or(false);
let shift = v.get("shift").and_then(|b| b.as_bool()).unwrap_or(false);
let code = match key {
"Enter" => KeyCode::Enter,
"Backspace" => KeyCode::Backspace,
"Delete" => KeyCode::Delete,
"Tab" => KeyCode::Tab,
"Escape" => KeyCode::Esc,
"ArrowUp" => KeyCode::Up,
"ArrowDown" => KeyCode::Down,
"ArrowLeft" => KeyCode::Left,
"ArrowRight" => KeyCode::Right,
"Home" => KeyCode::Home,
"End" => KeyCode::End,
"PageUp" => KeyCode::PageUp,
"PageDown" => KeyCode::PageDown,
s if s.chars().count() == 1 => KeyCode::Char(s.chars().next().unwrap()),
_ => return,
};
let mut mods = KeyModifiers::empty();
if ctrl {
mods |= KeyModifiers::CONTROL;
}
if alt {
mods |= KeyModifiers::ALT;
}
if meta {
mods |= KeyModifiers::SUPER;
}
if shift && !matches!(code, KeyCode::Char(_)) {
mods |= KeyModifiers::SHIFT;
}
if let Err(e) = editor.handle_key(code, mods) {
eprintln!("handle_key error: {e}");
}
}
fn apply_mouse(editor: &mut Editor, v: &Value) {
let col = v.get("col").and_then(|x| x.as_u64()).unwrap_or(0) as u16;
let row = v.get("row").and_then(|x| x.as_u64()).unwrap_or(0) as u16;
let n = v
.get("n")
.and_then(|x| x.as_u64())
.unwrap_or(1)
.clamp(1, 10);
let button = match v.get("button").and_then(|b| b.as_str()) {
Some("right") => MouseButton::Right,
Some("middle") => MouseButton::Middle,
_ => MouseButton::Left,
};
let kind = match v.get("kind").and_then(|k| k.as_str()).unwrap_or("") {
"down" => MouseEventKind::Down(button),
"up" => MouseEventKind::Up(button),
"drag" => MouseEventKind::Drag(button),
"moved" => MouseEventKind::Moved,
"scrollup" => MouseEventKind::ScrollUp,
"scrolldown" => MouseEventKind::ScrollDown,
"scrollleft" => MouseEventKind::ScrollLeft,
"scrollright" => MouseEventKind::ScrollRight,
_ => return,
};
let mut mods = KeyModifiers::empty();
if v.get("ctrl").and_then(|b| b.as_bool()).unwrap_or(false) {
mods |= KeyModifiers::CONTROL;
}
if v.get("alt").and_then(|b| b.as_bool()).unwrap_or(false) {
mods |= KeyModifiers::ALT;
}
if v.get("shift").and_then(|b| b.as_bool()).unwrap_or(false) {
mods |= KeyModifiers::SHIFT;
}
for _ in 0..n {
let ev = MouseEvent {
kind,
column: col,
row,
modifiers: mods,
};
if let Err(e) = editor.handle_mouse(ev) {
eprintln!("handle_mouse error: {e}");
break;
}
}
}
const ANSI16: [(u8, u8, u8); 16] = [
(0, 0, 0), (0xcd, 0x31, 0x31), (0x0d, 0xbc, 0x79), (0xe5, 0xe5, 0x10), (0x24, 0x72, 0xc8), (0xbc, 0x3f, 0xbc), (0x11, 0xa8, 0xcd), (0xe5, 0xe5, 0xe5), (0x66, 0x66, 0x66), (0xf1, 0x4c, 0x4c), (0x23, 0xd1, 0x8b), (0xf5, 0xf5, 0x43), (0x3b, 0x8e, 0xea), (0xd6, 0x70, 0xd6), (0x29, 0xb8, 0xdb), (0xff, 0xff, 0xff), ];
fn hex(r: u8, g: u8, b: u8) -> String {
format!("#{r:02x}{g:02x}{b:02x}")
}
fn color_css(c: Color) -> Option<String> {
let ansi = |i: usize| {
let (r, g, b) = ANSI16[i];
hex(r, g, b)
};
Some(match c {
Color::Reset => return None,
Color::Rgb(r, g, b) => hex(r, g, b),
Color::Black => ansi(0),
Color::Red => ansi(1),
Color::Green => ansi(2),
Color::Yellow => ansi(3),
Color::Blue => ansi(4),
Color::Magenta => ansi(5),
Color::Cyan => ansi(6),
Color::Gray => ansi(7),
Color::DarkGray => ansi(8),
Color::LightRed => ansi(9),
Color::LightGreen => ansi(10),
Color::LightYellow => ansi(11),
Color::LightBlue => ansi(12),
Color::LightMagenta => ansi(13),
Color::LightCyan => ansi(14),
Color::White => ansi(15),
Color::Indexed(i) => return Some(indexed_css(i)),
})
}
fn indexed_css(i: u8) -> String {
let (r, g, b) = if i < 16 {
ANSI16[i as usize]
} else if i < 232 {
let n = i - 16;
let levels = [0u8, 95, 135, 175, 215, 255];
(
levels[(n / 36) as usize],
levels[((n / 6) % 6) as usize],
levels[(n % 6) as usize],
)
} else {
let v = 8 + (i - 232) * 10;
(v, v, v)
};
hex(r, g, b)
}