alman 0.1.2

A command-line tool and TUI for managing shell aliases with intelligent suggestions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
mod cli;
mod database;
mod ops;
mod tui;
mod shell;

use cli::arg_handler::parse_args;
use cli::cli_data::Operation;
use database::database_structs::{Database, DeletedCommands};
use database::persistence::{
    ensure_data_directory, get_database_path, get_deleted_commands_path, load_database,
    load_deleted_commands, save_database, save_deleted_commands, load_config, save_config, AppConfig
};
use ops::add_alias::add_alias;
use ops::delete_suggestion::delete_suggestion;
use ops::get_suggestions;
use ops::insert_command::insert_command;
use ops::remove_alias::remove_alias;
use shell::{ShellOpts, render_shell_init};
use std::env;
use tui::run_tui;
use colored::*;
use clap::CommandFactory;
use std::path::PathBuf;
use std::fs;
use std::path::Path;
use std::os::unix::fs::PermissionsExt;

fn to_absolute_path(path: &str) -> String {
    let pb = PathBuf::from(path);
    match pb.canonicalize() {
        Ok(abs) => abs.to_string_lossy().to_string(),
        Err(_) => {
            // If the file doesn't exist yet, canonicalize the parent
            if let Some(parent) = pb.parent() {
                if let Ok(abs_parent) = parent.canonicalize() {
                    return abs_parent.join(pb.file_name().unwrap_or_default()).to_string_lossy().to_string();
                }
            }
            pb.to_string_lossy().to_string()
        }
    }
}

fn is_system_command(cmd: &str) -> bool {
    if cmd.is_empty() {
        return false;
    }
    if let Ok(paths) = env::var("PATH") {
        for path in paths.split(":") {
            let full_path = Path::new(path).join(cmd);
            if full_path.exists() && fs::metadata(&full_path).map(|m| m.is_file() && (m.permissions().mode() & 0o111 != 0)).unwrap_or(false) {
                return true;
            }
        }
    }
    false
}

fn print_source_message() {
    let shell_path = std::env::var("SHELL").unwrap_or_default();
    let shell_file = if shell_path.contains("zsh") {
        "~/.zshrc"
    } else if shell_path.contains("bash") {
        "~/.bashrc"
    } else if shell_path.contains("fish") {
        "~/.config/fish/config.fish"
    } else {
        "your shell's config file"
    };
    // ANSI green: \x1b[32m ... \x1b[0m
    println!("\nTo use your new aliases immediately, run: \x1b[32msource {}\x1b[0m", shell_file);
}

