1use crate::cluster::Cluster;
3use crate::command::{Command, MainCommand, UtilityCommands};
4use crate::theme::init_themes_file;
5use app::configuration::{ClusterConfig, GlobalConfig, SchemaRegistryConfig, YozefuConfig};
6use app::APPLICATION_NAME;
7use clap::command;
8use lib::Error;
9use reqwest::Url;
10use std::fs;
11use std::path::PathBuf;
12use tui::error::TuiError;
13
14pub use clap::Parser;
15use indexmap::IndexMap;
16
17#[derive(Parser)]
20#[command(author, version, about = "A terminal user interface to navigate Kafka topics and search for Kafka records.", name = APPLICATION_NAME, bin_name = APPLICATION_NAME, display_name = APPLICATION_NAME, long_about = None, propagate_version = true, args_conflicts_with_subcommands = true)]
21pub struct Cli<T>
22where
23 T: Cluster,
24{
25 #[command(subcommand)]
26 subcommands: Option<UtilityCommands>,
27 #[command(flatten)]
28 default_command: MainCommand<T>,
29 #[clap(skip)]
30 logs_file: Option<PathBuf>,
31}
32
33impl<T> Cli<T>
34where
35 T: Cluster,
36{
37 pub async fn execute(&self) -> Result<(), TuiError> {
40 self.run(None).await
41 }
42
43 pub async fn execute_with(&self, yozefu_config: YozefuConfig) -> Result<(), TuiError> {
45 self.run(Some(yozefu_config)).await
46 }
47
48 pub fn cluster(&self) -> Option<T> {
51 match self.subcommands.is_some() {
52 true => None,
53 false => Some(self.default_command.cluster()),
54 }
55 }
56
57 pub fn is_main_command(&self) -> bool {
58 self.cluster().is_some()
59 }
60
61 pub fn logs_file(&mut self, logs: PathBuf) -> &mut Self {
63 self.logs_file = Some(logs);
64 self
65 }
66
67 async fn run(&self, yozefu_config: Option<YozefuConfig>) -> Result<(), TuiError> {
68 init_files().await?;
69 match &self.subcommands {
70 Some(c) => c.execute().await.map_err(|e| e.into()),
71 None => {
72 let yozefu_config = match yozefu_config {
74 None => self.default_command.yozefu_config()?,
75 Some(c) => c,
76 };
77 let mut command = self.default_command.clone();
78 command.logs_file = self.logs_file.clone();
79 command.execute(yozefu_config).await
80 }
81 }
82 }
83}
84
85async fn init_files() -> Result<(), Error> {
88 init_config_file()?;
89 init_themes_file().await?;
90 Ok(())
91}
92
93fn init_config_file() -> Result<PathBuf, Error> {
96 let path = GlobalConfig::path()?;
97 if fs::metadata(&path).is_ok() {
98 return Ok(path);
99 }
100 let mut config = GlobalConfig::try_from(&path)?;
101 let mut localhost_config = IndexMap::new();
102 localhost_config.insert(
103 "bootstrap.servers".to_string(),
104 "localhost:9092".to_string(),
105 );
106 localhost_config.insert("security.protocol".to_string(), "plaintext".to_string());
107 localhost_config.insert("broker.address.family".to_string(), "v4".to_string());
108 config
109 .default_kafka_config
110 .insert("fetch.min.bytes".to_string(), "10000".to_string());
111
112 config.clusters.insert(
113 "localhost".into(),
114 ClusterConfig {
115 kafka: localhost_config,
116 schema_registry: Some(SchemaRegistryConfig {
117 url: Url::parse("http://localhost:8081").unwrap(),
118 headers: Default::default(),
119 }),
120 ..Default::default()
121 },
122 );
123
124 fs::create_dir_all(config.filters_dir())?;
125 fs::write(&path, serde_json::to_string_pretty(&config).unwrap()).unwrap();
126 #[cfg(unix)]
127 {
128 use std::os::unix::fs::PermissionsExt;
129 let mut perms: fs::Permissions = fs::metadata(&path)?.permissions();
130 perms.set_mode(0o600);
131 fs::set_permissions(&path, perms)?;
132 }
133
134 Ok(path)
135}
136
137#[test]
138pub fn test_conflicts() {
139 use clap::CommandFactory;
140 Cli::<String>::command().debug_assert();
141}
142#[test]
143fn test_valid_themes() {
144 use std::collections::HashMap;
145 use tui::Theme;
146
147 let content = include_str!("../themes.json");
148 let themes: HashMap<String, Theme> = serde_json::from_str(content).unwrap();
149 assert!(themes.keys().len() >= 3)
150}