glim-tui 0.2.0

A TUI for monitoring GitLab CI/CD pipelines and projects
use compact_str::CompactString;
use ratatui::{
    buffer::Buffer,
    layout::{Margin, Rect},
    prelude::StatefulWidget,
    text::{Line, Span},
    widgets::{Block, Clear, Widget},
};
use tachyonfx::RefRect;

use crate::{
    notice_service::{Notice, NoticeMessage},
    stores::ProjectStore,
    theme::theme,
};

#[derive(Clone)]
pub struct Notification {}

pub struct NotificationState {
    pub notice: Notice,
    project_name: Option<CompactString>,
    content_area: RefRect,
}

impl NotificationState {
    pub fn new(notice: Notice, project_lookup: &ProjectStore, content_area: RefRect) -> Self {
        let project_name = match notice.message {
            NoticeMessage::GeneralMessage(_)
            | NoticeMessage::ConfigError(_)
            | NoticeMessage::JsonDeserializeError(_, _)
            | NoticeMessage::ScreenCaptured
            | NoticeMessage::InvalidGitlabToken
            | NoticeMessage::ExpiredGitlabToken
            | NoticeMessage::LogLevelChanged(_) => None,

            NoticeMessage::JobLogDownloaded(id, _, _)
            | NoticeMessage::GitlabGetJobsError(id, _, _)
            | NoticeMessage::GitlabGetTriggerJobsError(id, _, _)
            | NoticeMessage::GitlabGetPipelinesError(id, _, _) => {
                project_lookup.find(id).map(|p| p.title())
            },
        };

        Self { notice, project_name, content_area }
    }
}

impl Notification {
    pub fn new() -> Self {
        Self {}
    }
}

impl StatefulWidget for Notification {
    type State = NotificationState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        let project: &str = if let Some(p) = &state.project_name { p } else { "<unknown project>" };

        let text: Line<'_> = match &state.notice.message {
            NoticeMessage::GeneralMessage(s) => Line::from(Span::from(s)),
            NoticeMessage::ConfigError(s) => {
                Line::from(vec![Span::from("Config error: "), Span::from(s)])
            },
            NoticeMessage::InvalidGitlabToken => Line::from(Span::from(
                "Invalid GitLab token - please check your configuration",
            )),
            NoticeMessage::ExpiredGitlabToken => Line::from(Span::from(
                "GitLab token has expired - please update your configuration",
            )),
            NoticeMessage::JsonDeserializeError(cat, s) => Line::from(vec![
                Span::from("Failed to parse JSON ("),
                Span::from(format!("{cat:?}")),
                Span::from(")"),
                Span::from(s),
            ]),
            NoticeMessage::GitlabGetJobsError(_, _, s) => Line::from(vec![
                Span::from("Failed to get jobs for "),
                Span::from(project).style(theme().notification_project),
                Span::from(": "),
                Span::from(s),
            ]),
            NoticeMessage::GitlabGetTriggerJobsError(_, _, s) => Line::from(vec![
                Span::from("Failed to get trigger jobs for "),
                Span::from(project).style(theme().notification_project),
                Span::from(": "),
                Span::from(s),
            ]),
            NoticeMessage::GitlabGetPipelinesError(_, _, s) => Line::from(vec![
                Span::from("Failed to get pipelines for "),
                Span::from(project).style(theme().notification_project),
                Span::from(": "),
                Span::from(s),
            ]),
            NoticeMessage::JobLogDownloaded(_, _, _) => Line::from(vec![
                Span::from("Finished downloading job log for "),
                Span::from(project).style(theme().notification_project),
            ]),
            NoticeMessage::ScreenCaptured => {
                Line::from(vec![Span::from("Screen contents copied to clipboard")])
            },
            NoticeMessage::LogLevelChanged(level) => Line::from(vec![
                Span::from("Log level changed to "),
                Span::from(format!("{level:?}")).style(theme().notification_project),
            ]),
        };

        let text_len = (text.width() as u16).min(area.width - 2);
        let content_area = Rect {
            x: area.x + (area.width - text_len) / 2 - 1,
            y: area.y,
            width: text_len + 2,
            height: 1,
        };
        state.content_area.set(content_area);

        Clear.render(content_area, buf);
        Block::new()
            .style(theme().notification)
            .render(content_area, buf);

        text.render(content_area.inner(Margin::new(1, 0)), buf);
    }
}