azterm 0.1.6

Fast, modern native cross-platform terminal, SSH profile manager, and SFTP client
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
use crate::db::Database;
use crate::settings::BackspaceSequence;
use crate::theme::{setting_row_disabled, setting_row_toggle, ThemeConfig};
use crate::{AppState, SettingsCategory};
use eframe::egui;

pub fn render_settings_view(app: &mut AppState, ctx: &egui::Context, ui: &mut egui::Ui) {
    ui.columns(2, |columns| {
        columns[0].set_max_width(210.0);
        columns[0].vertical(|ui| {
            ui.add_space(10.0);
            ui.label(egui::RichText::new("Preferences").strong().size(16.0).color(app.theme.text_primary_color()));
            ui.add_space(12.0);

            let nav_item = |ui: &mut egui::Ui, cat: SettingsCategory, label: &str, current: SettingsCategory, theme: &ThemeConfig| -> bool {
                let is_active = current == cat;
                let bg = if is_active { theme.bg_card_color() } else { egui::Color32::TRANSPARENT };
                let stroke = if is_active { egui::Stroke::new(1.0_f32, theme.accent_color()) } else { egui::Stroke::NONE };

                egui::Frame::none()
                    .fill(bg)
                    .stroke(stroke)
                    .rounding(4.0)
                    .inner_margin(egui::Margin::symmetric(10.0, 8.0))
                    .show(ui, |ui| {
                        ui.set_width(180.0);
                        let text_color = if is_active { theme.accent_color() } else { theme.text_primary_color() };
                        ui.selectable_label(is_active, egui::RichText::new(label).color(text_color)).clicked()
                    }).inner
            };

            if nav_item(ui, SettingsCategory::Appearance, "Themes & Window", app.settings_category, &app.theme) {
                app.settings_category = SettingsCategory::Appearance;
            }
            ui.add_space(4.0);
            if nav_item(ui, SettingsCategory::Terminal, "Terminal Interaction", app.settings_category, &app.theme) {
                app.settings_category = SettingsCategory::Terminal;
            }
            ui.add_space(4.0);
            if nav_item(ui, SettingsCategory::ShellEnv, "Shell & Environment", app.settings_category, &app.theme) {
                app.settings_category = SettingsCategory::ShellEnv;
            }
            ui.add_space(4.0);
            if nav_item(ui, SettingsCategory::Sftp, "SFTP & Transfers", app.settings_category, &app.theme) {
                app.settings_category = SettingsCategory::Sftp;
            }
            ui.add_space(4.0);
            if nav_item(ui, SettingsCategory::System, "Application & System", app.settings_category, &app.theme) {
                app.settings_category = SettingsCategory::System;
            }

            ui.add_space(20.0);
            ui.separator();
            ui.add_space(8.0);
            if ui.button("Restore Defaults").clicked() {
                app.settings = crate::settings::AppSettings::default();
                app.settings.save();
                app.theme = ThemeConfig::default();
                Database::save_active_theme(&app.theme);
                ctx.set_zoom_factor(1.0);
                app.set_toast("Defaults Restored");
            }
        });

        columns[1].vertical(|ui| {
            ui.add_space(10.0);
            let mut changed = false;

            app.theme.card_frame().show(ui, |ui| {
                egui::ScrollArea::vertical().show(ui, |ui| {
                    match app.settings_category {
                        SettingsCategory::Appearance => {
                            ui.label(egui::RichText::new("Themes & Window Appearance").strong().size(16.0).color(app.theme.accent_color()));
                            ui.label(egui::RichText::new("Choose from built-in themes, customize colors, transparency, and window decorations.").small().color(app.theme.text_muted_color()));
                            ui.add_space(12.0);

                            if setting_row_toggle(
                                ui,
                                "Use OS Native Title Bar",
                                "Use system window decorations. Toggle off to use the sleek custom integrated AZTerm titlebar.",
                                &mut app.settings.use_system_titlebar,
                                &app.theme,
                            ) {
                                ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(app.settings.use_system_titlebar));
                                changed = true;
                            }

                            // Zoom Factor Setting
                            ui.horizontal(|ui| {
                                ui.vertical(|ui| {
                                    ui.label(egui::RichText::new("Window Zoom Level").strong().color(app.theme.text_primary_color()));
                                    ui.label(egui::RichText::new("Current scale (Ctrl +, Ctrl -, Ctrl 0 to reset).").small().color(app.theme.text_muted_color()));
                                });
                                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                                    if ui.button("Reset (100%)").clicked() {
                                        app.settings.zoom_factor = 1.0;
                                        ctx.set_zoom_factor(1.0);
                                        changed = true;
                                    }
                                    ui.label(format!("{}%", (app.settings.zoom_factor * 100.0).round() as u32));
                                });
                            });
                            ui.add_space(8.0);
                            ui.separator();
                            ui.add_space(8.0);

                            ui.horizontal(|ui| {
                                ui.vertical(|ui| {
                                    ui.label(egui::RichText::new("Window Background Opacity").strong().color(app.theme.text_primary_color()));
                                    ui.label(egui::RichText::new("Set terminal transparency (20% to 100%). Live preview as you drag.").small().color(app.theme.text_muted_color()));
                                });
                                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                                    let pct = (app.theme.opacity * 100.0).round() as u32;
                                    ui.label(format!("{}%", pct));
                                    if ui.add(egui::Slider::new(&mut app.theme.opacity, 0.20..=1.0).show_value(false)).changed() {
                                        Database::save_active_theme(&app.theme);
                                    }
                                });
                            });
                            ui.add_space(8.0);
                            ui.separator();
                            ui.add_space(8.0);

                            ui.label(egui::RichText::new("Select Theme Preset").strong().color(app.theme.text_primary_color()));
                            ui.add_space(4.0);

                            let builtins = ThemeConfig::builtins();
                            let mut selected_theme_to_apply: Option<ThemeConfig> = None;

                            ui.horizontal_wrapped(|ui| {
                                for preset in &builtins {
                                    let is_active = app.theme.id == preset.id;
                                    let btn = ui.selectable_label(is_active, &preset.name);
                                    if btn.clicked() {
                                        let mut new_th = preset.clone();
                                        new_th.opacity = app.theme.opacity;
                                        selected_theme_to_apply = Some(new_th);
                                    }
                                }
                                for custom in &app.custom_themes {
                                    let is_active = app.theme.id == custom.id;
                                    let label = format!("* {}", custom.name);
                                    let btn = ui.selectable_label(is_active, label);
                                    if btn.clicked() {
                                        let mut new_th = custom.clone();
                                        new_th.opacity = app.theme.opacity;
                                        selected_theme_to_apply = Some(new_th);
                                    }
                                }
                            });

                            if let Some(new_th) = selected_theme_to_apply {
                                app.theme = new_th;
                                Database::save_active_theme(&app.theme);
                                app.set_toast(format!("Theme applied: {}", app.theme.name));
                            }

                            ui.add_space(8.0);
                            ui.horizontal(|ui| {
                                ui.text_edit_singleline(&mut app.new_theme_name);
                                if ui.button("+ Duplicate Current as Custom").clicked() {
                                    let mut custom = app.theme.clone();
                                    custom.id = format!("custom_{}", chrono::Utc::now().timestamp_millis());
                                    custom.name = app.new_theme_name.clone();
                                    custom.is_builtin = false;
                                    app.custom_themes.push(custom.clone());
                                    app.theme = custom;
                                    Database::save_custom_themes(&app.custom_themes);
                                    Database::save_active_theme(&app.theme);
                                    app.set_toast("Custom Theme Created");
                                }

                                if !app.theme.is_builtin {
                                    if ui.button("Delete This Custom Theme").clicked() {
                                        let delete_id = app.theme.id.clone();
                                        app.custom_themes.retain(|t| t.id != delete_id);
                                        Database::save_custom_themes(&app.custom_themes);
                                        app.theme = ThemeConfig::cyber_cyan();
                                        Database::save_active_theme(&app.theme);
                                        app.set_toast("Custom Theme Deleted");
                                    }
                                }
                            });

                            ui.add_space(10.0);
                            ui.separator();
                            ui.add_space(10.0);

                            ui.label(egui::RichText::new("Theme Colors (Real-Time Customization)").strong().color(app.theme.accent_color()));
                            ui.label(egui::RichText::new("Click any color swatch below to open the color picker. Changes take effect instantly.").small().color(app.theme.text_muted_color()));
                            ui.add_space(8.0);

                            let mut color_modified = false;

                            egui::Grid::new("colors_grid").num_columns(4).spacing([18.0, 8.0]).show(ui, |ui| {
                                ui.label("Terminal Background:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.bg_main).changed();

                                ui.label("Panel & Nav Background:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.bg_panel).changed();
                                ui.end_row();

                                ui.label("Card Background:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.bg_card).changed();

                                ui.label("Borders & Dividers:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.border).changed();
                                ui.end_row();

                                ui.label("Primary Accent:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.accent).changed();

                                ui.label("Accent Hover:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.accent_hover).changed();
                                ui.end_row();

                                ui.label("Primary Text:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.text_primary).changed();

                                ui.label("Muted Text:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.text_muted).changed();
                                ui.end_row();

                                ui.label("Success Badge:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.success).changed();

                                ui.label("Danger / Close:");
                                color_modified |= ui.color_edit_button_srgb(&mut app.theme.danger).changed();
                                ui.end_row();
                            });

                            ui.add_space(10.0);
                            ui.label(egui::RichText::new("Terminal 16 ANSI Palette").strong().color(app.theme.text_primary_color()));
                            ui.add_space(6.0);

                            egui::Grid::new("ansi_grid").num_columns(8).spacing([10.0, 6.0]).show(ui, |ui| {
                                let labels = ["Black", "Red", "Green", "Yellow", "Blue", "Magenta", "Cyan", "White"];
                                for (i, name) in labels.iter().enumerate() {
                                    ui.vertical(|ui| {
                                        ui.label(egui::RichText::new(*name).small().color(app.theme.text_muted_color()));
                                        color_modified |= ui.color_edit_button_srgb(&mut app.theme.ansi_colors[i]).changed();
                                    });
                                }
                                ui.end_row();

                                let bright_labels = ["Br-Black", "Br-Red", "Br-Green", "Br-Yellow", "Br-Blue", "Br-Magenta", "Br-Cyan", "Br-White"];
                                for (i, name) in bright_labels.iter().enumerate() {
                                    ui.vertical(|ui| {
                                        ui.label(egui::RichText::new(*name).small().color(app.theme.text_muted_color()));
                                        color_modified |= ui.color_edit_button_srgb(&mut app.theme.ansi_colors[i + 8]).changed();
                                    });
                                }
                                ui.end_row();
                            });

                            if color_modified {
                                if !app.theme.is_builtin {
                                    if let Some(existing) = app.custom_themes.iter_mut().find(|t| t.id == app.theme.id) {
                                        *existing = app.theme.clone();
                                        Database::save_custom_themes(&app.custom_themes);
                                    }
                                }
                                Database::save_active_theme(&app.theme);
                            }
                        }
                        SettingsCategory::Terminal => {
                            ui.label(egui::RichText::new("Terminal Interaction").strong().size(16.0).color(app.theme.accent_color()));
                            ui.label(egui::RichText::new("Configure mouse behavior, clipboard actions, and scrollback depth.").small().color(app.theme.text_muted_color()));
                            ui.add_space(12.0);

                            changed |= setting_row_toggle(ui, "Cursor Blink", "Animate cursor blinking in the active terminal buffer.", &mut app.settings.cursor_blink, &app.theme);
                            changed |= setting_row_toggle(ui, "Copy Selected Text on Select", "Automatically copy highlighted text to OS clipboard on drag release.", &mut app.settings.copy_on_select, &app.theme);
                            changed |= setting_row_toggle(ui, "Paste on Right Click", "Immediately write clipboard text into the terminal on right click.", &mut app.settings.paste_on_right_click, &app.theme);

                            ui.horizontal(|ui| {
                                ui.vertical(|ui| {
                                    ui.label(egui::RichText::new("Scrollback Buffer Depth").strong().color(app.theme.text_primary_color()));
                                    ui.label(egui::RichText::new("Total lines of output history retained per tab (scroll with mouse wheel).").small().color(app.theme.text_muted_color()));
                                });
                                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                                    egui::ComboBox::from_id_source("scrollback_depth_combo")
                                        .selected_text(format!("{} lines", app.settings.scrollback_lines))
                                        .show_ui(ui, |ui| {
                                            if ui.selectable_value(&mut app.settings.scrollback_lines, 2000, "2,000 lines").clicked() { changed = true; }
                                            if ui.selectable_value(&mut app.settings.scrollback_lines, 5000, "5,000 lines").clicked() { changed = true; }
                                            if ui.selectable_value(&mut app.settings.scrollback_lines, 10000, "10,000 lines").clicked() { changed = true; }
                                            if ui.selectable_value(&mut app.settings.scrollback_lines, 25000, "25,000 lines").clicked() { changed = true; }
                                            if ui.selectable_value(&mut app.settings.scrollback_lines, 50000, "50,000 lines").clicked() { changed = true; }
                                        });
                                });
                            });
                            ui.add_space(6.0);
                            ui.separator();
                            ui.add_space(6.0);

                            setting_row_disabled(ui, "Right Click Auto Select Word", "Double click/right click to select full alphanumeric words.", app.settings.right_click_select_word);
                            setting_row_disabled(ui, "Hold Ctrl / Meta to Open Links", "Require modifier key press before launching detected URL hyperlinks.", app.settings.must_hold_ctrl_for_links);
                            setting_row_disabled(ui, "Command Suggestions", "Display autocompletion hints based on history.", app.settings.show_command_suggestions);
                            setting_row_disabled(ui, "Auto Reconnect on Disconnect", "Automatically retry remote SSH sessions when connection drops.", app.settings.auto_reconnect_terminal);
                        }
                        SettingsCategory::ShellEnv => {
                            ui.label(egui::RichText::new("Shell & Environment").strong().size(16.0).color(app.theme.accent_color()));
                            ui.label(egui::RichText::new("Set your default command interpreter, session log directories, and keycodes.").small().color(app.theme.text_muted_color()));
                            ui.add_space(12.0);

                            ui.horizontal(|ui| {
                                ui.vertical(|ui| {
                                    ui.label(egui::RichText::new("Default Shell Path").strong().color(app.theme.text_primary_color()));
                                    ui.label(egui::RichText::new("Executable path spawned when opening new local tabs.").small().color(app.theme.text_muted_color()));
                                });
                                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                                    if ui.add(egui::TextEdit::singleline(&mut app.settings.default_shell).desired_width(180.0)).changed() {
                                        changed = true;
                                    }
                                });
                            });
                            ui.add_space(4.0);
                            ui.horizontal(|ui| {
                                ui.label(egui::RichText::new("Shell Presets:").small().color(app.theme.text_muted_color()));
                                if ui.small_button("bash").clicked() { app.settings.default_shell = "/bin/bash".to_string(); changed = true; }
                                if ui.small_button("zsh").clicked() { app.settings.default_shell = "/bin/zsh".to_string(); changed = true; }
                                if ui.small_button("fish").clicked() { app.settings.default_shell = "/bin/fish".to_string(); changed = true; }
                            });
                            ui.add_space(8.0);
                            ui.separator();
                            ui.add_space(8.0);

                            ui.horizontal(|ui| {
                                ui.vertical(|ui| {
                                    ui.label(egui::RichText::new("Backspace Keycode Sequence").strong().color(app.theme.text_primary_color()));
                                    ui.label(egui::RichText::new("Control character code sent to PTY upon pressing Backspace.").small().color(app.theme.text_muted_color()));
                                });
                                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                                    egui::ComboBox::from_id_source("backspace_seq_select")
                                        .selected_text(match app.settings.backspace_sequence {
                                            BackspaceSequence::Delete127 => "^? (Delete 0x7F)",
                                            BackspaceSequence::Backspace8 => "^H (Backspace 0x08)",
                                        })
                                        .show_ui(ui, |ui| {
                                            if ui.selectable_value(&mut app.settings.backspace_sequence, BackspaceSequence::Delete127, "^? (Delete 0x7F)").clicked() { changed = true; }
                                            if ui.selectable_value(&mut app.settings.backspace_sequence, BackspaceSequence::Backspace8, "^H (Backspace 0x08)").clicked() { changed = true; }
                                        });
                                });
                            });
                            ui.add_space(8.0);
                            ui.separator();
                            ui.add_space(8.0);

                            setting_row_disabled(ui, "Terminal Log Directory", "Target filesystem folder for saved session transcripts.", false);
                            setting_row_disabled(ui, "Save Terminal Session Logs", "Write all output streams into timestamped log files.", app.settings.save_terminal_log);
                            setting_row_disabled(ui, "Timestamp Log Entries", "Prefix each logged output line with local ISO timestamp.", app.settings.add_timestamp_to_log);
                        }
                        SettingsCategory::Sftp => {
                            ui.label(egui::RichText::new("SFTP & File Transfers").strong().size(16.0).color(app.theme.accent_color()));
                            ui.label(egui::RichText::new("Manage remote directory traversal, split paneling, and file syncing.").small().color(app.theme.text_muted_color()));
                            ui.add_space(12.0);

                            changed |= setting_row_toggle(ui, "Split View SFTP Explorer", "Show terminal on the left and directory browser on the right.", &mut app.settings.show_sftp_split_view, &app.theme);

                            setting_row_disabled(ui, "Synchronize SFTP with Terminal Path", "Automatically follow the current directory of the active shell.", app.settings.sftp_path_sync);
                            setting_row_disabled(ui, "Auto Refresh on Tab Switch", "Query remote directory metadata when navigating between sessions.", app.settings.auto_refresh_sftp);
                            setting_row_disabled(ui, "Show Hidden Dotfiles", "Display files and folders prefixed with a dot by default.", app.settings.show_hidden_sftp);
                            setting_row_disabled(ui, "Disable SFTP Transfer History", "Do not write upload/download records to disk.", app.settings.disable_sftp_history);
                        }
                        SettingsCategory::System => {
                            ui.label(egui::RichText::new("Application & System").strong().size(16.0).color(app.theme.accent_color()));
                            ui.label(egui::RichText::new("Window behavior, multi-instance options, and update checks.").small().color(app.theme.text_muted_color()));
                            ui.add_space(12.0);

                            changed |= setting_row_toggle(ui, "Open Default Tab on Startup", "Spawn a fresh local shell if no previous session was restored.", &mut app.settings.open_default_tab, &app.theme);

                            if setting_row_toggle(ui, "Check for Updates on Startup", "Check for newer releases on GitHub once daily.", &mut app.settings.check_updates, &app.theme) {
                                changed = true;
                                if !app.settings.check_updates {
                                    app.available_update = None;
                                    app.show_update_modal = false;
                                }
                            }

                            ui.horizontal(|ui| {
                                ui.vertical(|ui| {
                                    ui.label(egui::RichText::new("Manual Update Check").strong().color(app.theme.text_primary_color()));
                                    let status_text = if app.is_checking_update {
                                        "Checking GitHub releases...".to_string()
                                    } else if let Some(ref tag) = app.available_update {
                                        format!("New version available: {}", tag)
                                    } else {
                                        format!("Current version is v{}", env!("CARGO_PKG_VERSION"))
                                    };
                                    ui.label(egui::RichText::new(status_text).small().color(app.theme.text_muted_color()));
                                });
                                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                                    if ui.button("Check for Updates Now").clicked() {
                                        app.trigger_update_check(true, ctx.clone());
                                        app.set_toast("Checking for updates...");
                                    }
                                });
                            });
                            ui.add_space(6.0);
                            ui.separator();
                            ui.add_space(6.0);

                            setting_row_disabled(ui, "Allow Multi-Instance Execution", "Permit launching multiple independent AZTerm window processes.", app.settings.allow_multi_instance);
                            setting_row_disabled(ui, "Confirm Before Window Exit", "Ask for confirmation before terminating running session processes.", app.settings.confirm_before_exit);
                            setting_row_disabled(ui, "Mask Host IP Address", "Hide server IPs from status bars and session titles.", app.settings.hide_ip);
                            setting_row_disabled(ui, "Disable Connection History", "Do not cache recent SSH session targets in SQLite.", app.settings.disable_connection_history);
                            setting_row_disabled(ui, "Debug Logging Mode", "Emit verbose PTY and layout traces to stderr.", app.settings.debug_mode);
                        }
                    }
                });
            });

            if changed {
                app.settings.save();
            }
        });
    });
}