1use crate::display::{print_entries, print_success};
2use clap::{Parser, Subcommand};
3use hostcraft_core::{HostCraftError, file, host, platform::write_hosts_to};
4use std::{error::Error, net::IpAddr, path::PathBuf};
5
6#[derive(Parser)]
7#[command(
8 name = "hostcraft",
9 about = "Manage your system hosts file from the terminal",
10 version
11)]
12pub struct Cli {
13 #[arg(long)]
15 pub file: Option<String>,
16
17 #[command(subcommand)]
18 pub command: Command,
19}
20
21#[derive(Subcommand)]
22pub enum Command {
23 List,
25
26 Add {
28 name: String,
30
31 ip: IpAddr,
33 },
34
35 Edit {
37 old_name: String,
39
40 #[arg(long)]
42 new_ip: Option<IpAddr>,
43
44 #[arg(long)]
46 new_name: Option<String>,
47 },
48
49 Remove {
51 name: String,
53
54 #[arg(long)]
56 partial: bool,
57 },
58
59 Toggle {
61 name: String,
63
64 #[arg(long)]
66 partial: bool,
67 },
68
69 Update,
71}
72
73pub fn run(cli: Cli) -> Result<(), Box<dyn Error>> {
74 if matches!(cli.command, Command::Update) {
75 return crate::update::handle_update();
76 }
77
78 let path = match cli.file {
79 Some(ref p) => PathBuf::from(p),
80 None => hostcraft_core::platform::get_hosts_path().map_err(|e| e.to_string())?,
81 };
82
83 let lines = file::read_file(&path)
84 .map_err(|e| format!("Failed to read hosts file '{}': {}", path.display(), e))?;
85
86 let mut entries = host::parse_contents(lines);
87
88 match cli.command {
89 Command::List => {
90 print_entries(&entries);
91 }
92
93 Command::Add { name, ip } => {
94 host::add_entry(&mut entries, ip, name.as_str()).map_err(|e| match e {
95 HostCraftError::DuplicateEntry => format!("Entry already exists. {}", e),
96 _ => format!("Failed to add entry: {}", e),
97 })?;
98
99 write_hosts_to(&path, &entries).map_err(|e| e.to_string())?;
100
101 print_success(&format!("Added '{}'", name));
102 print_entries(&entries);
103 }
104
105 Command::Edit {
106 old_name,
107 new_ip,
108 new_name,
109 } => {
110 if new_ip.is_none() && new_name.is_none() {
111 return Err("Provide at least one of --new_ip or --new_name".into());
112 }
113
114 let (current_ip, current_name) = entries
115 .iter()
116 .find(|e| e.name == old_name)
117 .map(|e| (e.ip, e.name.clone()))
118 .ok_or_else(|| format!("No entry found with exact name '{}'.", old_name))?;
119
120 let resolved_ip = new_ip.unwrap_or(current_ip);
121 let resolved_name = new_name.as_deref().unwrap_or(¤t_name);
122
123 host::edit_entry(&mut entries, &old_name, resolved_ip, resolved_name).map_err(|e| {
124 match e {
125 HostCraftError::EntryNotFound => {
126 format!("No entry found with exact name '{}'. {}", old_name, e)
127 }
128 HostCraftError::DuplicateEntry => format!("Entry already exists. {}", e),
129 HostCraftError::NoChange => format!("No changes made to entry '{}'.", old_name),
130 _ => format!("Failed to edit entry: {}", e),
131 }
132 })?;
133
134 write_hosts_to(&path, &entries).map_err(|e| e.to_string())?;
135
136 print_success(&format!("Edited '{}'", old_name));
137 print_entries(&entries);
138 }
139
140 Command::Remove { name, partial } => {
141 let removed_count = if partial {
142 host::remove_entries_matching(&mut entries, &name).map_err(|e| match e {
143 HostCraftError::EntryNotFound => {
144 format!("No entries found containing '{}'. {}", name, e)
145 }
146 _ => format!("Failed to remove entry: {}", e),
147 })?
148 } else {
149 host::remove_entry(&mut entries, &name).map_err(|e| match e {
150 HostCraftError::EntryNotFound => {
151 format!("No entry found with exact name '{}'. {}", name, e)
152 }
153 _ => format!("Failed to remove entry: {}", e),
154 })?;
155 1
156 };
157
158 write_hosts_to(&path, &entries).map_err(|e| e.to_string())?;
159
160 if partial {
161 print_success(&format!(
162 "Removed {} {} containing '{}'",
163 removed_count,
164 if removed_count == 1 {
165 "entry"
166 } else {
167 "entries"
168 },
169 name
170 ));
171 } else {
172 print_success(&format!("Removed '{}'", name));
173 }
174 print_entries(&entries);
175 }
176
177 Command::Toggle { name, partial } => {
178 let toggled_count = if partial {
179 host::toggle_entries_matching(&mut entries, &name).map_err(|e| match e {
180 HostCraftError::EntryNotFound => {
181 format!("No entries found containing '{}'. {}", name, e)
182 }
183 _ => format!("Failed to toggle entry: {}", e),
184 })?
185 } else {
186 host::toggle_entry(&mut entries, &name).map_err(|e| match e {
187 HostCraftError::EntryNotFound => {
188 format!("No entry found with exact name '{}'. {}", name, e)
189 }
190 _ => format!("Failed to toggle entry: {}", e),
191 })?;
192 1
193 };
194
195 write_hosts_to(&path, &entries).map_err(|e| e.to_string())?;
196
197 if partial {
198 print_success(&format!(
199 "Toggled {} {} containing '{}'",
200 toggled_count,
201 if toggled_count == 1 {
202 "entry"
203 } else {
204 "entries"
205 },
206 name
207 ));
208 } else {
209 print_success(&format!("Toggled '{}'", name));
210 }
211 print_entries(&entries);
212 }
213
214 Command::Update => unreachable!("handled above"),
215 }
216
217 Ok(())
218}