feat(tui): add markdown rendering support and toggle command

- Introduce new `owlen-markdown` crate that converts Markdown strings to `ratatui::Text` with headings, lists, bold/italic, and inline code.
- Add `render_markdown` config option (default true) and expose it via `app.render_markdown_enabled()`.
- Implement `:markdown [on|off]` command to toggle markdown rendering.
- Update help overlay to document the new markdown toggle.
- Adjust UI rendering to conditionally apply markdown styling based on the markdown flag and code mode.
- Wire the new crate into `owlen-tui` Cargo.toml.
This commit is contained in:
2025-10-14 01:35:13 +02:00
parent 99064b6c41
commit 498e6e61b6
24 changed files with 911 additions and 247 deletions

View File

@@ -221,10 +221,11 @@ fn ensure_provider_entry(config: &mut Config, provider: &str, endpoint: &str) {
if provider == "ollama"
&& config.providers.contains_key("ollama-cloud")
&& !config.providers.contains_key("ollama")
&& let Some(mut legacy) = config.providers.remove("ollama-cloud")
{
legacy.provider_type = "ollama".to_string();
config.providers.insert("ollama".to_string(), legacy);
if let Some(mut legacy) = config.providers.remove("ollama-cloud") {
legacy.provider_type = "ollama".to_string();
config.providers.insert("ollama".to_string(), legacy);
}
}
core_config::ensure_provider_config(config, provider);
@@ -315,8 +316,10 @@ fn unlock_vault(path: &Path) -> Result<encryption::VaultHandle> {
use std::env;
if path.exists() {
if let Ok(password) = env::var("OWLEN_MASTER_PASSWORD")
&& !password.trim().is_empty()
if let Some(password) = env::var("OWLEN_MASTER_PASSWORD")
.ok()
.map(|value| value.trim().to_string())
.filter(|password| !password.is_empty())
{
return encryption::unlock_with_password(path.to_path_buf(), &password)
.context("Failed to unlock vault with OWLEN_MASTER_PASSWORD");
@@ -356,30 +359,31 @@ async fn hydrate_api_key(
config: &mut Config,
manager: Option<&Arc<CredentialManager>>,
) -> Result<Option<String>> {
if let Some(manager) = manager
&& let Some(credentials) = manager.get_credentials(OLLAMA_CLOUD_CREDENTIAL_ID).await?
{
let credentials = match manager {
Some(manager) => manager.get_credentials(OLLAMA_CLOUD_CREDENTIAL_ID).await?,
None => None,
};
if let Some(credentials) = credentials {
let key = credentials.api_key.trim().to_string();
if !key.is_empty() {
set_env_if_missing("OLLAMA_API_KEY", &key);
set_env_if_missing("OLLAMA_CLOUD_API_KEY", &key);
}
if let Some(cfg) = provider_entry_mut(config)
&& cfg.base_url.is_none()
&& !credentials.endpoint.trim().is_empty()
{
cfg.base_url = Some(credentials.endpoint);
let Some(cfg) = provider_entry_mut(config) else {
return Ok(Some(key));
};
if cfg.base_url.is_none() && !credentials.endpoint.trim().is_empty() {
cfg.base_url = Some(credentials.endpoint.clone());
}
return Ok(Some(key));
}
if let Some(cfg) = provider_entry(config)
&& let Some(key) = cfg
.api_key
.as_ref()
.map(|value| value.trim())
.filter(|value| !value.is_empty())
if let Some(key) = provider_entry(config)
.and_then(|cfg| cfg.api_key.as_ref())
.map(|value| value.trim())
.filter(|value| !value.is_empty())
{
set_env_if_missing("OLLAMA_API_KEY", key);
set_env_if_missing("OLLAMA_CLOUD_API_KEY", key);

View File

@@ -1,3 +1,5 @@
#![allow(clippy::collapsible_if)] // TODO: Remove once Rust 2024 let-chains are available
//! OWLEN CLI - Chat TUI client
mod cloud;

View File

@@ -151,8 +151,9 @@ fn handle_list(args: ListArgs) -> Result<()> {
"", "Scope", "Name", "Transport"
);
for entry in scoped {
if let Some(target_scope) = filter_scope
&& entry.scope != target_scope
if filter_scope
.as_ref()
.is_some_and(|target_scope| entry.scope != *target_scope)
{
continue;
}
@@ -186,8 +187,9 @@ fn handle_list(args: ListArgs) -> Result<()> {
.collect();
for entry in scoped_resources {
if let Some(target_scope) = filter_scope
&& entry.scope != target_scope
if filter_scope
.as_ref()
.is_some_and(|target_scope| entry.scope != *target_scope)
{
continue;
}