1#![allow(dead_code)]
10
11extern crate alloc;
12use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15
16use crate::kernel::draw::{Color, Screen};
17use crate::kernel::text_box::TextBox;
18use crate::kernel::window_mgr::App;
19use crate::os_lib::cloud::{self, DriveFile, GdriveAuth};
20
21#[derive(PartialEq, Clone, Copy)]
23enum Pane {
24 Remote,
25 Local,
26}
27
28#[derive(PartialEq, Clone, Copy)]
30enum SetupField {
31 ClientId,
32 ClientSecret,
33}
34
35pub struct DriveApp {
36 auth: GdriveAuth,
37 remote: Vec<DriveFile>,
39 local: Vec<String>,
40 remote_sel: usize,
41 local_sel: usize,
42 pane: Pane,
43 scroll: usize,
44 folder_path: Vec<(String, String)>,
46 device_user_code: String,
48 device_url: String,
49 device_code: String,
50 awaiting_auth: bool,
51 viewer_open: bool,
53 viewer_title: String,
54 viewer_text: String,
55 viewer_scroll: usize,
56 status: String,
58 last_win_w: u32,
60 setup_mode: bool,
62 setup_focus: SetupField,
63 setup_id_box: TextBox,
64 setup_secret_box: TextBox,
65}
66
67impl Default for DriveApp {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl DriveApp {
74 pub fn new() -> Self {
75 let auth = cloud::load_auth();
76 let mut app = Self {
77 auth,
78 remote: Vec::new(),
79 local: Vec::new(),
80 remote_sel: 0,
81 local_sel: 0,
82 pane: Pane::Remote,
83 scroll: 0,
84 folder_path: Vec::new(),
85 device_user_code: String::new(),
86 device_url: String::new(),
87 device_code: String::new(),
88 awaiting_auth: false,
89 viewer_open: false,
90 viewer_title: String::new(),
91 viewer_text: String::new(),
92 viewer_scroll: 0,
93 status: String::new(),
94 last_win_w: 900,
95 setup_mode: false,
96 setup_focus: SetupField::ClientId,
97 setup_id_box: TextBox::new(),
98 setup_secret_box: TextBox::new(),
99 };
100 app.reload_local();
101 if app.auth.is_logged_in() {
102 app.status = String::from("Logged in. Press R to load Drive files.");
103 } else if app.auth.has_client() {
104 app.status = String::from("Press L to log in to Google Drive.");
105 } else {
106 app.status = String::from("Setup required: press S to enter client_id/secret.");
107 }
108 app
109 }
110
111 fn reload_local(&mut self) {
112 let fs = crate::kernel::fs::get_fs();
113 self.local = fs
114 .list_files()
115 .iter()
116 .map(|m| m.get_filename().to_string())
117 .collect();
118 if self.local_sel >= self.local.len() {
119 self.local_sel = 0;
120 }
121 }
122
123 fn try_refresh(&mut self) -> bool {
125 if self.auth.refresh_token.is_empty() || !self.auth.has_client() {
126 return false;
127 }
128 match cloud::drive_refresh_token(
129 &self.auth.client_id,
130 &self.auth.client_secret,
131 &self.auth.refresh_token,
132 ) {
133 Ok(tok) if !tok.access_token.is_empty() => {
134 self.auth.access_token = tok.access_token;
135 if let Err(e) = cloud::save_auth(&self.auth) {
139 crate::error!("[DRIVE] refreshed token not saved: {}", e);
140 self.status = alloc::format!("Warning: token not saved ({})", e);
141 }
142 true
143 }
144 _ => false,
145 }
146 }
147
148 fn action_login(&mut self) {
151 if !self.auth.has_client() {
152 self.status = String::from("No client_id/secret. Edit gdrive_auth.json first.");
153 return;
154 }
155 self.status = String::from("Requesting device code...");
156 match cloud::drive_request_device_code(&self.auth.client_id) {
157 Ok(dc) if !dc.user_code.is_empty() => {
158 self.device_user_code = dc.user_code;
159 self.device_url = dc.verification_url;
160 self.device_code = dc.device_code;
161 self.awaiting_auth = true;
162 self.status =
163 String::from("Open the URL on another device, enter the code, then press P.");
164 }
165 Ok(_) => {
166 self.status =
167 String::from("Device code request returned empty (check client_id/scope).")
168 }
169 Err(e) => self.status = format!("Device code error: {}", e),
170 }
171 }
172
173 fn action_poll(&mut self) {
174 if !self.awaiting_auth {
175 return;
176 }
177 self.status = String::from("Polling for authorization...");
178 match cloud::drive_poll_token(
179 &self.auth.client_id,
180 &self.auth.client_secret,
181 &self.device_code,
182 ) {
183 Ok(tok) if !tok.access_token.is_empty() => {
184 self.auth.access_token = tok.access_token;
185 if !tok.refresh_token.is_empty() {
186 self.auth.refresh_token = tok.refresh_token;
187 }
188 self.awaiting_auth = false;
189 match cloud::save_auth(&self.auth) {
192 Ok(()) => self.status = String::from("Logged in! Loading Drive files..."),
193 Err(e) => {
194 crate::error!("[DRIVE] login token not saved: {}", e);
195 self.status =
196 alloc::format!("Logged in, but token NOT saved ({}). Login will not persist.", e);
197 }
198 }
199 self.action_refresh_remote();
200 }
201 Ok(tok) if tok.error == "authorization_pending" => {
202 self.status =
203 String::from("Still pending. Authorize in browser, then press P again.");
204 }
205 Ok(tok) if tok.error == "slow_down" => {
206 self.status = String::from("Slow down: wait a few seconds, then press P.");
207 }
208 Ok(tok) if !tok.error.is_empty() => {
209 self.status = format!("Auth error: {}", tok.error);
210 }
211 Ok(_) => self.status = String::from("No token yet. Press P again."),
212 Err(e) => self.status = format!("Poll error: {}", e),
213 }
214 }
215
216 fn current_folder_id(&self) -> Option<&str> {
218 self.folder_path.last().map(|(id, _)| id.as_str())
219 }
220
221 fn action_refresh_remote(&mut self) {
222 if !self.auth.is_logged_in() {
223 self.status = String::from("Not logged in.");
224 return;
225 }
226 self.status = String::from("Loading Drive files...");
227 let folder_id = self.current_folder_id().map(String::from);
228 match cloud::drive_list_files_in_folder(&self.auth.access_token, folder_id.as_deref()) {
229 Ok(files) => {
230 self.remote = files;
231 self.remote_sel = 0;
232 self.scroll = 0;
233 self.status = format!("Loaded {} Drive files.", self.remote.len());
234 }
235 Err("unauthorized (token expired?)") => {
236 if self.try_refresh() {
237 match cloud::drive_list_files_in_folder(
238 &self.auth.access_token,
239 folder_id.as_deref(),
240 ) {
241 Ok(files) => {
242 self.remote = files;
243 self.status = format!("Loaded {} Drive files.", self.remote.len());
244 }
245 Err(e) => self.status = format!("Drive list error after refresh: {}", e),
246 }
247 } else {
248 self.status =
249 String::from("Token expired and refresh failed. Press L to re-login.");
250 }
251 }
252 Err(e) => self.status = format!("Drive list error: {}", e),
253 }
254 }
255
256 fn action_enter_folder(&mut self, id: String, name: String) {
258 self.folder_path.push((id, name));
259 self.action_refresh_remote();
260 }
261
262 fn action_leave_folder(&mut self) {
264 if self.folder_path.pop().is_some() {
265 self.action_refresh_remote();
266 }
267 }
268
269 fn action_open_remote(&mut self) {
270 if self.remote.is_empty() {
271 return;
272 }
273 let f = self.remote[self.remote_sel].clone();
274 if f.mime_type == cloud::MIME_FOLDER {
275 self.action_enter_folder(f.id, f.name);
276 return;
277 }
278 self.status = format!("Fetching '{}'...", f.name);
279 let mut result = cloud::drive_get_text(&self.auth.access_token, &f.id, &f.mime_type);
280 if let Err("unauthorized (token expired?)") = result {
281 if self.try_refresh() {
282 result = cloud::drive_get_text(&self.auth.access_token, &f.id, &f.mime_type);
283 }
284 }
285 match result {
286 Ok(text) => {
287 self.viewer_title = f.name.clone();
288 self.viewer_text = text;
289 self.viewer_scroll = 0;
290 self.viewer_open = true;
291 self.status = format!("Opened '{}'. Press V to close.", f.name);
292 }
293 Err(e) => self.status = format!("Open error: {}", e),
294 }
295 }
296
297 fn action_download_remote(&mut self) {
300 if self.remote.is_empty() {
301 return;
302 }
303 let f = self.remote[self.remote_sel].clone();
304 if f.mime_type == cloud::MIME_FOLDER {
305 self.status = String::from("That is a folder.");
306 return;
307 }
308 self.status = format!("Downloading '{}'...", f.name);
309 let mut result = cloud::drive_download_binary(&self.auth.access_token, &f.id);
310 if let Err("unauthorized (token expired?)") = result {
311 if self.try_refresh() {
312 result = cloud::drive_download_binary(&self.auth.access_token, &f.id);
313 }
314 }
315 match result {
316 Ok(bytes) => {
317 let len = bytes.len();
318 let fs = crate::kernel::fs::get_fs();
319 match fs.save_file(&f.name, &bytes, "downloaded") {
320 Ok(()) => {
321 self.status = format!("Downloaded '{}' ({} bytes).", f.name, len);
322 self.reload_local();
323 }
324 Err(e) => self.status = format!("Download save error: {}", e),
325 }
326 }
327 Err(e) => self.status = format!("Download error: {}", e),
328 }
329 }
330
331 fn action_upload_local(&mut self) {
332 if !self.auth.is_logged_in() {
333 self.status = String::from("Not logged in.");
334 return;
335 }
336 if self.local.is_empty() {
337 return;
338 }
339 let name = self.local[self.local_sel].clone();
340 let fs = crate::kernel::fs::get_fs();
341 let content = match fs.read_file(&name) {
342 Some((_m, bytes)) => String::from_utf8_lossy(&bytes).into_owned(),
343 None => {
344 self.status = format!("Local file '{}' not found.", name);
345 return;
346 }
347 };
348 self.status = format!("Uploading '{}'...", name);
349 let mut result = cloud::drive_create_text(&self.auth.access_token, &name, &content);
350 if let Err("unauthorized (token expired?)") = result {
351 if self.try_refresh() {
352 result = cloud::drive_create_text(&self.auth.access_token, &name, &content);
353 }
354 }
355 match result {
356 Ok(id) => {
357 self.status = format!("Uploaded '{}' (id={}). Press R to refresh.", name, id);
358 }
359 Err(e) => self.status = format!("Upload error: {}", e),
360 }
361 }
362
363 fn action_delete_remote(&mut self) {
364 if self.remote.is_empty() {
365 return;
366 }
367 let f = self.remote[self.remote_sel].clone();
368 self.status = format!("Deleting '{}'...", f.name);
369 let mut result = cloud::drive_delete(&self.auth.access_token, &f.id);
370 if let Err("unauthorized (token expired?)") = result {
371 if self.try_refresh() {
372 result = cloud::drive_delete(&self.auth.access_token, &f.id);
373 }
374 }
375 match result {
376 Ok(()) => {
377 self.remote.remove(self.remote_sel);
378 if self.remote_sel >= self.remote.len() && self.remote_sel > 0 {
379 self.remote_sel -= 1;
380 }
381 self.status = format!("Deleted '{}'.", f.name);
382 }
383 Err(e) => self.status = format!("Delete error: {}", e),
384 }
385 }
386
387 fn action_logout(&mut self) {
388 self.auth.access_token = String::new();
389 self.auth.refresh_token = String::new();
390 self.remote.clear();
391 self.folder_path.clear();
392 match cloud::save_auth(&self.auth) {
395 Ok(()) => {
396 self.status = String::from("Logged out (client_id kept). Press L to log in.")
397 }
398 Err(e) => {
399 crate::error!("[DRIVE] logout not persisted: {}", e);
400 self.status = alloc::format!(
401 "Logout NOT saved ({}). Token may remain on disk!",
402 e
403 );
404 }
405 }
406 }
407
408 fn action_enter_setup(&mut self) {
411 self.setup_id_box = TextBox::with_text(&self.auth.client_id);
412 self.setup_secret_box = TextBox::with_text(&self.auth.client_secret);
413 self.setup_focus = SetupField::ClientId;
414 self.setup_mode = true;
415 }
416
417 fn action_save_setup(&mut self) {
419 self.auth.client_id = self.setup_id_box.text.clone();
420 self.auth.client_secret = self.setup_secret_box.text.clone();
421 match cloud::save_auth(&self.auth) {
422 Ok(()) => {
423 self.setup_mode = false;
424 self.status = if self.auth.has_client() {
425 String::from("Saved client_id/secret. Press L to log in.")
426 } else {
427 String::from("Saved, but client_id/secret still empty.")
428 };
429 }
430 Err(e) => self.status = format!("Save error: {}", e),
431 }
432 }
433}
434
435const HEADER_H: u32 = 28;
436const ROW_H: u32 = 20;
437
438impl App for DriveApp {
439 fn name(&self) -> &str {
440 "drive"
441 }
442
443 fn instance_info(&self) -> String {
444 if self.auth.is_logged_in() {
445 String::from("Google Drive")
446 } else {
447 String::from("Google Drive (logged out)")
448 }
449 }
450
451 fn draw(&mut self, screen: &Screen, win_x: u32, win_y: u32, win_w: u32, win_h: u32) {
452 self.last_win_w = win_w;
453 let config = crate::kernel::config::get_config();
454 screen.boxfill(
456 win_x,
457 win_y,
458 win_x + win_w,
459 win_y + win_h,
460 Color(config.theme.app_bg),
461 );
462 screen.boxfill(
464 win_x,
465 win_y,
466 win_x + win_w,
467 win_y + HEADER_H,
468 Color(config.theme.window_title_bar),
469 );
470 screen.draw_string_vector(win_x + 12, win_y + 7, "Google Drive", Color(config.theme.window_title_fg), 16);
471 let legend =
472 "L:login P:poll R:refresh Enter:open/enter U:upload B:download D:delete BS:up-folder Tab:pane V:close O:logout S:setup";
473 screen.draw_string_vector(win_x + 170, win_y + 9, legend, Color(config.theme.non_active_window_border), 11);
474
475 let body_y = win_y + HEADER_H + 4;
476
477 if self.setup_mode {
479 screen.draw_string_vector(
480 win_x + 14,
481 body_y + 4,
482 "Google Cloud Console: OAuth client type 'TVs and Limited Input devices',",
483 Color(config.theme.non_active_window_border),
484 12,
485 );
486 screen.draw_string_vector(
487 win_x + 14,
488 body_y + 22,
489 "with Drive API + Docs API enabled and scope https://www.googleapis.com/auth/drive.",
490 Color(config.theme.non_active_window_border),
491 12,
492 );
493 screen.draw_string_vector(
494 win_x + 14,
495 body_y + 52,
496 "Client ID:",
497 Color(config.theme.terminal_fg),
498 13,
499 );
500 self.draw_text_field(
501 screen,
502 win_x + 14,
503 body_y + 70,
504 win_w - 28,
505 &self.setup_id_box,
506 self.setup_focus == SetupField::ClientId,
507 "(paste client_id)",
508 false,
509 );
510 screen.draw_string_vector(
511 win_x + 14,
512 body_y + 108,
513 "Client Secret:",
514 Color(config.theme.terminal_fg),
515 13,
516 );
517 self.draw_text_field(
518 screen,
519 win_x + 14,
520 body_y + 126,
521 win_w - 28,
522 &self.setup_secret_box,
523 self.setup_focus == SetupField::ClientSecret,
524 "(paste client_secret)",
525 true,
526 );
527 screen.draw_string_vector(
528 win_x + 14,
529 body_y + 164,
530 "Tab: switch field Enter: save Esc: cancel",
531 Color(config.theme.non_active_window_border),
532 12,
533 );
534 self.draw_status(screen, win_x, win_y, win_w, win_h);
535 return;
536 }
537
538 if !self.auth.has_client() {
540 let lines = [
541 "Setup required.",
542 "Press S to enter your OAuth client_id/secret directly in this app,",
543 "or save them into 'gdrive_auth.json' yourself:",
544 "1. Google Cloud Console: create an OAuth client of type",
545 " 'TVs and Limited Input devices'. Enable Drive API + Docs API.",
546 "2. Add scope: https://www.googleapis.com/auth/drive",
547 "3. Press S here, paste client_id/secret, press Enter to save.",
548 ];
549 let mut y = body_y + 6;
550 for l in lines.iter() {
551 screen.draw_string_vector(win_x + 14, y, l, Color(config.theme.terminal_fg), 12);
552 y += 20;
553 }
554 self.draw_status(screen, win_x, win_y, win_w, win_h);
555 return;
556 }
557
558 if self.awaiting_auth {
559 screen.draw_string_vector(
560 win_x + 14,
561 body_y + 10,
562 "Authorize this device:",
563 Color(config.theme.terminal_fg),
564 14,
565 );
566 screen.draw_string_vector(
567 win_x + 14,
568 body_y + 40,
569 "1. Open this URL on your phone/PC:",
570 Color(config.theme.non_active_window_border),
571 12,
572 );
573 screen.draw_string_vector(
574 win_x + 30,
575 body_y + 62,
576 &self.device_url,
577 Color(config.theme.active_window_border),
578 13,
579 );
580 screen.draw_string_vector(
581 win_x + 14,
582 body_y + 92,
583 "2. Enter this code:",
584 Color(config.theme.non_active_window_border),
585 12,
586 );
587 screen.draw_string_vector(
588 win_x + 30,
589 body_y + 116,
590 &self.device_user_code,
591 Color(config.theme.focus_highlight),
592 24,
593 );
594 screen.draw_string_vector(
595 win_x + 14,
596 body_y + 152,
597 "3. Press P here to complete login.",
598 Color(config.theme.non_active_window_border),
599 12,
600 );
601 self.draw_status(screen, win_x, win_y, win_w, win_h);
602 return;
603 }
604
605 if self.viewer_open {
607 screen.draw_string_vector(
608 win_x + 14,
609 body_y,
610 &format!("View: {}", self.viewer_title),
611 Color(config.theme.focus_highlight),
612 14,
613 );
614 let mut y = body_y + 24;
615 let max_y = win_y + win_h - 28;
616 for line in self.viewer_text.lines().skip(self.viewer_scroll) {
617 if y + ROW_H > max_y {
618 break;
619 }
620 let shown: String = line.chars().take(90).collect();
622 screen.draw_string_vector(win_x + 14, y, &shown, Color(config.theme.terminal_fg), 12);
623 y += 16;
624 }
625 self.draw_status(screen, win_x, win_y, win_w, win_h);
626 return;
627 }
628
629 let mut crumb = String::from("/");
631 for (_, name) in self.folder_path.iter() {
632 crumb.push_str(name);
633 crumb.push('/');
634 }
635 let crumb_shown: String = crumb.chars().take(80).collect();
636 screen.draw_string_vector(
637 win_x + 14,
638 body_y,
639 &crumb_shown,
640 Color(config.theme.non_active_window_border),
641 11,
642 );
643
644 let mid_x = win_x + win_w / 2;
646 let remote_focus = self.pane == Pane::Remote;
647 let pane_header_y = body_y + 16;
648 screen.draw_string_vector(
649 win_x + 14,
650 pane_header_y,
651 "Drive (remote)",
652 if remote_focus {
653 Color(config.theme.active_window_border)
654 } else {
655 Color(config.theme.non_active_window_border)
656 },
657 13,
658 );
659 screen.draw_string_vector(
660 mid_x + 14,
661 pane_header_y,
662 "Local (this OS)",
663 if !remote_focus {
664 Color(config.theme.active_window_border)
665 } else {
666 Color(config.theme.non_active_window_border)
667 },
668 13,
669 );
670
671 let list_y = pane_header_y + 22;
672 let max_rows = ((win_h - HEADER_H - 76) / ROW_H) as usize;
673
674 for (i, f) in self
676 .remote
677 .iter()
678 .enumerate()
679 .skip(self.scroll)
680 .take(max_rows)
681 {
682 let y = list_y + (i - self.scroll) as u32 * ROW_H;
683 if remote_focus && i == self.remote_sel {
684 screen.boxfill(
685 win_x + 8,
686 y - 2,
687 mid_x - 8,
688 y + ROW_H - 4,
689 Color(config.theme.button_active_bg),
690 );
691 }
692 let is_folder = f.mime_type == cloud::MIME_FOLDER;
693 let is_doc = f.mime_type == cloud::MIME_GOOGLE_DOC;
694 let tag = if is_folder {
695 "[D] "
696 } else if is_doc {
697 "[G] "
698 } else {
699 " "
700 };
701 let label: String = format!("{}{}", tag, f.name);
702 let shown: String = label.chars().take(40).collect();
703 screen.draw_string_vector(win_x + 14, y, &shown, Color(config.theme.terminal_fg), 12);
704 }
705 if self.remote.is_empty() {
706 screen.draw_string_vector(
707 win_x + 14,
708 list_y,
709 "(press R to load)",
710 Color(config.theme.non_active_window_border),
711 12,
712 );
713 }
714
715 for (i, name) in self.local.iter().enumerate().take(max_rows) {
717 let y = list_y + i as u32 * ROW_H;
718 if !remote_focus && i == self.local_sel {
719 screen.boxfill(
720 mid_x + 8,
721 y - 2,
722 win_x + win_w - 8,
723 y + ROW_H - 4,
724 Color(config.theme.button_active_bg),
725 );
726 }
727 let shown: String = name.chars().take(40).collect();
728 screen.draw_string_vector(mid_x + 14, y, &shown, Color(config.theme.terminal_fg), 12);
729 }
730
731 self.draw_status(screen, win_x, win_y, win_w, win_h);
732 }
733
734 fn on_key(&mut self, keycode: u8, pressed: bool, ascii: Option<char>) {
735 if !pressed {
736 return;
737 }
738 if self.setup_mode {
741 match keycode {
742 0x0F => {
743 self.setup_focus = if self.setup_focus == SetupField::ClientId {
744 SetupField::ClientSecret
745 } else {
746 SetupField::ClientId
747 };
748 }
749 0x1C => self.action_save_setup(),
750 0x01 => {
751 self.setup_mode = false;
752 self.status = String::from("Setup cancelled.");
753 }
754 _ => {
755 let tb = match self.setup_focus {
756 SetupField::ClientId => &mut self.setup_id_box,
757 SetupField::ClientSecret => &mut self.setup_secret_box,
758 };
759 let _ = tb.handle_key(keycode, ascii);
760 }
761 }
762 return;
763 }
764 let c = match ascii {
765 Some(c) => c.to_ascii_lowercase(),
766 None => return,
767 };
768 if self.viewer_open {
770 match c {
771 'v' | 'q' => self.viewer_open = false,
772 'j' => self.viewer_scroll = self.viewer_scroll.saturating_add(1),
773 'k' => self.viewer_scroll = self.viewer_scroll.saturating_sub(1),
774 _ => {}
775 }
776 return;
777 }
778 match c {
779 'l' => self.action_login(),
780 'p' => self.action_poll(),
781 'r' => self.action_refresh_remote(),
782 'u' => self.action_upload_local(),
783 'd' => self.action_delete_remote(),
784 'o' => self.action_logout(),
785 's' => self.action_enter_setup(),
786 'b' if self.pane == Pane::Remote => self.action_download_remote(),
787 '\x08' if self.pane == Pane::Remote => self.action_leave_folder(),
788 '\t' => {
789 self.pane = if self.pane == Pane::Remote {
790 Pane::Local
791 } else {
792 Pane::Remote
793 }
794 }
795 '\n' | '\r' => {
796 if self.pane == Pane::Remote {
797 self.action_open_remote();
798 } else {
799 self.action_upload_local();
800 }
801 }
802 'j' => self.move_sel(1),
803 'k' => self.move_sel(-1),
804 _ => {}
805 }
806 }
807
808 fn on_mouse(
809 &mut self,
810 local_x: i32,
811 local_y: i32,
812 btn_left: bool,
813 _btn_right: bool,
814 wheel: i32,
815 ) {
816 if wheel != 0 && self.viewer_open {
817 if wheel < 0 {
818 self.viewer_scroll = self.viewer_scroll.saturating_add(1);
819 } else {
820 self.viewer_scroll = self.viewer_scroll.saturating_sub(1);
821 }
822 return;
823 }
824 if !btn_left {
825 return;
826 }
827 let list_top = (HEADER_H + 26) as i32;
829 if local_y < list_top {
830 return;
831 }
832 let row = ((local_y - list_top) / ROW_H as i32).max(0) as usize;
833 let mid = (self.last_win_w / 2) as i32;
836 if local_x < mid {
837 self.pane = Pane::Remote;
838 if row + self.scroll < self.remote.len() {
839 self.remote_sel = row + self.scroll;
840 }
841 } else {
842 self.pane = Pane::Local;
843 if row < self.local.len() {
844 self.local_sel = row;
845 }
846 }
847 }
848}
849
850impl DriveApp {
851 fn move_sel(&mut self, delta: i32) {
852 if self.pane == Pane::Remote {
853 let n = self.remote.len();
854 if n == 0 {
855 return;
856 }
857 let cur = self.remote_sel as i32 + delta;
858 self.remote_sel = cur.clamp(0, n as i32 - 1) as usize;
859 } else {
860 let n = self.local.len();
861 if n == 0 {
862 return;
863 }
864 let cur = self.local_sel as i32 + delta;
865 self.local_sel = cur.clamp(0, n as i32 - 1) as usize;
866 }
867 }
868
869 #[allow(clippy::too_many_arguments)]
870 fn draw_text_field(
871 &self,
872 screen: &Screen,
873 x: u32,
874 y: u32,
875 w: u32,
876 tb: &TextBox,
877 focused: bool,
878 placeholder: &str,
879 mask: bool,
880 ) {
881 let t = &crate::kernel::config::get_config().theme;
882 let border = if focused { t.active_window_border } else { t.browser_secondary };
883 let bg = if focused { t.browser_bg } else { t.ui_input_bg };
884 screen.boxfill(x, y, x + w, y + 28, Color(bg));
885 screen.boxfill(x, y, x + w, y + 1, Color(border));
886 screen.boxfill(x, y + 27, x + w, y + 28, Color(border));
887 screen.boxfill(x, y, x + 1, y + 28, Color(border));
888 screen.boxfill(x + w - 1, y, x + w, y + 28, Color(border));
889
890 let display: String = if tb.text.is_empty() && !focused {
891 String::from(placeholder)
892 } else if focused {
893 tb.get_text_with_cursor(true)
894 .chars()
895 .map(|c| if mask && c != '|' { '*' } else { c })
896 .collect()
897 } else if mask {
898 tb.text.chars().map(|_| '*').collect()
899 } else {
900 tb.text.clone()
901 };
902
903 let text_color = if tb.text.is_empty() && !focused {
904 t.fn_bar_dim_fg
905 } else {
906 t.login_text_fg
907 };
908 let shown: String = display.chars().take(60).collect();
909 screen.draw_string_vector(x + 8, y + 7, &shown, Color(text_color), 13);
910 }
911
912 fn draw_status(&self, screen: &Screen, win_x: u32, win_y: u32, win_w: u32, win_h: u32) {
913 let config = crate::kernel::config::get_config();
914 let bar_y = win_y + win_h - 24;
915 screen.boxfill(
916 win_x,
917 bar_y,
918 win_x + win_w,
919 win_y + win_h,
920 Color(config.theme.window_title_bar),
921 );
922 let shown: String = self.status.chars().take(110).collect();
923 screen.draw_string_vector(win_x + 12, bar_y + 5, &shown, Color(config.theme.window_title_fg), 12);
924 }
925}