fn main() {
    // Intercept --help/-h to show dynamic default alias file path
    let args: Vec<String> = std::env::args().collect();
    if args.iter().any(|arg| arg == "--help" || arg == "-h") {
        // Load config and get default alias file path
        let default_path = {
            if let Some(cfg) = crate::database::persistence::load_config() {
                cfg.alias_file_paths.first().map(|p| to_absolute_path(p)).unwrap_or_else(|| crate::database::persistence::get_default_alias_file_path())
            } else {
                crate::database::persistence::get_default_alias_file_path()
            }
        };
        println!("Current default alias file path: {}\n", default_path.green());
        <crate::cli::cli_data::Cli as CommandFactory>::command().print_help().unwrap();
        println!();
        std::process::exit(0);
    }

    // Ensure data directory exists
    if let Err(e) = ensure_data_directory() {
        eprintln!("Failed to create data directory: {}", e);
        return;
    }

    // Load config for alias file paths
    let config = load_config();
    let mut alias_file_paths = if let Some(cfg) = &config {
        cfg.alias_file_paths.clone()
    } else {
        vec![crate::database::persistence::get_default_alias_file_path()]
    };

    // Parse CLI args
    let command_strings: Vec<String> = env::args().collect();

    // If no arguments (just the binary name), launch TUI by default
    if command_strings.len() == 1 {
        // Use the first alias file path for TUI
        let file_path = alias_file_paths.first().unwrap_or(&crate::database::persistence::get_default_alias_file_path()).clone();
        if let Err(e) = run_tui(std::path::PathBuf::from(file_path), alias_file_paths) {
            eprintln!("{}", format!("TUI error: {}", e).red());
        }
        return;
    }

    let cli = if command_strings.len() > 1 && command_strings[1] != "custom" {
        Some(parse_args())
    } else {
        None
    };

    // If only --alias-file-path is provided (no subcommand), update config and exit
    if let Some(cli) = &cli {
        if cli.operation.is_none() && cli.alias_file_path.is_some() {
            let cli_path_str = to_absolute_path(&cli.alias_file_path.as_ref().unwrap().to_string_lossy());
            if !alias_file_paths.contains(&cli_path_str) {
                alias_file_paths.push(cli_path_str.clone());
            }
            // Move the new path to the front (make it default)
            if let Some(pos) = alias_file_paths.iter().position(|p| p == &cli_path_str) {
                let new_default = alias_file_paths.remove(pos);
                alias_file_paths.insert(0, new_default);
            }
            let new_config = AppConfig { alias_file_paths: alias_file_paths.clone() };
            let _ = save_config(&new_config);
            println!("Default alias file path set to {}", cli_path_str.green());
            return;
        }
    }

    // If CLI provided alias_file_path, add it to the list and update config
    if let Some(ref cli) = cli {
        if let Some(ref cli_path) = cli.alias_file_path {
            let cli_path_str = to_absolute_path(&cli_path.to_string_lossy());
            if !alias_file_paths.contains(&cli_path_str) {
                alias_file_paths.push(cli_path_str.clone());
                let new_config = AppConfig { alias_file_paths: alias_file_paths.clone() };
                let _ = save_config(&new_config);
            }
        }
    }

    // Load database and deleted commands from persistent storage
    let db_path = get_database_path();
    let deleted_commands_path = get_deleted_commands_path();

    let mut db = match load_database(&db_path) {
        Ok(db) => db,
        Err(e) => {
            eprintln!("{}", format!("Failed to load database: {}", e).red());
            Database {
                command_list: std::collections::BTreeSet::new(),
                reverse_command_map: std::collections::HashMap::new(),
                total_num_commands: 0,
                total_score: 0,
            }
        }
    };

    let mut deleted_commands = match load_deleted_commands(&deleted_commands_path) {
        Ok(dc) => dc,
        Err(e) => {
            eprintln!("{}", format!("Failed to load deleted commands: {}", e).red());
            DeletedCommands {
                deleted_commands: std::collections::BTreeSet::new(),
            }
        }
    };

    let db_ref: &mut Database = &mut db;
    let dc_ref: &mut DeletedCommands = &mut deleted_commands;

    // Check if this is a custom command (starts with "custom")
    if command_strings[1] == "custom" {
        // This is a direct command to insert
        if command_strings.len() < 3 {
            eprintln!("Usage: {} custom <command>", command_strings[0]);
            eprintln!("Example: {} custom 'ls -la'", command_strings[0]);
            return;
        }
        
        let command = command_strings[2..].join(" ");
        insert_command(command.to_string(), db_ref, dc_ref);

        // Save database after inserting command
        if let Err(e) = save_database(db_ref, &db_path) {
            eprintln!("Failed to save database: {}", e);
        }
        if let Err(e) = save_deleted_commands(dc_ref, &deleted_commands_path) {
            eprintln!("Failed to save deleted commands: {}", e);
        }
    } else {
        // This is a subcommand, parse and handle it
        let cli = parse_args();

        match &cli.operation {
            Some(Operation::Add { alias, command }) => {
                use ops::alias_ops::add_alias_to_multiple_files;
                add_alias_to_multiple_files(&alias_file_paths, alias, command);
                if let Some(first_path) = alias_file_paths.first() {
                    add_alias(db_ref, dc_ref, first_path, alias, command);
                }
                if let Err(e) = save_database(db_ref, &db_path) {
                    eprintln!("{}", format!("Failed to save database: {}", e).red());
                }
                if let Err(e) = save_deleted_commands(dc_ref, &deleted_commands_path) {
                    eprintln!("{}", format!("Failed to save deleted commands: {}", e).red());
                }
                print_source_message();
            }
            Some(Operation::Remove { alias }) => {
                use ops::alias_ops::remove_alias_from_multiple_files;
                remove_alias_from_multiple_files(&alias_file_paths, alias);
                if let Some(first_path) = alias_file_paths.first() {
                    remove_alias(dc_ref, first_path, alias);
                }
                if let Err(e) = save_deleted_commands(dc_ref, &deleted_commands_path) {
                    eprintln!("{}", format!("Failed to save deleted commands: {}", e).red());
                }
                print_source_message();
            }
            Some(Operation::List) => {
                use ops::alias_ops::get_aliases_from_multiple_files;
                let aliases = get_aliases_from_multiple_files(&alias_file_paths);
                if aliases.is_empty() {
                    println!("{}", "No aliases found.".yellow());
                    return;
                }
                let max_alias_length = aliases.iter().map(|(alias, _)| alias.len()).max().unwrap_or(5).max(5);
                let max_command_length = aliases.iter().map(|(_, command)| command.len()).max().unwrap_or(7).max(7);
                let _total_width = 3 + max_alias_length + 3 + max_command_length + 2;
                println!("{}", format!("┌{:─<alias$}┬{:─<cmd$}┐", "", "", alias = max_alias_length + 2, cmd = max_command_length + 2).cyan());
                println!("{}", format!("│ {:<alias$} │ {:<cmd$} │", "ALIAS", "COMMAND", alias = max_alias_length, cmd = max_command_length).cyan());
                println!("{}", format!("├{:─<alias$}┼{:─<cmd$}┤", "", "", alias = max_alias_length + 2, cmd = max_command_length + 2).cyan());
                for (alias, command) in &aliases {
                    println!("{}{}",
                        format!("{:<width$}", alias, width = max_alias_length).cyan(),
                        format!("{:<width$}", command, width = max_command_length)
                    );
                }
                println!("{}", format!("└{:─<alias$}┴{:─<cmd$}┘", "", "", alias = max_alias_length + 2, cmd = max_command_length + 2).cyan());
                println!("{}", format!("Total: {} alias(es) across {} file(s)", aliases.len(), alias_file_paths.len()).green());
            }
            Some(Operation::Change { old_alias, new_alias }) => {
                use ops::alias_ops::remove_alias_from_multiple_files;
                use ops::alias_ops::add_alias_to_multiple_files_force;
                use ops::alias_ops::get_aliases_from_multiple_files;
                
                // Get all aliases to find the command for the old alias
                let aliases = get_aliases_from_multiple_files(&alias_file_paths);
                let old_command = aliases.iter()
                    .find(|(alias, _)| alias == old_alias)
                    .map(|(_, command)| command.clone());
                
                if let Some(command) = old_command {
                    // First remove the old alias from all files
                    remove_alias_from_multiple_files(&alias_file_paths, &old_alias);
                    if let Some(first_path) = alias_file_paths.first() {
                        remove_alias(dc_ref, first_path, &old_alias);
                    }
                    // Then add the new alias with the same command (force add)
                    add_alias_to_multiple_files_force(&alias_file_paths, &new_alias, &command);
                    if let Some(first_path) = alias_file_paths.first() {
                        add_alias(db_ref, dc_ref, first_path, &new_alias, &command);
                    }
                    if let Err(e) = save_database(db_ref, &db_path) {
                        eprintln!("{}", format!("Failed to save database: {}", e).red());
                    }
                    if let Err(e) = save_deleted_commands(dc_ref, &deleted_commands_path) {
                        eprintln!("{}", format!("Failed to save deleted commands: {}", e).red());
                    }
                    print_source_message();
                } else {
                    eprintln!("{}", format!("Alias '{}' not found.", old_alias).red());
                }
            }
            Some(Operation::GetSuggestions { num }) => {
                // Get total number of commands in the database
                let total_commands = db_ref.command_list.len();
                if let Some(n) = num {
                    if *n == 0 {
                        eprintln!("{}", "Number of suggestions must be greater than 0.".red());
                        return;
                    }
                    if *n > total_commands {
                        eprintln!("{}", format!("Requested number of suggestions (n = {}) exceeds total available commands ({}).", n, total_commands).red());
                        return;
                    }
                }
                let list = get_suggestions::get_suggestions_with_aliases(*num, db_ref, alias_file_paths.first().unwrap_or(&crate::database::persistence::get_default_alias_file_path()));
                
                if list.is_empty() {
                    println!("{}", "No suggestions found.".yellow());
                    return;
                }

                // Prepare filtered list: only top alias, and not a system command
                let filtered: Vec<_> = list.iter().map(|cmd| {
                    let top_alias = cmd.alias_suggestions.iter().find(|a| !is_system_command(&a.alias));
                    (cmd, top_alias)
                }).collect();

                // Find the longest command, alias, and score for alignment
                let max_command_length = filtered.iter().map(|(cmd, _)| cmd.command.command_text.len()).max().unwrap_or(7).max(7); // at least 'COMMAND'
                let max_alias_length = filtered.iter().map(|(_, alias_opt)| alias_opt.map(|a| a.alias.len()).unwrap_or(0)).max().unwrap_or(9).max(9); // at least 'TOP ALIAS'
                let max_score_length = filtered.iter().map(|(cmd, _)| cmd.command.score.to_string().len()).max().unwrap_or(5).max(5); // at least 'SCORE'

                // Table width: borders + padding + columns
                let _total_width = 3 + max_command_length + 3 + max_alias_length + 3 + max_score_length + 2; // | command | alias | score |

                // Top border
                println!("{}", format!("┌{:─<cmd$}┬{:─<alias$}┬{:─<score$}┐", "", "", "", cmd = max_command_length + 2, alias = max_alias_length + 2, score = max_score_length + 2).cyan());
                // Header
                println!("{}", format!("│ {:<cmd$} │ {:>alias$} │ {:>score$} │", "COMMAND", "TOP ALIAS", "SCORE", cmd = max_command_length, alias = max_alias_length, score = max_score_length).cyan());
                // Separator
                println!("{}", format!("├{:─<cmd$}┼{:─<alias$}┼{:─<score$}┤", "", "", "", cmd = max_command_length + 2, alias = max_alias_length + 2, score = max_score_length + 2).cyan());

                // Rows
                for (cmd_with_alias, top_alias_opt) in &filtered {
                    let command_text = format!("{:<width$}", cmd_with_alias.command.command_text, width = max_command_length);
                    let alias_text = if let Some(top_alias) = top_alias_opt {
                        format!("{:>width$}", top_alias.alias, width = max_alias_length)
                    } else {
                        format!("{:>width$}", "", width = max_alias_length)
                    };
                    let score_text = format!("{:>width$}", cmd_with_alias.command.score, width = max_score_length);
                    println!("{}{}{}",
                        command_text.bold(),
                        alias_text.cyan(),
                        score_text.yellow()
                    );
                }

                // Bottom border
                println!("{}", format!("└{:─<cmd$}┴{:─<alias$}┴{:─<score$}┘", "", "", "", cmd = max_command_length + 2, alias = max_alias_length + 2, score = max_score_length + 2).cyan());
                println!("{}", format!("Total: {} suggestion(s)", filtered.len()).green());
            }
            Some(Operation::DeleteSuggestion { alias }) => {
                delete_suggestion(alias, db_ref, dc_ref);
                println!("{}", format!("Deleted suggestions for: {}", alias).yellow());
                if let Err(e) = save_database(db_ref, &db_path) {
                    eprintln!("{}", format!("Failed to save database: {}", e).red());
                }
                if let Err(e) = save_deleted_commands(dc_ref, &deleted_commands_path) {
                    eprintln!("{}", format!("Failed to save deleted commands: {}", e).red());
                }
            }
            Some(Operation::Tui) => {
                let tui_path = cli.alias_file_path.clone().unwrap_or_else(|| {
                    alias_file_paths.first().unwrap_or(&crate::database::persistence::get_default_alias_file_path()).into()
                });
                if let Err(e) = run_tui(tui_path, alias_file_paths) {
                    eprintln!("{}", format!("TUI error: {}", e).red());
                }
            }
            Some(Operation::Init { shell }) => {
                let opts = ShellOpts::new();
                let init_script = render_shell_init(shell.clone(), &opts);
                println!("{}", init_script);
            }
            Some(Operation::InitData) => {
                // Initialize the data directory (for database and deleted commands)
                if let Err(e) = ensure_data_directory() {
                    eprintln!("Failed to create data directory: {}", e);
                    return;
                }
                
                // Initialize the config directory (for config and aliases)
                if let Err(e) = crate::database::persistence::ensure_config_directory() {
                    eprintln!("Failed to create config directory: {}", e);
                    return;
                }
                
                // Create default config if it doesn't exist
                if load_config().is_none() {
                    let default_config = AppConfig {
                        alias_file_paths: vec![crate::database::persistence::get_default_alias_file_path()],
                    };
                    if let Err(e) = save_config(&default_config) {
                        eprintln!("Failed to save config: {}", e);
                    }
                }
                
                // Create empty database and deleted commands files if they don't exist
                let db_path = get_database_path();
                let deleted_commands_path = get_deleted_commands_path();
                
                if !std::path::Path::new(&db_path).exists() {
                    let empty_db = Database {
                        command_list: std::collections::BTreeSet::new(),
                        reverse_command_map: std::collections::HashMap::new(),
                        total_num_commands: 0,
                        total_score: 0,
                    };
                    if let Err(e) = save_database(&empty_db, &db_path) {
                        eprintln!("Failed to create database file: {}", e);
                    }
                }
                
                if !std::path::Path::new(&deleted_commands_path).exists() {
                    let empty_deleted = DeletedCommands {
                        deleted_commands: std::collections::BTreeSet::new(),
                    };
                    if let Err(e) = save_deleted_commands(&empty_deleted, &deleted_commands_path) {
                        eprintln!("Failed to create deleted commands file: {}", e);
                    }
                }
                
                // Create default alias file if it doesn't exist
                let default_alias_path = crate::database::persistence::get_default_alias_file_path();
                if !std::path::Path::new(&default_alias_path).exists() {
                    // Ensure the parent directory exists
                    if let Some(parent) = std::path::Path::new(&default_alias_path).parent() {
                        if let Err(e) = std::fs::create_dir_all(parent) {
                            eprintln!("Failed to create alias file directory: {}", e);
                            return;
                        }
                    }
                    if let Err(e) = std::fs::write(&default_alias_path, "# Alman aliases file\n") {
                        eprintln!("Failed to create alias file: {}", e);
                    }
                }
            }
            None => {}
        }
    }
}