llm-manager 1.10.0

Terminal UI for managing LLMs
Documentation
use ratatui::{
    Frame,
    layout::Rect,
    widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState},
};

use crate::models::BenchTuneParamValue;

pub mod app;
pub mod colors;
pub mod event;
pub mod gguf_naming;
pub mod i18n;
pub mod panel;
pub mod render;
pub mod settings;
pub mod toast;

/// Format a byte count into a human-readable size string.
pub fn format_size(bytes: u64) -> String {
    let kb = 1024.0;
    let mb = kb * 1024.0;
    let gb = mb * 1024.0;
    let tb = gb * 1024.0;
    let s = bytes as f64;
    if s < kb {
        format!("{} B", bytes)
    } else if s < mb {
        format!("{:.1} KB", s / kb)
    } else if s < gb {
        format!("{:.1} MB", s / mb)
    } else if s < tb {
        format!("{:.1} GB", s / gb)
    } else {
        format!("{:.1} TB", s / tb)
    }
}

/// Format a number into an abbreviated human-readable string (e.g., 1.5K, 2.3M, 1.2B).
/// Uses 1024 as base for K (consistent with context length in tokens).
pub fn format_number(n: u64) -> String {
    let k = 1024.0;
    let m = k * 1024.0;
    let b = m * 1024.0;
    let s = n as f64;
    if s >= b {
        format!("{:.1}B", s / b)
    } else if s >= m {
        format!("{:.1}M", s / m)
    } else if s >= k {
        format!("{:.1}K", s / k)
    } else {
        format!("{}", n)
    }
}

/// Format context length in K units, showing extended size when YARN is enabled.
pub fn format_context_k(context_length: u32, rope_yarn_enabled: bool, rope_scale: f32) -> String {
    if context_length == 0 {
        return "N/A".to_string();
    }
    let effective = if rope_yarn_enabled && rope_scale > 1.0 {
        (context_length as f64 * rope_scale as f64) as u32
    } else {
        context_length
    };
    format_number(effective as u64)
}

/// Render a vertical scrollbar on the right edge of an area.
///
/// `top_offset` and `height_offset` allow adjusting the scrollbar position
/// (e.g., offset 1 from top, 2 from height for panels with title borders).
pub fn render_vertical_scrollbar(
    f: &mut Frame,
    area: Rect,
    total_items: usize,
    position: usize,
    top_offset: u16,
    height_offset: u16,
) {
    let scrollbar_area = Rect {
        x: area.right().saturating_sub(1),
        y: area.top() + top_offset,
        width: 1,
        height: area.height.saturating_sub(height_offset),
    };
    let mut scrollbar_state = ScrollbarState::new(total_items).position(position);
    f.render_stateful_widget(
        Scrollbar::new(ScrollbarOrientation::VerticalRight)
            .begin_symbol(Some(""))
            .end_symbol(Some("")),
        scrollbar_area,
        &mut scrollbar_state,
    );
}

/// Format benchmark parameters as display strings.
///
/// If `verbose` is true, returns full "key: value" lines with 2-decimal floats.
/// If `verbose` is false, returns compact "key=value" strings (1-decimal floats).
pub fn format_bench_params(params: &BenchTuneParamValue, verbose: bool) -> Vec<String> {
    let mut parts = Vec::new();
    if let Some(v) = params.temperature {
        parts.push(if verbose {
            format!("  temperature: {:.2}", v)
        } else {
            format!("temp={:.1}", v)
        });
    }
    if let Some(v) = params.top_p {
        parts.push(if verbose {
            format!("  top_p: {:.2}", v)
        } else {
            format!("top_p={:.1}", v)
        });
    }
    if let Some(v) = params.top_k {
        parts.push(if verbose {
            format!("  top_k: {}", v)
        } else {
            format!("top_k={}", v)
        });
    }
    if let Some(v) = params.repeat_penalty {
        parts.push(if verbose {
            format!("  repeat_penalty: {:.2}", v)
        } else {
            format!("repeat_penalty={:.1}", v)
        });
    }
    if let Some(v) = params.context_length {
        parts.push(if verbose {
            format!("  context_length: {}", format_number(v as u64))
        } else {
            format!("context_length={}", format_number(v as u64))
        });
    }
    if let Some(v) = params.batch_size {
        parts.push(if verbose {
            format!("  batch_size: {}", v)
        } else {
            format!("batch={}", v)
        });
    }
    if let Some(v) = params.threads {
        parts.push(if verbose {
            format!("  threads: {}", v)
        } else {
            format!("threads={}", v)
        });
    }
    if let Some(v) = params.flash_attn {
        parts.push(if verbose {
            format!("  flash_attn: {}", if v { "on" } else { "off" })
        } else {
            format!("fa={}", if v { "on" } else { "off" })
        });
    }
    if let Some(v) = params.expert_count {
        parts.push(if verbose {
            format!("  expert_count: {}", v)
        } else {
            format!("experts={}", v)
        });
    }
    if let Some(ref v) = params.spec_type
        && !v.is_empty()
    {
        parts.push(if verbose {
            format!("  spec_type: {}", v)
        } else {
            format!("spec={}", v)
        });
    }
    if let Some(v) = params.draft_tokens
        && v > 0
    {
        parts.push(if verbose {
            format!("  draft_tokens: {}", v)
        } else {
            format!("draft={}", v)
        });
    }
    parts
}