summaryrefslogtreecommitdiff
path: root/rola-cli
diff options
context:
space:
mode:
Diffstat (limited to 'rola-cli')
-rw-r--r--rola-cli/Cargo.toml4
-rw-r--r--rola-cli/locales/helps/basic.toml177
-rw-r--r--rola-cli/src/bin/rola.rs14
-rw-r--r--rola-cli/src/error/io.rs14
-rw-r--r--rola-cli/src/lib.rs5
-rw-r--r--rola-cli/src/output.rs6
-rw-r--r--rola-cli/src/output/ansi_control.rs32
-rw-r--r--rola-cli/src/output/ansi_control/colorize_wrapper.rs209
-rw-r--r--rola-cli/src/output/display.rs384
-rw-r--r--rola-cli/src/output/env_logger.rs54
-rw-r--r--rola-cli/src/output/setup.rs41
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(&current_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!");
+}