diff options
Diffstat (limited to 'rola-cli')
| -rw-r--r-- | rola-cli/Cargo.toml | 4 | ||||
| -rw-r--r-- | rola-cli/locales/helps/basic.toml | 177 | ||||
| -rw-r--r-- | rola-cli/src/bin/rola.rs | 14 | ||||
| -rw-r--r-- | rola-cli/src/error/io.rs | 14 | ||||
| -rw-r--r-- | rola-cli/src/lib.rs | 5 | ||||
| -rw-r--r-- | rola-cli/src/output.rs | 6 | ||||
| -rw-r--r-- | rola-cli/src/output/ansi_control.rs | 32 | ||||
| -rw-r--r-- | rola-cli/src/output/ansi_control/colorize_wrapper.rs | 209 | ||||
| -rw-r--r-- | rola-cli/src/output/display.rs | 384 | ||||
| -rw-r--r-- | rola-cli/src/output/env_logger.rs | 54 | ||||
| -rw-r--r-- | rola-cli/src/output/setup.rs | 41 |
11 files changed, 930 insertions, 10 deletions
diff --git a/rola-cli/Cargo.toml b/rola-cli/Cargo.toml index 6b45c5a..0ed9a3b 100644 --- a/rola-cli/Cargo.toml +++ b/rola-cli/Cargo.toml @@ -15,6 +15,10 @@ space-system.workspace = true tokio.workspace = true +colored = "3.1.1" +chrono = "0.4.45" +env_logger = "0.11.10" +log = "0.4.32" shakehand = "0.1.3" [dependencies.mingling] diff --git a/rola-cli/locales/helps/basic.toml b/rola-cli/locales/helps/basic.toml index 656cdc4..ff0dfd1 100644 --- a/rola-cli/locales/helps/basic.toml +++ b/rola-cli/locales/helps/basic.toml @@ -1,9 +1,182 @@ [en] help = """ -NO YET +[[b_blue]]**Usage**[[/]]: **rola** [-v | --version] [-h | --help] [-L | --lang *<LANG>*] +__ **OUTPUT CONTROL:** +__ [--silence | --quiet] [--no-error] [--no-result] +__ [--no-color] [--json | --json-pretty] +__ **LOGS:** +__ [-V | --verbose] [--log-time] +__ [--log-level _disable_ | [[gray]]_trace_[[/]] | [[b_cyan]]_debug_[[/]] | [[b_yellow]]_warn_[[/]] | [[b_red]]_error_[[/]]] +__ **CONTEXT:** +__ [--bucket-dir *<DIR>*] [--draft-dir *<DIR>*] [--dir *<CWD>*] +__ **BEHAVIOUR CONTROL:** +__ [[b_yellow]]**[-y | --yes]**[[/]] [[b_red]]**[-O | --overwrite]**[[/]] [--no-pager] """ [zh_CN] help = """ -暂无 +[[b_blue]]**用法**[[/]]:**rola** [-v | --version] [-h | --help] [-L | --lang *<LANG>*] +__ **输出控制:** +__ [--silence | --quiet] [--no-error] [--no-result] +__ [--no-color] [--json | --json-pretty] +__ **日志:** +__ [-V | --verbose] [--log-time] +__ [--log-level _disable_ | [[gray]]_trace_[[/]] | [[b_cyan]]_debug_[[/]] | [[b_yellow]]_warn_[[/]] | [[b_red]]_error_[[/]]] +__ **上下文:** +__ [--bucket-dir *<DIR>*] [--draft-dir *<DIR>*] [--dir *<CWD>*] +__ **行为控制:** +__ [[b_yellow]]**[-y | --yes]**[[/]] [[b_red]]**[-O | --overwrite]**[[/]] [--no-pager] """ + + +# 全局: +# --draft-dir 草稿路径 +# --bucket-dir 桶路径 +# --dir 路径 +# -V --version +# -v --verbose +# -h --help +# -L --lang --language +# --json --json-pretty +# --no-color --no-pager +# --no-error --only-error +# --quiet / --silence +# -y / --yes +# --overwrite + +# # 创建草稿,临时视图 +# rola create --draft +# rola init --draft + +# # 创建桶,存储库,远程真相 +# rola create --bucket +# rola init --bucket + +# # 仅 draft 中命令 +# # 桶 +# rola bucket origin --bind-to-url url +# rola bucket origin --delete / -D +# rola bucket origin --rename-to other +# rola bucket # 列出 +# rola bucket origin # 显示元数据 + +# # 检查连接 +# rola test-connection --url url +# rola test-connection --bucket origin + +# # 信息同步 +# rola update +# rola update --all + +# # 视图 +# rola view # 列出所有视图 +# rola view THIS # 当前视图 +# rola view origin/main # 主视图 +# rola view --new other # 新建视图 +# rola view --clone other # 克隆视图 +# rola view --checkout-clone other # 新建视图并直接切换 +# rola view --checkout-new other # 新建视图并直接切换 +# rola view --checkout other # 切换 +# rola view origin --rename-to other # 命名 +# rola view --forget origin # 忘记视图 +# rola view origin/main --track-to main # 跟踪 +# rola view main --track-to origin/main # 跟踪 +# rola view main --break-track # 断开跟踪 +# rola view --show-track # 展示跟踪状态 +# rola view main --show-track # 展示跟踪状态 +# rola view main --up-track # 同步视图 上传 (只提醒使用--overwrite-up-track) +# rola view main --overwrite-up-track # 同步视图 上传 +# rola view main --down-track # 同步视图 下载 + +# # 状态 +# rola status --view main # 检查视图状态 (等效 rola view main) +# rola status # 检查草稿状态(如果在draft) +# rola status # 检查桶状态(如果在bucket) +# rola status --file ./file # 检查文件状态 +# rola status --file . # 检查多个文件状态 +# rola status --bucket origin # 检查远程桶状态 (等效 rola bucket origin) +# rola status --bucket-url url # 检查url状态 (等效 rola test-connection --url url) +# rola status --align task # 检查对齐状态 (等效 rola align task) +# rola status --align-task # 检查所有对齐状态 (等效 rola align) + +# # 实际版控 +# rola set-forward ./my.file --follow latest # 将文件总是追踪最新版本 +# rola set-forward ./my.file --follow ver:16 # 锁定到16版本 +# rola set-forward ./my.file --follow origin/main # 跟随某个view的版本 + +# rola track . # 跟踪文件 +# 为每个文件判断: +# 在 forward 为 latest 时 +# 1. 本地版本同步,且已修改:尝试上传(可能冲突) +# 2. 本地版本不同步,且未修改:下载 +# 3. 本地版本不同步,且已修改:阻止!(使用 --overwrite 覆盖) +# 4. 在 3 的情况下,使用 --overwrite 覆盖本地(或显式使用 --down --overwrite) +# 5. 在 3 的情况下,使用 --up --overwrite 覆盖远程(强制提交) +# 在 forward 为 ver 时 +# 1. 本地版本同步 ver,且已修改:阻止!(使用 --overwrite 覆盖) +# 2. 本地版本不同步 ver,且未修改:下载 +# 3. 本地版本同步 ver,未修改,什么都不做 +# 在 forward 为 view 时 +# 1. 如果view中没有该文件,阻止!(使用 --allow-non-forward) +# 2. 如果view中没有该文件,且使用 --allow-non-forward:不做任何事 +# 3. 如果view存在该文件,将view中该文件的版本视作ver,同(在 forward 为 ver 时)的行为 + +# 显式模式: +# --up --down 显式指定上传或下载,若状态不一,必须使用 --overwrite + +# # 对齐 +# rola align # 查看所有本地 view 和实际结构不一致的情况(移动、新增、丢失) +# rola align 移动项 --connect 丢失项 +# rola align 丢失项 --connect 移动项 # 匹配为移动 +# rola align 移动项 --confirm # 确认移动 +# rola align 新增项 --confirm # 确认新增 +# rola align 丢失项 --confirm # 确认丢失 (为删除) +# rola align --confirm-all # 全部确认(谨慎!) +# rola align 移动项 --break # 断开为丢失和新增 +# rola align 项 # 查看项 + +# # view 之间操作 (--overwrite 覆写) +# rola send 文件 --to-view view # 将该文件发送到view(同当前view的位置) +# rola send 文件 --to-view view --path # 将文件发送到view(指定位置) +# rola send view:文件 --to-view ... # 使用指定view的文件 +# rola send view:文件 --to-view THIS # 从其他view拉取文件到此处 +# rola send bucket/view:文件 --to-view THIS # 从远程view拉取文件到此处 + +# # 直接 view-tree 操作(危险的修复行为) +# rola op-view-tree rm path # 删除某条路径记录 +# rola op-view-tree add path --id 文件id # 新增某条路径记录 +# rola op-view-tree set path --forward xx --id xx # 修改某条路径记录 +# rola op-view-tree mv path ... path2 # 移动某条路径记录 +# rola op-view-tree cp path ... path2 # 移动某条路径记录 +# rola stack view1 view2 view3 ... --clone-into view4 (将view1 view2 view3的视图堆叠成一个视图,放置到view4) +# rola stack ... --into view4 (同上,但是会删除旧视图) +# rola op-idmap ls # 列出所有 id map +# rola op-idmap ls id # 列出关于该 id 的映射 +# rola op-idmap ls --remote id # 反向查找:通过远程 id 查找本地 id +# rola op-idmap write 12 --to-remote 25 # 将 12 号 id 映射到 远程 id 25 +# rola op-idmap clean 12 # 将 12 号 id 映射移除 +# rola op-idmap clean --remote 12 # 将映射到远程12号id的本地id移除 + +# # 查询操作 +# rola query bind-buckets +# rola query views +# rola query idmaps +# rola query objects + +# # 视图查看 +# rola ls # 等效别名 (rls):展示当前目录的元数据(富有rola元数据的ls命令) +# rola tree # 等效别名 (rtree): 展示当前目录的树 + +# # GUI +# rola desktop --install # 安装 RorolalaVCS - Dashboard +# rola desktop # 启动 RorolalaVCS - Dashboard + +# # 更新 +# rola source show # 展示远程更新源 +# rola source update-info # 更新远程信息 +# rola source update # 更新rola +# rola source change 源 # 切换更新源 +# rola source show-info # 查询更新信息 + +# # 交互式 +# rola op-view-tree --shell-mode # 进入 REPL 直接使用 cd ls rm add set mv 操作 tree diff --git a/rola-cli/src/bin/rola.rs b/rola-cli/src/bin/rola.rs index 0cfe675..a201953 100644 --- a/rola-cli/src/bin/rola.rs +++ b/rola-cli/src/bin/rola.rs @@ -5,7 +5,10 @@ use mingling::{ macros::program_setup, setup::{ExitCodeSetup, HelpFlagSetup, QuietFlagSetup}, }; -use rola_cli::{ThisProgram, locale, res::current_dir::ResCurrentDir}; +use rola_cli::{ + ThisProgram, locale, output::ColorOutputSetup, output::EnvLoggerSetup, + res::current_dir::ResCurrentDir, +}; fn main() { let mut program = ThisProgram::new(); @@ -32,11 +35,18 @@ fn main() { program.with_setup(HelpFlagSetup::new(["-h", "--help"])); program.with_setup(StandardOutputSetup); program.with_setup(ExitCodeSetup::default()); + program.with_setup(ColorOutputSetup); - // Execute + // stdout/stderr control let quiet = program.stdout_setting.quiet; let error_output = program.stdout_setting.error_output && !quiet; let render_output = program.stdout_setting.render_output && !quiet; + + if error_output { + program.with_setup(EnvLoggerSetup); + } + + // Execute let result = program.exec_without_render().unwrap(); if !result.is_empty() { if result.exit_code == 0 && render_output { diff --git a/rola-cli/src/error/io.rs b/rola-cli/src/error/io.rs index 7e4de56..d65b765 100644 --- a/rola-cli/src/error/io.rs +++ b/rola-cli/src/error/io.rs @@ -61,7 +61,14 @@ pub enum ErrorIo { pub fn render_error_io(err: ErrorIo, ec: &mut ResExitCode) { let err: std::io::Error = err.into(); let content = format!("{:?}", err); - let (error_info, exit_code) = match err.kind() { + let (error_info, exit_code) = io_error_info(err, content); + + r_println!("{}{}", I18nIoError::io_error_name(), error_info); + ec.exit_code = exit_code; +} + +fn io_error_info(err: std::io::Error, content: String) -> (String, i32) { + match err.kind() { std::io::ErrorKind::NotFound => (I18nIoError::not_found(content), EC_IOERR_NOT_FOUND), std::io::ErrorKind::PermissionDenied => ( I18nIoError::permission_denied(content), @@ -185,10 +192,7 @@ pub fn render_error_io(err: ErrorIo, ec: &mut ResExitCode) { } std::io::ErrorKind::Other => (I18nIoError::other(content), EC_IOERR_OTHER), _ => (I18nIoError::other(content), EC_IOERR_OTHER), - }; - - r_println!("{}: {}", I18nIoError::io_error_name(), error_info); - ec.exit_code = exit_code; + } } impl From<std::io::Error> for ErrorIo { diff --git a/rola-cli/src/lib.rs b/rola-cli/src/lib.rs index 54ff09a..b3587e2 100644 --- a/rola-cli/src/lib.rs +++ b/rola-cli/src/lib.rs @@ -2,6 +2,7 @@ use std::process::exit; use mingling::macros::{gen_program, help}; +pub mod output; pub mod res; pub mod tokio_wrapper; @@ -11,12 +12,14 @@ use bucket_mgr::*; mod error; use error::*; +use crate::output::display::markdown; + #[help] fn handle_error_dispatch_not_found(_err: ErrorDispatcherNotFound) { let help = locale::helps::Basic::help().trim(); // Print directly to stderr and exit with code 0 - eprintln!("{}", help); + eprintln!("{}", markdown(help)); exit(0) } diff --git a/rola-cli/src/output.rs b/rola-cli/src/output.rs new file mode 100644 index 0000000..65cafed --- /dev/null +++ b/rola-cli/src/output.rs @@ -0,0 +1,6 @@ +pub mod ansi_control; +pub mod display; +pub mod env_logger; + +mod setup; +pub use setup::*; diff --git a/rola-cli/src/output/ansi_control.rs b/rola-cli/src/output/ansi_control.rs new file mode 100644 index 0000000..5db8c2a --- /dev/null +++ b/rola-cli/src/output/ansi_control.rs @@ -0,0 +1,32 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +mod colorize_wrapper; +pub use colorize_wrapper::*; + +/// Global flag controlling whether ANSI color output is enabled. +static ANSI_ENABLED: AtomicBool = AtomicBool::new(true); + +/// Enable ANSI color codes in output. +pub fn enable_ansi() { + ANSI_ENABLED.store(true, Ordering::Relaxed); +} + +/// Disable ANSI color codes in output. +pub fn disable_ansi() { + ANSI_ENABLED.store(false, Ordering::Relaxed); +} + +/// Set ANSI color output to the specified state (`true` to enable, `false` to disable). +pub fn set_ansi(enabled: bool) { + ANSI_ENABLED.store(enabled, Ordering::Relaxed); +} + +/// Check whether ANSI color output is currently enabled. +pub fn is_ansi_enabled() -> bool { + ANSI_ENABLED.load(Ordering::Relaxed) +} + +/// Returns `true` if ANSI is enabled, `false` otherwise. +pub fn is_enabled() -> bool { + is_ansi_enabled() +} diff --git a/rola-cli/src/output/ansi_control/colorize_wrapper.rs b/rola-cli/src/output/ansi_control/colorize_wrapper.rs new file mode 100644 index 0000000..4c92f66 --- /dev/null +++ b/rola-cli/src/output/ansi_control/colorize_wrapper.rs @@ -0,0 +1,209 @@ +use colored::ColoredString; +use colored::Colorize as _ColoredColorize; + +use super::is_ansi_enabled; + +macro_rules! if_ansi { + ($self:expr, $method:ident $(, $arg:expr)*) => {{ + if is_ansi_enabled() { + _ColoredColorize::$method($self $(, $arg)*) + } else { + _ColoredColorize::clear($self) + } + }}; +} + +/// A drop-in wrapper for [`colored::Colorize`] that respects the global ANSI +/// output flag controlled by [`enable_ansi()`](super::enable_ansi) / +/// [`disable_ansi()`](super::disable_ansi). +/// +/// When ANSI is **disabled**, all colorization methods return a plain +/// [`ColoredString`] with no foreground/background color or style — +/// effectively a no-op. +/// +/// Import this trait instead of `colored::Colorize` to automatically honour +/// the `--no-color` / `--quiet` flags set by the CLI. +/// +/// # Example +/// +/// ```ignore +/// use rola_cli::res::ansi_control::Colorize; +/// +/// println!("{}", "error".red().bold()); +/// // ^ ANSI on → red bold text +/// // ^ ANSI off → plain "error" +/// ``` +pub trait Colorize: _ColoredColorize { + fn black(self) -> ColoredString { + if_ansi!(self, black) + } + fn red(self) -> ColoredString { + if_ansi!(self, red) + } + fn green(self) -> ColoredString { + if_ansi!(self, green) + } + fn yellow(self) -> ColoredString { + if_ansi!(self, yellow) + } + fn blue(self) -> ColoredString { + if_ansi!(self, blue) + } + fn magenta(self) -> ColoredString { + if_ansi!(self, magenta) + } + fn purple(self) -> ColoredString { + if_ansi!(self, purple) + } + fn cyan(self) -> ColoredString { + if_ansi!(self, cyan) + } + fn white(self) -> ColoredString { + if_ansi!(self, white) + } + + fn bright_black(self) -> ColoredString { + if_ansi!(self, bright_black) + } + fn bright_red(self) -> ColoredString { + if_ansi!(self, bright_red) + } + fn bright_green(self) -> ColoredString { + if_ansi!(self, bright_green) + } + fn bright_yellow(self) -> ColoredString { + if_ansi!(self, bright_yellow) + } + fn bright_blue(self) -> ColoredString { + if_ansi!(self, bright_blue) + } + fn bright_magenta(self) -> ColoredString { + if_ansi!(self, bright_magenta) + } + fn bright_purple(self) -> ColoredString { + if_ansi!(self, bright_purple) + } + fn bright_cyan(self) -> ColoredString { + if_ansi!(self, bright_cyan) + } + fn bright_white(self) -> ColoredString { + if_ansi!(self, bright_white) + } + + fn truecolor(self, r: u8, g: u8, b: u8) -> ColoredString { + if_ansi!(self, truecolor, r, g, b) + } + + fn color<C: Into<colored::Color>>(self, color: C) -> ColoredString { + if is_ansi_enabled() { + _ColoredColorize::color(self, color) + } else { + _ColoredColorize::clear(self) + } + } + + fn on_black(self) -> ColoredString { + if_ansi!(self, on_black) + } + fn on_red(self) -> ColoredString { + if_ansi!(self, on_red) + } + fn on_green(self) -> ColoredString { + if_ansi!(self, on_green) + } + fn on_yellow(self) -> ColoredString { + if_ansi!(self, on_yellow) + } + fn on_blue(self) -> ColoredString { + if_ansi!(self, on_blue) + } + fn on_magenta(self) -> ColoredString { + if_ansi!(self, on_magenta) + } + fn on_purple(self) -> ColoredString { + if_ansi!(self, on_purple) + } + fn on_cyan(self) -> ColoredString { + if_ansi!(self, on_cyan) + } + fn on_white(self) -> ColoredString { + if_ansi!(self, on_white) + } + + fn on_bright_black(self) -> ColoredString { + if_ansi!(self, on_bright_black) + } + fn on_bright_red(self) -> ColoredString { + if_ansi!(self, on_bright_red) + } + fn on_bright_green(self) -> ColoredString { + if_ansi!(self, on_bright_green) + } + fn on_bright_yellow(self) -> ColoredString { + if_ansi!(self, on_bright_yellow) + } + fn on_bright_blue(self) -> ColoredString { + if_ansi!(self, on_bright_blue) + } + fn on_bright_magenta(self) -> ColoredString { + if_ansi!(self, on_bright_magenta) + } + fn on_bright_purple(self) -> ColoredString { + if_ansi!(self, on_bright_purple) + } + fn on_bright_cyan(self) -> ColoredString { + if_ansi!(self, on_bright_cyan) + } + fn on_bright_white(self) -> ColoredString { + if_ansi!(self, on_bright_white) + } + + fn on_truecolor(self, r: u8, g: u8, b: u8) -> ColoredString { + if_ansi!(self, on_truecolor, r, g, b) + } + + fn on_color<C: Into<colored::Color>>(self, color: C) -> ColoredString { + if is_ansi_enabled() { + _ColoredColorize::on_color(self, color) + } else { + _ColoredColorize::clear(self) + } + } + + fn clear(self) -> ColoredString { + if_ansi!(self, clear) + } + fn normal(self) -> ColoredString { + if_ansi!(self, normal) + } + fn bold(self) -> ColoredString { + if_ansi!(self, bold) + } + fn dimmed(self) -> ColoredString { + if_ansi!(self, dimmed) + } + fn italic(self) -> ColoredString { + if_ansi!(self, italic) + } + fn underline(self) -> ColoredString { + if_ansi!(self, underline) + } + fn blink(self) -> ColoredString { + if_ansi!(self, blink) + } + #[allow(deprecated)] + fn reverse(self) -> ColoredString { + if_ansi!(self, reverse) + } + fn reversed(self) -> ColoredString { + if_ansi!(self, reversed) + } + fn hidden(self) -> ColoredString { + if_ansi!(self, hidden) + } + fn strikethrough(self) -> ColoredString { + if_ansi!(self, strikethrough) + } +} + +impl<T: _ColoredColorize> Colorize for T {} diff --git a/rola-cli/src/output/display.rs b/rola-cli/src/output/display.rs new file mode 100644 index 0000000..0845456 --- /dev/null +++ b/rola-cli/src/output/display.rs @@ -0,0 +1,384 @@ +use crate::output::ansi_control::Colorize; +use std::collections::VecDeque; + +/// Trait for adding markdown formatting to strings +pub trait Markdown { + fn markdown(&self) -> String; +} + +impl Markdown for &str { + fn markdown(&self) -> String { + markdown(self) + } +} + +impl Markdown for String { + fn markdown(&self) -> String { + markdown(self) + } +} + +/// Converts a string to colored/formatted text with ANSI escape codes. +/// +/// Supported syntax: +/// - Bold: `**text**` +/// - Italic: `*text*` +/// - Underline: `_text_` +/// - Angle-bracketed content: `<text>` (displayed as cyan) +/// - Inline code: `` `text` `` (displayed as green) +/// - Color tags: `[[color_name]]` and `[[/]]` to close color +/// - Escape characters: `\*`, `\<`, `\>`, `` \` ``, `\_` for literal characters +/// - Headings: `# Heading 1`, `## Heading 2`, up to `###### Heading 6` +/// - Blockquote: `> text` (displays a gray background marker at the beginning of the line) +/// +/// Color tags support the following color names: +/// Color tags support the following color names: +/// +/// | Type | Color Names | +/// |-------------------------|-----------------------------------------------------------------------------| +/// | Standard colors | `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` | +/// | Bright colors | `bright_black` | +/// | | `bright_red` | +/// | | `bright_green` | +/// | | `bright_yellow` | +/// | | `bright_blue` | +/// | | `bright_magenta` | +/// | | `bright_cyan` | +/// | | `bright_white` | +/// | Bright color shorthands | `b_black` | +/// | | `b_red` | +/// | | `b_green` | +/// | | `b_yellow` | +/// | | `b_blue` | +/// | | `b_magenta` | +/// | | `b_cyan` | +/// | | `b_white` | +/// | Gray colors | `gray`/`grey` | +/// | | `bright_gray`/`bright_grey` | +/// | | `b_gray`/`b_grey` | +/// +/// Color tags can be nested, `[[/]]` will close the most recently opened color tag. +/// +/// # Arguments +/// * `text` - The text to format, can be any type that implements `AsRef<str>` +/// +/// # Returns +/// Returns a `String` containing ANSI escape codes that can display colored/formatted text in ANSI-supported terminals. +/// +/// # Examples +/// ``` +/// # use mingling_cli::display::markdown; +/// let formatted = markdown("Hello **world**!"); +/// println!("{}", formatted); +/// +/// let colored = markdown("[[red]]Red text[[/]] and normal text"); +/// println!("{}", colored); +/// +/// let nested = markdown("[[blue]]Blue [[green]]Green[[/]] Blue[[/]] normal"); +/// println!("{}", nested); +/// ``` +pub fn markdown(text: impl AsRef<str>) -> String { + let text = text.as_ref(); + let lines: Vec<&str> = text.lines().collect(); + let mut result = String::new(); + let mut content_indent = 0; + + for line in lines { + // Don't trim the line initially, we need to check if it starts with # + let trimmed_line = line.trim(); + let mut line_result = String::new(); + + // Check if line starts with # for heading + // Check if the original line (not trimmed) starts with # + if line.trim_start().starts_with('#') { + let chars: Vec<char> = line.trim_start().chars().collect(); + let mut level = 0; + + // Count # characters at the beginning + while level < chars.len() && level < 7 && chars[level] == '#' { + level += 1; + } + + // Cap level at 6 + let effective_level = if level > 6 { 6 } else { level }; + + // Skip # characters and any whitespace after them + let mut content_start = level; + while content_start < chars.len() && chars[content_start].is_whitespace() { + content_start += 1; + } + + // Extract heading content + let heading_content: String = if content_start < chars.len() { + chars[content_start..].iter().collect() + } else { + String::new() + }; + + // Process the heading content with formatting + let processed_content = process_line(&heading_content); + + // Format heading as white background, black text, bold + // ANSI codes: \x1b[1m for bold, \x1b[47m for white background, \x1b[30m for black text + let formatted_heading = format!("\x1b[1m\x1b[47m\x1b[30m {processed_content} \x1b[0m"); + + // Add indentation to the heading line itself + // Heading indentation = level - 1 + let heading_indent = if effective_level > 0 { + effective_level - 1 + } else { + 0 + }; + let indent = " ".repeat(heading_indent); + line_result.push_str(&indent); + line_result.push_str(&formatted_heading); + + // Update content indent level for subsequent content + // Content after heading should be indented by effective_level + content_indent = effective_level; + } else if !trimmed_line.is_empty() { + // Process regular line with existing formatting + let processed_line = process_line_with_quote(trimmed_line); + + // Add indentation based on content_indent + let indent = " ".repeat(content_indent); + line_result.push_str(&indent); + line_result.push_str(&processed_line); + } else { + line_result.push(' '); + } + + if !line_result.is_empty() { + result.push_str(&line_result); + result.push('\n'); + } + } + + // Remove trailing newline + if result.ends_with('\n') { + result.pop(); + } + + result +} + +// Helper function to process a single line with existing formatting and handle > quotes +fn process_line_with_quote(line: &str) -> String { + let chars: Vec<char> = line.chars().collect(); + + // Check if line starts with '>' and not escaped + if !chars.is_empty() && chars[0] == '>' { + // Check if it's escaped + if chars.len() > 1 && chars[1] == '\\' { + // It's \>, so treat as normal text starting from position 0 + return process_line(line); + } + + // It's a regular > at the beginning, replace with gray background gray text space + let gray_bg_space = "\x1b[48;5;242m\x1b[38;5;242m \x1b[0m"; + let rest_of_line = if chars.len() > 1 { + chars[1..].iter().collect::<String>() + } else { + String::new() + }; + + // Process the rest of the line normally + let processed_rest = process_line(&rest_of_line); + + // Combine the gray background space with the processed rest + format!("{gray_bg_space}{processed_rest}") + } else { + // No > at the beginning, process normally + process_line(line) + } +} + +// Helper function to process a single line with existing formatting +fn process_line(line: &str) -> String { + let mut result = String::new(); + let mut color_stack: VecDeque<String> = VecDeque::new(); + + let chars: Vec<char> = line.chars().collect(); + let mut i = 0; + + while i < chars.len() { + // Check for escape character \ + if chars[i] == '\\' && i + 1 < chars.len() { + let escaped_char = chars[i + 1]; + // Only escape specific characters + if matches!(escaped_char, '*' | '<' | '>' | '`' | '_') { + let mut escaped_text = escaped_char.to_string(); + apply_color_stack(&mut escaped_text, &color_stack); + result.push_str(&escaped_text); + i += 2; + continue; + } + } + + // Check for color tag start [[color]] + if i + 1 < chars.len() + && chars[i] == '[' + && chars[i + 1] == '[' + && let Some(end) = find_tag_end(&chars, i) + { + let tag_content: String = chars[i + 2..end].iter().collect(); + + // Check if it's a closing tag [[/]] + if tag_content == "/" { + color_stack.pop_back(); + } else { + // It's a color tag + color_stack.push_back(tag_content.clone()); + } + i = end + 2; + continue; + } + + // Check for bold **text** + if i + 1 < chars.len() + && chars[i] == '*' + && chars[i + 1] == '*' + && let Some(end) = find_matching(&chars, i + 2, "**") + { + let bold_text: String = chars[i + 2..end].iter().collect(); + let mut formatted_text = bold_text.bold().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 2; + continue; + } + + // Check for italic *text* + if chars[i] == '*' + && let Some(end) = find_matching(&chars, i + 1, "*") + { + let italic_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = italic_text.italic().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Check for underline _text_ + if chars[i] == '_' + && let Some(end) = find_matching(&chars, i + 1, "_") + { + let underline_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = format!("\x1b[4m{underline_text}\x1b[0m"); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Check for angle-bracketed content <text> + if chars[i] == '<' + && let Some(end) = find_matching(&chars, i + 1, ">") + { + // Include the angle brackets in the output + let angle_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = angle_text.cyan().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Check for inline code `text` + if chars[i] == '`' + && let Some(end) = find_matching(&chars, i + 1, "`") + { + // Include the backticks in the output + let code_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = code_text.green().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Regular character + let mut current_char = chars[i].to_string(); + apply_color_stack(&mut current_char, &color_stack); + result.push_str(¤t_char); + i += 1; + } + + result +} + +// Helper function to find matching delimiter +fn find_matching(chars: &[char], start: usize, delimiter: &str) -> Option<usize> { + let delim_chars: Vec<char> = delimiter.chars().collect(); + let delim_len = delim_chars.len(); + + let mut j = start; + while j < chars.len() { + if delim_len == 1 { + if chars[j] == delim_chars[0] { + return Some(j); + } + } else if j + 1 < chars.len() + && chars[j] == delim_chars[0] + && chars[j + 1] == delim_chars[1] + { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to find color tag end +fn find_tag_end(chars: &[char], start: usize) -> Option<usize> { + let mut j = start + 2; + while j + 1 < chars.len() { + if chars[j] == ']' && chars[j + 1] == ']' { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to apply color stack to text +fn apply_color_stack(text: &mut String, color_stack: &VecDeque<String>) { + let mut result = text.clone(); + for color in color_stack.iter().rev() { + result = apply_color(&result, color); + } + *text = result; +} + +// Helper function to apply color to text +fn apply_color(text: impl AsRef<str>, color_name: impl AsRef<str>) -> String { + let text = text.as_ref(); + let color_name = color_name.as_ref(); + match color_name { + // Normal colors + "black" => text.black().to_string(), + "red" => text.red().to_string(), + "green" => text.green().to_string(), + "yellow" => text.yellow().to_string(), + "blue" => text.blue().to_string(), + "magenta" => text.magenta().to_string(), + "cyan" => text.cyan().to_string(), + "white" | "b_white" | "bright_gray" | "bright_grey" | "b_gray" | "b_grey" => { + text.white().to_string() + } + + // Bright colors and their b_ short aliases + "bright_black" | "b_black" | "gray" | "grey" => text.bright_black().to_string(), + "bright_red" | "b_red" => text.bright_red().to_string(), + "bright_green" | "b_green" => text.bright_green().to_string(), + "bright_yellow" | "b_yellow" => text.bright_yellow().to_string(), + "bright_blue" | "b_blue" => text.bright_blue().to_string(), + "bright_magenta" | "b_magenta" => text.bright_magenta().to_string(), + "bright_cyan" | "b_cyan" => text.bright_cyan().to_string(), + "bright_white" => text.bright_white().to_string(), + + // Default to white if color not recognized + _ => text.to_string(), + } +} diff --git a/rola-cli/src/output/env_logger.rs b/rola-cli/src/output/env_logger.rs new file mode 100644 index 0000000..289de77 --- /dev/null +++ b/rola-cli/src/output/env_logger.rs @@ -0,0 +1,54 @@ +use chrono::Local; +use std::io::Write; + +use crate::output::ansi_control::Colorize; + +/// Simple env logger that prints formatted messages. +/// Usage: `env_logger::init_with(EnvLogger { show_time: true, show_level: true });` +pub struct EnvLogger { + pub show_time: bool, + pub show_level: bool, + pub level: log::Level, +} + +impl log::Log for EnvLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::Level::Info + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let mut buf = Vec::new(); + + if self.show_time { + let now = Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + write!(buf, "[{}] ", now).ok(); + } + + if self.show_level { + let level_str = match record.level() { + log::Level::Trace => "TRACE".bright_black(), + log::Level::Debug => "DEBUG".cyan(), + log::Level::Info => "INFO".green(), + log::Level::Warn => "WARN".yellow(), + log::Level::Error => "ERROR".red(), + }; + write!(buf, "{}: ", level_str.bold()).ok(); + } + + write!(buf, "{}", record.args()).ok(); + eprintln!("{}", String::from_utf8_lossy(&buf)); + } + + fn flush(&self) {} +} + +/// Initialize the env logger with the given configuration. +pub fn init_envlogger(config: EnvLogger) { + log::set_boxed_logger(Box::new(config)) + .map(|()| log::set_max_level(log::LevelFilter::Info)) + .ok(); +} diff --git a/rola-cli/src/output/setup.rs b/rola-cli/src/output/setup.rs new file mode 100644 index 0000000..824348b --- /dev/null +++ b/rola-cli/src/output/setup.rs @@ -0,0 +1,41 @@ +use mingling::{Program, macros::program_setup}; +use shared_functions::info; + +use crate::{ + ThisProgram, + output::{ + ansi_control::disable_ansi, + env_logger::{EnvLogger, init_envlogger}, + }, +}; + +#[program_setup] +pub fn color_output_setup(program: &mut Program<ThisProgram>) { + program.global_flag("--no-color", |_| { + disable_ansi(); + }); +} + +#[program_setup] +pub fn env_logger_setup(program: &mut Program<ThisProgram>) { + program.stdout_setting.verbose = program.pick_global_flag(["-V", "--verbose"]); + let log_show_time = program.pick_global_flag("--log-time"); + let log_level = program + .pick_global_argument("--log-level") + .unwrap_or("info".to_string()); + if program.stdout_setting.verbose { + init_envlogger(EnvLogger { + show_time: log_show_time, + show_level: log_level.as_str() != "disable", + level: match log_level.as_str() { + "trace" => log::Level::Trace, + "debug" => log::Level::Debug, + "warn" => log::Level::Warn, + "error" => log::Level::Error, + _ => log::Level::Info, + }, + }); + } + + info!("Verbose mode enabled!"); +} |
