amdgpu_top_tui 0.11.1

TUI library for amdgpu_top
Documentation
use std::sync::{Arc, Mutex};
use cursive::view::{Nameable, Scrollable};
use cursive::{event::Key, menu, traits::With};
use cursive::theme::{BorderStyle, Theme, Palette};

use libamdgpu_top::{app::AppAmdgpuTop, DevicePath, Sampling, UiArgs};
use libamdgpu_top::stat::{self, FdInfoSortType, PCType};

mod view;
use view::*;

mod app;
use app::*;

mod smi;
pub use smi::run_smi;

#[derive(Debug, Clone)]
struct ToggleOptions {
    grbm: bool,
    grbm2: bool,
    vram: bool,
    activity: bool,
    sensor: bool,
    high_freq: bool,
    fdinfo: bool,
    fdinfo_sort: FdInfoSortType,
    reverse_sort: bool,
    gpu_metrics: bool,
    select_index: usize,
    indexes: Vec<usize>,
    is_dark_mode: bool,
}

impl Default for ToggleOptions {
    fn default() -> Self {
        Self {
            grbm: true,
            grbm2: true,
            vram: true,
            activity: true,
            sensor: true,
            high_freq: false,
            fdinfo: true,
            fdinfo_sort: Default::default(),
            reverse_sort: false,
            gpu_metrics: true,
            select_index: 0,
            indexes: Vec::new(),
            is_dark_mode: false,
        }
    }
}

type Opt = Arc<Mutex<ToggleOptions>>;

