Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ async fn main() -> taolk::error::Result<()> {
| `ui.mouse` | `true` | Mouse support |
| `ui.timestamp_format` | `%H:%M` | Message time format |
| `ui.date_format` | `%Y-%m-%d %H:%M` | Full date format |
| `ui.icons` | `unicode` | Icon style: `nerd`, `unicode`, or `ascii` |

### Icons

taolk draws its UI icons in one of three styles, set with `ui.icons` or the `TAOLK_ICONS`
environment variable:

- `unicode` (default) — plain single-cell symbols that render on any terminal, no special font.
- `nerd` — [Nerd Font](https://www.nerdfonts.com/) glyphs (nicer, but needs a patched font such as
a Nerd Font or `Symbols Nerd Font Mono`). Also enabled by `NERD_FONT=1`.
- `ascii` — pure ASCII, for the most limited terminals.

Precedence: `TAOLK_ICONS` > `NERD_FONT=1` > `ui.icons` > default. When output isn't a terminal
(piped or redirected), icons fall back to `ascii`.

## Mirrors

Expand Down
38 changes: 19 additions & 19 deletions src/cmd/registry.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::app::{App, Focus, Overlay, View};
use crate::ui::icons;
use crate::ui::icons::Icon;
use taolk::types::BlockRef;

pub type CmdResult = Result<(), String>;

pub struct Command {
pub name: &'static str,
pub glyph: &'static str,
pub glyph: Icon,
pub summary: &'static str,
pub run: fn(&mut App, &[&str]) -> CmdResult,
}
Expand All @@ -15,100 +15,100 @@ pub const COMMANDS: &[Command] = &[
// Compose
Command {
name: "thread",
glyph: icons::THREADS,
glyph: Icon::Threads,
summary: "Start a new thread with a contact",
run: run_thread,
},
Command {
name: "message",
glyph: icons::OUTBOX,
glyph: Icon::Outbox,
summary: "Send a one-off message (not a thread)",
run: run_message,
},
Command {
name: "group",
glyph: icons::GROUPS,
glyph: Icon::Groups,
summary: "Create a new group conversation",
run: run_group,
},
// Navigate
Command {
name: "search",
glyph: icons::MAGNIFY,
glyph: Icon::Magnify,
summary: "Search messages in the current view",
run: run_search,
},
Command {
name: "channels",
glyph: icons::CHANNELS,
glyph: Icon::Channels,
summary: "Browse the channel directory",
run: run_channels,
},
Command {
name: "inbox",
glyph: icons::INBOX,
glyph: Icon::Inbox,
summary: "Jump to the inbox view",
run: run_inbox,
},
Command {
name: "outbox",
glyph: icons::OUTBOX,
glyph: Icon::Outbox,
summary: "Jump to the sent view",
run: run_outbox,
},
// View
Command {
name: "sidebar",
glyph: icons::MENU,
glyph: Icon::Menu,
summary: "Toggle the sidebar",
run: run_sidebar,
},
Command {
name: "help",
glyph: icons::HELP,
glyph: Icon::Help,
summary: "Show the help overlay",
run: run_help,
},
// System
Command {
name: "get",
glyph: icons::BLOCK,
glyph: Icon::Block,
summary: "Get remark(s) at block:index positions",
run: run_fetch,
},
Command {
name: "refresh",
glyph: icons::REFRESH,
glyph: Icon::Refresh,
summary: "Reload and fill message gaps",
run: run_refresh,
},
Command {
name: "copy",
glyph: icons::COPY,
glyph: Icon::Copy,
summary: "Copy a sender's SS58 address",
run: run_copy,
},
Command {
name: "unlock",
glyph: icons::LOCK_OPEN,
glyph: Icon::LockOpen,
summary: "Unlock all locked outbound messages",
run: run_unlock,
},
Command {
name: "lock",
glyph: icons::ENCRYPTED,
glyph: Icon::Encrypted,
summary: "Lock the session",
run: run_lock,
},
Command {
name: "wallet",
glyph: icons::SWAP,
glyph: Icon::Swap,
summary: "Switch to a different wallet",
run: run_wallet,
},
Command {
name: "quit",
glyph: icons::EXIT,
glyph: Icon::Exit,
summary: "Exit taolk",
run: run_quit,
},
Expand Down Expand Up @@ -253,7 +253,7 @@ mod tests {
for c in COMMANDS {
assert!(!c.name.is_empty());
assert!(!c.summary.is_empty());
assert!(!c.glyph.is_empty());
assert!(!c.glyph.glyph().is_empty());
}
}
}
20 changes: 20 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub struct Ui {
pub mouse: bool,
pub timestamp_format: String,
pub date_format: String,
pub icons: String,
}

#[derive(Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -75,6 +76,7 @@ impl Default for Ui {
mouse: true,
timestamp_format: "%H:%M".into(),
date_format: "%Y-%m-%d %H:%M".into(),
icons: "unicode".into(),
}
}
}
Expand Down Expand Up @@ -178,6 +180,13 @@ pub const KEYS: &[KeyDef] = &[
description: "Full date format (chrono strftime)",
default_display: "%Y-%m-%d %H:%M",
},
KeyDef {
key: "ui.icons",
section: "ui",
field: "icons",
description: "Icon style (nerd, unicode, ascii)",
default_display: "unicode",
},
KeyDef {
key: "notifications.enabled",
section: "notifications",
Expand Down Expand Up @@ -250,6 +259,7 @@ pub fn get_value(config: &Config, key: &str) -> String {
"ui.mouse" => config.ui.mouse.to_string(),
"ui.timestamp_format" => config.ui.timestamp_format.clone(),
"ui.date_format" => config.ui.date_format.clone(),
"ui.icons" => config.ui.icons.clone(),
"notifications.enabled" => config.notifications.enabled.to_string(),
"notifications.volume" => config.notifications.volume.to_string(),
"notifications.dm" => config.notifications.dm.to_string(),
Expand Down Expand Up @@ -287,6 +297,16 @@ pub fn set_key(key: &str, raw: &[String]) -> Result<String, ConfigError> {
"wallet.default" | "network.node" | "ui.timestamp_format" | "ui.date_format" => {
toml::Value::String(raw.join(" "))
}
"ui.icons" => {
let v = raw.first().map(|s| s.as_str()).unwrap_or("");
if !matches!(v, "nerd" | "unicode" | "ascii") {
return Err(ConfigError::InvalidValue {
expected: "nerd, unicode, or ascii".into(),
got: v.into(),
});
}
toml::Value::String(v.to_string())
}
"network.mirrors" => {
let items: Vec<toml::Value> = raw
.iter()
Expand Down
41 changes: 32 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ enum Commands {
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let cfg = config::load();
ui::icons::init(ui::icons::resolve(&cfg.ui.icons));

match cli.command {
Some(Commands::Wallet { action }) => cmd::wallet::run(action),
Expand Down Expand Up @@ -320,7 +321,7 @@ fn run_lock_screen(

let mut spans: Vec<Span<'static>> = Vec::new();
if win_start > 0 {
spans.push(Span::styled(ui::icons::CHEVRON_LEFT, dim_style));
spans.push(Span::styled(ui::icons::icons().chevron_left, dim_style));
spans.push(Span::raw(" "));
} else {
spans.push(Span::raw(" "));
Expand All @@ -337,23 +338,23 @@ fn run_lock_screen(
}
if win_end < wallets.len() {
spans.push(Span::raw(" "));
spans.push(Span::styled(ui::icons::CHEVRON_RIGHT, dim_style));
spans.push(Span::styled(ui::icons::icons().chevron_right, dim_style));
} else {
spans.push(Span::raw(" "));
}

lines.push(centered_spans(spans, w));
} else {
lines.push(centered_line(
&format!("{} Wallet: {}", ui::icons::WALLET, current_wallet),
&format!("{} Wallet: {}", ui::icons::icons().wallet, current_wallet),
w,
active_style,
));
}

lines.push(Line::raw(""));

let prompt = format!("{} Password: ", ui::icons::KEY);
let prompt = format!("{} Password: ", ui::icons::icons().key);
let prompt_cols = prompt.chars().count();
let prompt_style = if inserting {
prompt_active_style
Expand All @@ -376,13 +377,17 @@ fn run_lock_screen(
}

let hints = if inserting {
"Enter unlock \u{00B7} Esc back"
"Enter unlock \u{00B7} Esc back".to_string()
} else if show_carousel {
"\u{F004D}/\u{F0054} select \u{00B7} i unlock \u{00B7} q quit"
format!(
"{}/{} select \u{00B7} i unlock \u{00B7} q quit",
ui::icons::icons().arrow_left,
ui::icons::icons().arrow_right
)
} else {
"i unlock \u{00B7} q quit"
"i unlock \u{00B7} q quit".to_string()
};
lines.push(centered_line(hints, w, dim_style));
lines.push(centered_line(&hints, w, dim_style));

frame.render_widget(Paragraph::new(lines), area);

Expand Down Expand Up @@ -724,7 +729,7 @@ fn prompt_password_modal(
let inner = block.inner(rect);
frame.render_widget(block, rect);

let prompt_text = format!("{} Password: ", ui::icons::KEY);
let prompt_text = format!("{} Password: ", ui::icons::icons().key);
let prompt_cols = u16::try_from(prompt_text.chars().count()).unwrap_or(u16::MAX);
let mut lines: Vec<Line> = Vec::new();
lines.push(Line::raw(""));
Expand Down Expand Up @@ -2681,6 +2686,24 @@ mod tests {
assert!(parse_channel_ref("100:99999").is_err());
}

#[test]
fn e2e_default_theme_is_unicode_no_pua() {
// Tests never call icons::init, so the active theme is the safe default
// (unicode). The screen must render real glyphs and never a nerd PUA glyph.
let mut h = TuiHarness::new();
let screen = h.screen();
assert!(
screen.contains(ui::icons::UNICODE.inbox),
"default render should use the unicode inbox glyph"
);
assert!(
!screen
.chars()
.any(|c| (0xF0000..=0xFFFFD).contains(&(c as u32))),
"no nerd PUA glyph should appear under the default theme"
);
}

#[test]
fn e2e_help_overlay_renders_and_closes() {
let mut h = TuiHarness::new();
Expand Down
Loading