mod wizard;
use super::data_storage::DataStorage;
use crate::api::gitlab::GitLabConfig;
use crate::api::jira::JiraConfig;
use crate::api::si::SiConfig;
use crate::libs::messages::Message;
use crate::libs::task::normalize_task_name;
use crate::msg_error;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::str;
pub const CONFIG_FILE_NAME: &str = "config.json";
#[derive(Debug, Clone)]
pub struct ConfigModule {
pub key: String,
pub name: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct MonitorConfig {
pub min_pause_duration: u64,
pub pause_threshold: u64,
pub poll_interval: u64,
pub activity_threshold: u64,
pub min_work_interval: u64,
#[serde(default = "default_pause_merge_gap")]
pub pause_merge_gap: u64,
}
fn default_pause_merge_gap() -> u64 {
30
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct ProductivityConfig {
pub min_productivity_threshold: f64,
pub workday_hours: f64,
pub min_workday_fraction_before_suggest: f64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub struct ReportConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub output_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename_template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct ServerConfig {
pub api_url: String,
pub auth_token: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct KaslServerConfig {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ca_certificate: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct Config {
#[serde(skip_serializing_if = "Option::is_none")]
pub si: Option<SiConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gitlab: Option<GitLabConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jira: Option<JiraConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub monitor: Option<MonitorConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<ServerConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kasl_server: Option<KaslServerConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub productivity: Option<ProductivityConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub report: Option<ReportConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_discovery: Option<TaskDiscoveryConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jira_inbox: Option<JiraInboxConfig>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct JiraInboxConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_jira_inbox_poll_interval")]
pub poll_interval_secs: u64,
#[serde(default = "default_true")]
pub notify: bool,
#[serde(default = "default_true")]
pub notify_changes: bool,
#[serde(default)]
pub notify_gone: bool,
#[serde(default)]
pub custom_fields: Vec<JiraCustomField>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort_by_field: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct JiraCustomField {
pub id: String,
pub label: String,
}
fn default_true() -> bool {
true
}
fn default_jira_inbox_poll_interval() -> u64 {
300
}
impl Default for JiraInboxConfig {
fn default() -> Self {
Self {
enabled: true,
poll_interval_secs: default_jira_inbox_poll_interval(),
notify: true,
notify_changes: true,
notify_gone: false,
custom_fields: Vec::new(),
sort_by_field: None,
}
}
}
impl JiraInboxConfig {
pub fn extra_field_ids(&self) -> Vec<String> {
let mut ids: Vec<String> = self.custom_fields.iter().map(|f| f.id.trim().to_string()).filter(|id| !id.is_empty()).collect();
if let Some(sort_id) = &self.sort_by_field {
let trimmed = sort_id.trim();
if !trimmed.is_empty() && !ids.iter().any(|id| id == trimmed) {
ids.push(trimmed.to_string());
}
}
ids
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct TaskDiscoveryConfig {
#[serde(default = "default_ignore_names")]
pub ignore_names: Vec<String>,
}
pub fn default_ignore_names() -> Vec<String> {
vec![
"Merge remote-tracking branch".to_string(),
"Merge branch ".to_string(),
"update webui".to_string(),
]
}
impl Default for TaskDiscoveryConfig {
fn default() -> Self {
Self {
ignore_names: default_ignore_names(),
}
}
}
impl Default for MonitorConfig {
fn default() -> Self {
MonitorConfig {
min_pause_duration: 20,
pause_threshold: 60,
poll_interval: 500,
activity_threshold: 30,
min_work_interval: 10,
pause_merge_gap: default_pause_merge_gap(),
}
}
}
impl Default for ProductivityConfig {
fn default() -> Self {
ProductivityConfig {
min_productivity_threshold: 75.0,
workday_hours: 8.0,
min_workday_fraction_before_suggest: 0.5,
}
}
}
impl Config {
pub fn read() -> Result<Config> {
let config_file_path = DataStorage::new().get_path(CONFIG_FILE_NAME)?;
if !config_file_path.exists() {
return Ok(Config::default());
}
let config_str = fs::read_to_string(config_file_path)?;
let config: Config = serde_json::from_str(&config_str)?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_file_path = DataStorage::new().get_path(CONFIG_FILE_NAME)?;
let json_content = serde_json::to_string_pretty(&self)?;
fs::write(config_file_path, json_content)?;
Ok(())
}
pub fn effective_ignore_names(&self) -> Vec<String> {
self.task_discovery
.as_ref()
.map(|c| c.ignore_names.clone())
.unwrap_or_else(default_ignore_names)
}
pub fn add_ignore_names(&mut self, names: &[String]) -> Result<usize> {
let mut discovery = self.task_discovery.clone().unwrap_or_default();
let mut added = 0;
for name in names {
let trimmed = name.trim();
if trimmed.is_empty() {
continue;
}
let key = normalize_task_name(trimmed);
let exists = discovery.ignore_names.iter().any(|existing| normalize_task_name(existing) == key);
if !exists {
discovery.ignore_names.push(trimmed.to_string());
added += 1;
}
}
self.task_discovery = Some(discovery);
self.save()?;
Ok(added)
}
pub fn set_app_global() -> Result<()> {
let current_exe_path = env::current_exe()?;
let exe_dir = current_exe_path.parent().unwrap();
let mut paths: Vec<PathBuf> = env::split_paths(&env::var_os("PATH").unwrap()).collect();
let str_paths: Vec<&str> = paths.iter().filter_map(|p| p.to_str()).collect();
if str_paths.contains(&exe_dir.to_str().unwrap()) {
return Ok(());
}
if paths.iter().any(|p| p.to_str() == Some(exe_dir.to_str().unwrap())) {
return Ok(());
}
paths.push(exe_dir.to_path_buf());
let new_path = env::join_paths(paths).unwrap_or_else(|_| panic!("{}", Message::FailedToJoinPaths.to_string()));
let path_key = r"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment";
let reg_query_output = Command::new("reg")
.arg("query")
.arg(path_key)
.arg("/v")
.arg("Path")
.output()
.unwrap_or_else(|_| panic!("{}", Message::FailedToExecuteRegQuery.to_string()));
if !reg_query_output.status.success() {
let status = reg_query_output.status.to_string();
msg_error!(Message::PathRegistryQueryError { status: status.clone() });
return Err(anyhow::anyhow!("{}", Message::PathRegistryQueryError { status }));
}
let current_path = str::from_utf8(®_query_output.stdout)
.unwrap_or_else(|_| panic!("{}", Message::FailedToParseRegOutput.to_string()))
.split_whitespace()
.last()
.unwrap_or_else(|| panic!("{}", Message::FailedToGetPathFromReg.to_string()));
let reg_set_output = Command::new("reg")
.arg("add")
.arg(path_key)
.arg("/v")
.arg("Path")
.arg("/t")
.arg("REG_EXPAND_SZ") .arg("/d")
.arg(format!("{};{}", current_path, new_path.to_string_lossy()))
.arg("/f") .output()
.unwrap_or_else(|_| panic!("{}", Message::FailedToExecuteRegSet.to_string()));
if !reg_set_output.status.success() {
let status = reg_set_output.status.to_string();
let stderr = String::from_utf8_lossy(®_set_output.stderr).to_string();
msg_error!(Message::PathRegistryUpdateError {
status: status.clone(),
stderr: stderr.clone()
});
return Err(anyhow::anyhow!("{}", Message::PathRegistryUpdateError { status, stderr }));
}
Ok(())
}
}