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
use crate::adb::{AdbManager, DeviceStatus};
use crate::devtools::DevToolsState;
use crate::effects::EffectsManager;
use crate::fastboot::FastbootManager;
use crate::logcat::LogcatState;
use crate::menu::{Menu, MenuCommand};
use crate::theme::{Theme, ThemeSelector};
use std::time::Instant;
/// Mode of the logcat save dialog.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogcatSaveMode {
/// Text input for the save path.
PathInput,
/// File explorer overlay for "Save As…" browsing.
FileBrowser,
}
/// Application state following Elm architecture
/// All mutable state is contained within this model
#[derive(Debug)]
pub struct Model {
/// Current application state
pub state: AppState,
/// Menu state and navigation
pub menu: Menu,
/// Visual effects manager
pub effects: EffectsManager,
/// Last tick time for animations
pub last_tick: Instant,
/// Command execution result (success)
pub command_result: Option<String>,
/// Command execution error
pub command_error: Option<String>,
/// Loading animation counter
pub loading_counter: u64,
/// Scroll position for result view
pub scroll_position: usize,
/// Original result lines (before wrapping)
pub result_lines: Vec<String>,
/// Wrapped lines for display (handles wide content)
pub wrapped_lines: Vec<String>,
/// Reveal animation counter for result display
pub reveal_counter: u64,
/// Whether the application should continue running
pub running: bool,
/// ADB client manager
pub adb_manager: AdbManager,
/// Fastboot manager (shells out to the `fastboot` binary)
pub fastboot_manager: FastbootManager,
/// Label of the last executed command (shown in the result title)
pub last_command_label: Option<String>,
/// Live device status shown in the header status bar.
pub device_status: DeviceStatus,
/// When true the next tick will fetch fresh device info.
pub needs_device_refresh: bool,
/// Logcat viewer state
pub logcat: LogcatState,
/// Whether the save dialog is active in logcat view.
pub logcat_save_active: bool,
/// The path input for the save dialog.
pub logcat_save_path: String,
/// Cursor position in the save path input.
pub logcat_save_cursor: usize,
/// Whether to save only filtered entries (true) or all entries (false).
pub logcat_save_filtered_only: bool,
/// Save format (TXT or JSON).
pub logcat_save_format: crate::logcat::SaveFormat,
/// Current mode of the save dialog.
pub logcat_save_mode: LogcatSaveMode,
/// File explorer for "Save As…" browsing.
pub logcat_file_explorer: Option<tui_file_explorer::FileExplorer>,
/// Dev Tools state.
pub devtools: DevToolsState,
/// Custom-ROM flasher state.
pub rom_flash: crate::rom_flash::RomFlashState,
/// Current active colour theme.
pub theme: Theme,
/// Theme selector overlay state.
pub theme_selector: ThemeSelector,
}
/// Application states
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppState {
/// Initial startup animation
Startup,
/// Main menu navigation
Menu,
/// Loading animation during command execution
Loading,
/// Showing command results
ShowResult,
/// Logcat viewer
Logcat,
/// Dev Tools workstation
DevMode,
/// Custom-ROM flasher
RomFlash,
}
impl Default for Model {
fn default() -> Self {
Self::new()
}
}
impl Model {
/// Create a new model with initial state
pub fn new() -> Self {
Self {
state: AppState::Menu,
menu: Menu::new(),
effects: EffectsManager::new(),
last_tick: Instant::now(),
command_result: None,
command_error: None,
loading_counter: 0,
scroll_position: 0,
result_lines: Vec::new(),
wrapped_lines: Vec::new(),
reveal_counter: 0,
running: true,
adb_manager: AdbManager::new(),
fastboot_manager: FastbootManager::new(),
last_command_label: None,
device_status: DeviceStatus::default(),
needs_device_refresh: true,
// Skip startup animation — go straight to menu
logcat: LogcatState::new(),
logcat_save_active: false,
logcat_save_path: String::new(),
logcat_save_cursor: 0,
logcat_save_filtered_only: false,
logcat_save_format: crate::logcat::SaveFormat::Text,
logcat_save_mode: LogcatSaveMode::PathInput,
logcat_file_explorer: None,
devtools: DevToolsState::new(),
rom_flash: crate::rom_flash::RomFlashState::new(),
theme: Theme::default(),
theme_selector: ThemeSelector::default(),
}
}
/// Check if the application should quit
pub fn should_quit(&self) -> bool {
!self.running
}
/// Check if we're in the result view
pub fn is_showing_result(&self) -> bool {
self.state == AppState::ShowResult
}
/// Check if we're in the menu
pub fn is_in_menu(&self) -> bool {
self.state == AppState::Menu
}
/// Check if we're in logcat view
pub fn is_in_logcat(&self) -> bool {
self.state == AppState::Logcat
}
/// Check if startup animation is complete
pub fn is_startup_complete(&self) -> bool {
self.state != AppState::Startup || self.effects.is_startup_complete()
}
/// Get the currently selected command
pub fn get_selected_command(&self) -> MenuCommand {
self.menu.get_selected_command()
}
/// Clear result state
pub fn clear_results(&mut self) {
self.command_result = None;
self.command_error = None;
self.scroll_position = 0;
self.result_lines.clear();
self.wrapped_lines.clear();
}
/// Set command result (success)
pub fn set_result(&mut self, output: String) {
self.command_result = Some(output.clone());
self.result_lines = output.lines().map(|s| s.to_string()).collect();
self.scroll_position = 0;
self.reveal_counter = 0;
}
/// Set command error
pub fn set_error(&mut self, error: String) {
self.command_error = Some(error.clone());
self.result_lines = error.lines().map(|s| s.to_string()).collect();
self.scroll_position = 0;
self.reveal_counter = 0;
}
/// Get total number of lines in current result
pub fn total_result_lines(&self) -> usize {
self.wrapped_lines.len()
}
/// Check if scrolling is available
pub fn can_scroll(&self) -> bool {
self.wrapped_lines.len() > 1
}
/// Update wrapped lines for current terminal width
pub fn update_wrapped_lines(&mut self, max_width: usize) {
if self.result_lines.is_empty() {
self.wrapped_lines = vec!["No output".to_string()];
return;
}
self.wrapped_lines = self
.result_lines
.iter()
.flat_map(|line| droidkraft_core::utils::wrap_text(line, max_width))
.collect();
}
}