stynx-code-tui 3.8.1

Terminal user interface with ratatui for interactive sessions
Documentation
#[derive(Clone)]
pub struct DialogOption {
    pub value: String,
    pub title: String,
    pub description: Option<String>,
    pub category: Option<String>,
    pub footer: Option<String>,
    pub disabled: bool,
}

impl DialogOption {
    pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
        Self {
            value: value.into(),
            title: title.into(),
            description: None,
            category: None,
            footer: None,
            disabled: false,
        }
    }

    pub fn with_description(mut self, d: impl Into<String>) -> Self {
        self.description = Some(d.into());
        self
    }

    pub fn with_category(mut self, c: impl Into<String>) -> Self {
        self.category = Some(c.into());
        self
    }

    pub fn with_footer(mut self, f: impl Into<String>) -> Self {
        self.footer = Some(f.into());
        self
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PermissionChoice {
    Once,
    Always,
    Reject,
}

pub enum ModalKind {
    QuitConfirm,
    Permission {
        tool_name: String,
        description: String,
        choice: PermissionChoice,
    },
    Select {
        title: String,
        query: String,
        options: Vec<DialogOption>,
        selected: usize,
        current_value: Option<String>,
        kind: SelectKind,
        footer_hint: Option<String>,
    },
    Info {
        title: String,
        rows: Vec<(String, String)>,
    },
    Input {
        title: String,
        prompt: String,
        buffer: String,
        kind: InputKind,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputKind {
    SessionRename,
    AskUserQuestion,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum SelectKind {
    CommandPalette,
    ModelPicker,
    SessionList,
    Help,
    SkillPicker,
    FileMention,
    ThemePicker,
}

pub struct ModalState {
    pub active: Option<ModalKind>,
}

impl ModalState {
    pub fn new() -> Self {
        Self { active: None }
    }

    pub fn open_select(
        &mut self,
        kind: SelectKind,
        title: impl Into<String>,
        options: Vec<DialogOption>,
        current_value: Option<String>,
        footer_hint: Option<String>,
    ) {
        let selected = current_value
            .as_ref()
            .and_then(|cv| options.iter().position(|o| &o.value == cv))
            .unwrap_or(0);
        self.active = Some(ModalKind::Select {
            title: title.into(),
            query: String::new(),
            options,
            selected,
            current_value,
            kind,
            footer_hint,
        });
    }

    pub fn open_quit_confirm(&mut self) {
        self.active = Some(ModalKind::QuitConfirm);
    }

    pub fn open_permission(&mut self, tool_name: impl Into<String>, description: impl Into<String>) {        self.active = Some(ModalKind::Permission {
            tool_name: tool_name.into(),
            description: description.into(),
            choice: PermissionChoice::Once,
        });
    }

    pub fn open_info(&mut self, title: impl Into<String>, rows: Vec<(String, String)>) {
        self.active = Some(ModalKind::Info { title: title.into(), rows });
    }

    pub fn open_input(
        &mut self,
        title: impl Into<String>,
        prompt: impl Into<String>,
        initial: impl Into<String>,
        kind: InputKind,
    ) {
        self.active = Some(ModalKind::Input {
            title: title.into(),
            prompt: prompt.into(),
            buffer: initial.into(),
            kind,
        });
    }

    pub fn close(&mut self) {
        self.active = None;
    }
}

impl Default for ModalState {
    fn default() -> Self {
        Self::new()
    }
}

pub fn filter_options(opts: &[DialogOption], query: &str) -> Vec<usize> {
    let q = query.trim().to_lowercase();
    if q.is_empty() {
        return (0..opts.len()).collect();
    }
    opts.iter()
        .enumerate()
        .filter(|(_, o)| {
            o.title.to_lowercase().contains(&q)
                || o.description
                    .as_deref()
                    .map(|d| d.to_lowercase().contains(&q))
                    .unwrap_or(false)
                || o.category
                    .as_deref()
                    .map(|c| c.to_lowercase().contains(&q))
                    .unwrap_or(false)
        })
        .map(|(i, _)| i)
        .collect()
}