pub fn run(
    title: &str,
    UiArgs {
        selected_device_path,
        device_path_list,
        update_process_index,
        no_pc,
        is_dark_mode,
        hide_fdinfo,
        ..
    }: UiArgs,
) {
    let is_dark_mode = is_dark_mode == Some(true); // The default theme for TUI is light.
    let title = title.to_string();
    let mut toggle_opt = ToggleOptions { is_dark_mode, fdinfo: !hide_fdinfo, ..Default::default() };

    let (vec_app, suspended_devices) = AppAmdgpuTop::create_app_and_suspended_list(
        &device_path_list,
        &Default::default(),
    );
    let mut vec_app: Vec<_> = vec_app
        .into_iter()
        .enumerate()
        .map(|(i, app)| TuiApp::new_with_app(app, no_pc, i))
        .collect();
    let app_len = vec_app.len();
    let mut vec_sus_app: Vec<_> = suspended_devices
        .into_iter()
        .enumerate()
        .map(|(i, app)| SuspendedTuiApp::new(app, no_pc, app_len+i))
        .collect();

    for app in vec_app.iter_mut() {
        app.update(&toggle_opt, &Sampling::low());
    }

    toggle_opt.indexes = vec_app.iter().map(|app| app.index).collect();

    {
        let mut device_paths: Vec<DevicePath> = device_path_list;

        if let Some(xdna_device_path) = vec_app
            .iter()
            .find_map(|app| app.app_amdgpu_top.xdna_device_path.as_ref())
        {
            device_paths.push(xdna_device_path.clone());
        }

        stat::spawn_update_index_thread(device_paths, update_process_index);
    }

    let mut siv = cursive::default();
    {
        let menubar = siv.menubar();
        
        menubar.add_subtree(
            "Device List [ESC]",
            menu::Tree::new()
                .with(|tree| {
                    for app in &vec_app {
                        let index = app.index;

                        tree.add_leaf(
                            app.label(),
                            move |siv: &mut cursive::Cursive| {
                                let screen = siv.screen_mut();
                                let Some(pos) = screen.find_layer_from_name(&index.to_string())
                                    else { return };
                                screen.move_to_front(pos);

                                let mut opt = siv.user_data::<Opt>().unwrap().lock().unwrap();
                                opt.select_index = index;
                            },
                        );
                    }

                    for app in &vec_sus_app {
                        tree.add_leaf(
                            app.label(),
                            |_siv: &mut cursive::Cursive| {},
                        );
                    }
                })
                .delimiter()
                .leaf("Quit", cursive::Cursive::quit),
        );
    }

    {
        let screen = siv.screen_mut();
        for app in &vec_app {
            screen.add_layer(
                app.view(&title)
                    .scrollable()
                    .scroll_x(true)
                    .scroll_y(true)
                    .with_name(app.index.to_string())
            );

            if app.app_amdgpu_top.device_path.pci == selected_device_path.pci {
                toggle_opt.select_index = app.index;
            }
        }
        if let Some(pos) = screen.find_layer_from_name(&toggle_opt.select_index.to_string()) {
            screen.move_to_front(pos);
        }
    }

    let mut flags = toggle_opt.clone();
    let toggle_opt = Arc::new(Mutex::new(toggle_opt));

    siv.set_autohide_menu(false);
    siv.set_user_data(toggle_opt.clone());
    siv.set_theme(if is_dark_mode { dark_mode() } else { Theme::default() });

    {
        if !no_pc {
            siv.add_global_callback('g', pc_type_cb(PCType::GRBM));
            siv.add_global_callback('r', pc_type_cb(PCType::GRBM2));
        }
        siv.add_global_callback('v', VramUsageView::cb);
        siv.add_global_callback('a', ActivityView::cb);
        siv.add_global_callback('f', AppTextView::cb_fdinfo);
        siv.add_global_callback('R', AppTextView::cb_reverse_sort);
        siv.add_global_callback('P', AppTextView::cb_sort_by_pid);
        siv.add_global_callback('V', AppTextView::cb_sort_by_vram);
        siv.add_global_callback('C', AppTextView::cb_sort_by_cpu);
        siv.add_global_callback('G', AppTextView::cb_sort_by_gfx);
        siv.add_global_callback('M', AppTextView::cb_sort_by_media);
        siv.add_global_callback('n', AppTextView::cb_sensors);
        siv.add_global_callback('m', AppTextView::cb_gpu_metrics);
        siv.add_global_callback('q', cursive::Cursive::quit);
        siv.add_global_callback('h', |siv| {
            let mut opt = siv.user_data::<Opt>().unwrap().lock().unwrap();
            opt.high_freq ^= true;
        });
        siv.add_global_callback('T', |siv| {
            let is_dark_mode;
            {
                let mut opt = siv.user_data::<Opt>().unwrap().lock().unwrap();
                opt.is_dark_mode ^= true;
                is_dark_mode = opt.is_dark_mode;
            }

            siv.set_theme(if is_dark_mode { dark_mode() } else { Theme::default() });
        });
        siv.add_global_callback(Key::Esc, |siv| siv.select_menubar());
    }

    if hide_fdinfo {
        AppTextView::cb_fdinfo(&mut siv);
    }

    let cb_sink = siv.cb_sink().clone();

    std::thread::spawn(move || loop {
        {
            let lock = toggle_opt.try_lock();
            if let Ok(opt) = lock {
                flags = opt.clone();
            }
        }

        let sample = if flags.high_freq { Sampling::high() } else { Sampling::low() };

        {
            let selected_app = vec_app
                .iter_mut()
                .find(|app| flags.select_index == app.index)
                .unwrap();

            if !no_pc {
                for _ in 0..sample.count {
                    selected_app.app_amdgpu_top.update_pc();

                    std::thread::sleep(sample.delay);
                }

                selected_app.app_amdgpu_top.update_pc_usage();
            } else {
                std::thread::sleep(sample.to_duration());
            }

            selected_app.update(&flags, &sample);
            if !no_pc { selected_app.app_amdgpu_top.clear_pc(); }
        }

        vec_sus_app.retain(|sus_app| {
            let is_active = sus_app.device_path.check_if_device_is_active();

            if is_active {
                let title = title.clone();
                let Some(tui_app) = sus_app.to_tui_app() else { return true };
                let index = tui_app.index;
                let label = tui_app.label();
                let info_bar = tui_app.app_amdgpu_top.device_info.info_bar();
                let stat = tui_app.app_amdgpu_top.stat.clone();
                let xdna_device_path = tui_app.app_amdgpu_top.xdna_device_path.clone();
                let app_layout = tui_app.layout.clone();

                vec_app.push(tui_app);

                cb_sink.send(Box::new(move |siv| {
                    {
                        let view = app_layout
                            .view(&title, info_bar, &stat, &xdna_device_path)
                            .scrollable()
                            .scroll_x(true)
                            .scroll_y(true)
                            .with_name(index.to_string());
                        let screen = siv.screen_mut();
                        let select_index = flags.select_index.to_string();
                        screen.add_layer(view);
                        if let Some(pos) = screen.find_layer_from_name(&select_index) {
                            screen.move_to_front(pos);
                        }
                    }

                    let menubar = siv.menubar();
                    let subtree = menubar.get_subtree(0).unwrap();
                    let len = subtree.len();
                    subtree.remove(len-3);

                    subtree.insert_leaf(
                        len-3,
                        label,
                        move |siv: &mut cursive::Cursive| {
                            let screen = siv.screen_mut();
                            let Some(pos) = screen.find_layer_from_name(&index.to_string())
                                else { return };
                            screen.move_to_front(pos);

                            let mut opt = siv.user_data::<Opt>().unwrap().lock().unwrap();
                            opt.select_index = index;
                        },
                    );
                })).unwrap();
            }

            !is_active
        });

        cb_sink.send(Box::new(cursive::Cursive::noop)).unwrap();
    });

    siv.run();
}

fn dark_mode() -> Theme {
    Theme {
            shadow: true,
            borders: BorderStyle::Simple,
            palette: Palette::terminal_default().with(|palette| {
                use cursive::theme::PaletteColor::*;
                use cursive::theme::BaseColor;

                palette[Background] = BaseColor::Black.light();

                palette[View] = BaseColor::Black.dark();
                palette[Primary] = BaseColor::White.dark();
                palette[TitlePrimary] = BaseColor::Cyan.light();

                palette[Highlight] = BaseColor::Cyan.light();
                palette[HighlightInactive] = BaseColor::Cyan.dark();
                palette[HighlightText] = BaseColor::Black.dark();
            }),
    }
}