#![allow(clippy::use_self)]
use std::{
env,
fs::{create_dir_all, read_to_string, File},
io::Write,
path::Path,
str::FromStr,
};
use color_eyre::eyre::{bail, Error, Result};
use serde::{Deserialize, Serialize};
use crate::{
handlers::{
app::State,
args::{merge_args_into_config, Cli},
},
utils::pathing::config_path,
};
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct CompleteConfig {
pub twitch: TwitchConfig,
pub terminal: TerminalConfig,
pub storage: StorageConfig,
pub filters: FiltersConfig,
pub frontend: FrontendConfig,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(default)]
pub struct TwitchConfig {
pub username: String,
pub channel: String,
pub server: String,
pub token: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(default)]
pub struct TerminalConfig {
pub tick_delay: u64,
pub maximum_messages: usize,
pub log_file: Option<String>,
pub verbose: bool,
pub start_state: State,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct StorageConfig {
pub channels: bool,
pub mentions: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct FiltersConfig {
pub enabled: bool,
pub reversed: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(default)]
pub struct FrontendConfig {
pub date_shown: bool,
pub date_format: String,
pub maximum_username_length: u16,
pub username_alignment: Alignment,
pub palette: Palette,
pub title_shown: bool,
pub margin: u16,
pub badges: bool,
pub theme: Theme,
pub username_highlight: bool,
pub state_tabs: bool,
pub cursor_shape: CursorType,
}
impl Default for TwitchConfig {
fn default() -> Self {
Self {
username: String::new(),
channel: String::new(),
server: "irc.chat.twitch.tv".to_string(),
token: None,
}
}
}
impl Default for TerminalConfig {
fn default() -> Self {
Self {
tick_delay: 30,
maximum_messages: 150,
log_file: None,
verbose: false,
start_state: State::Normal,
}
}
}
impl Default for FrontendConfig {
fn default() -> Self {
Self {
date_shown: true,
date_format: "%a %b %e %T %Y".to_string(),
maximum_username_length: 26,
username_alignment: Alignment::default(),
palette: Palette::default(),
title_shown: true,
margin: 0,
badges: false,
theme: Theme::Dark,
username_highlight: true,
state_tabs: false,
cursor_shape: CursorType::default(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Alignment {
Left,
Center,
Right,
}
impl Default for Alignment {
fn default() -> Self {
Self::Right
}
}
impl FromStr for Alignment {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"left" => Ok(Self::Left),
"center" => Ok(Self::Center),
_ => Ok(Self::Right),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum CursorType {
Line,
Block,
UnderScore,
}
impl FromStr for CursorType {
type Err = Error;
fn from_str(s: &str) -> Result<CursorType, Self::Err> {
match s {
"line" => Ok(CursorType::Line),
"underscore" => Ok(CursorType::UnderScore),
_ => Ok(CursorType::Block),
}
}
}
impl Default for CursorType {
fn default() -> Self {
Self::Block
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Palette {
Pastel,
Vibrant,
Warm,
Cool,
}
impl Default for Palette {
fn default() -> Self {
Self::Pastel
}
}
impl FromStr for Palette {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"vibrant" => Ok(Self::Vibrant),
"warm" => Ok(Self::Warm),
"cool" => Ok(Self::Cool),
_ => Ok(Self::Pastel),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Theme {
Dark,
Light,
Custom,
}
impl Default for Theme {
fn default() -> Self {
Self::Dark
}
}
impl FromStr for Theme {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"light" => Ok(Self::Light),
_ => Ok(Self::Dark),
}
}
}
impl CompleteConfig {
pub fn new(cli: Cli) -> Result<Self, Error> {
let path_str = config_path("config.toml");
let p = Path::new(&path_str);
if !p.exists() {
create_dir_all(p.parent().unwrap()).unwrap();
let default_toml_string = toml::to_string(&CompleteConfig::default()).unwrap();
let mut file = File::create(path_str.clone()).unwrap();
file.write_all(default_toml_string.as_bytes()).unwrap();
bail!("Configuration was generated at {path_str}, please fill it out with necessary information.")
} else if let Ok(config_contents) = read_to_string(p) {
let mut config: CompleteConfig = toml::from_str(config_contents.as_str()).unwrap();
merge_args_into_config(&mut config, cli);
let token: Option<&'static str> = option_env!("TWT_TOKEN");
if let Some(env_token) = token {
if !env_token.is_empty() {
config.twitch.token = Some(env_token.to_string());
}
}
{
let t = &config.twitch;
let check_token = t.token.as_ref().map_or("", |t| t);
if t.username.is_empty() || t.channel.is_empty() || check_token.is_empty() {
bail!("Twitch config section is missing one or more of the following: username, channel, token.");
}
}
config.twitch.channel = config.twitch.channel.to_lowercase();
Ok(config)
} else {
bail!(
"Configuration could not be read correctly. See the following link for the example config: {}",
format!("{}/blob/main/default-config.toml", env!("CARGO_PKG_REPOSITORY"))
)
}
}
}