jocalsend 1.6.180339887

A TUI for LocalSend
Documentation
use crossterm::event::{Event, KeyCode, KeyEvent};
use jocalsend::ReceiveDialog;
use log::{debug, error, warn};
use tui_input::backend::crossterm::EventHandler;

use crate::app::{App, CurrentScreen, FileMode, SendingScreen};

impl App {
    pub(super) async fn handle_key_event(
        &mut self,
        key_event: KeyEvent,
        event: crossterm::event::Event,
    ) {
        let code = key_event.code;
        let mode = self.screen();
        match mode {
            CurrentScreen::Main
            | CurrentScreen::Help
            | CurrentScreen::Logging
            | CurrentScreen::Receiving => match code {
                KeyCode::Char('q') => self.exit().await,
                KeyCode::Char('s') => self.send(),
                KeyCode::Char('r') => self.recv(),
                KeyCode::Char('l') => self.logs(),
                KeyCode::Char('m') => self.main(),
                KeyCode::Char('h') | KeyCode::Char('?') => self.help(),
                KeyCode::Char('c') => self.service.clear_peers().await,
                KeyCode::Esc => self.pop(),
                _ => match mode {
                    CurrentScreen::Main | CurrentScreen::Help => {}
                    CurrentScreen::Logging => match code {
                        KeyCode::Left => change_log_level(LogDelta::Down),
                        KeyCode::Right => change_log_level(LogDelta::Up),
                        _ => {}
                    },
                    CurrentScreen::Receiving => match code {
                        KeyCode::Up => self.receiving_state.select_previous(),
                        KeyCode::Down => self.receiving_state.select_next(),
                        KeyCode::Char('a') => self.accept(),
                        KeyCode::Char('d') => self.deny(),
                        _ => {}
                    },
                    CurrentScreen::Stopping | CurrentScreen::Sending(_) => unreachable!(),
                },
            },
            CurrentScreen::Sending(_) => self.sending_screen(key_event, event).await,
            CurrentScreen::Stopping => {}
        }
    }

    async fn sending_screen(&mut self, key_event: KeyEvent, event: Event) {
        let mode = self.screen();
        let CurrentScreen::Sending(mode) = mode else {
            return;
        };
        let code = key_event.code;
        match mode {
            SendingScreen::Peers => match code {
                KeyCode::Char('q') => self.exit().await,
                KeyCode::Char('s') => self.send(),
                KeyCode::Char('r') => self.recv(),
                KeyCode::Char('l') => self.logs(),
                KeyCode::Char('m') => self.main(),
                KeyCode::Char('h') | KeyCode::Char('?') => self.help(),
                KeyCode::Char('c') => self.service.clear_peers().await,
                KeyCode::Char('t') => self.sending_text(),
                KeyCode::Tab => self.sending_files(),
                KeyCode::Enter => self.send_content().await,
                KeyCode::Esc => self.pop(),
                _ => {}
            },
            SendingScreen::Files(FileMode::Picking) => match code {
                KeyCode::Char('q') => self.exit().await,
                KeyCode::Char('s') => self.send(),
                KeyCode::Char('r') => self.recv(),
                KeyCode::Char('l') => self.logs(),
                KeyCode::Char('m') => self.main(),
                KeyCode::Char('h') | KeyCode::Char('?') => self.help(),
                KeyCode::Char('c') => self.service.clear_peers().await,
                KeyCode::Char('t') => self.sending_text(),
                KeyCode::Tab => self.sending_peers(),
                KeyCode::Enter => self.chdir_or_send_file().await,
                KeyCode::Esc => self.pop(),
                KeyCode::Char('/') => self.sending_fuzzy(),
                _ => self.file_finder.handle(&event).unwrap_or_default(),
            },
            SendingScreen::Files(FileMode::Fuzzy) => match code {
                KeyCode::Tab => self.sending_peers(),
                KeyCode::Enter => self.chdir_or_send_file().await,
                KeyCode::Esc => {
                    self.file_finder.reset_fuzzy();
                    self.sending_files();
                }
                KeyCode::Up | KeyCode::Down => {
                    if let Err(e) = self.file_finder.handle(&event) {
                        log::error!("error selecting file: {e:?}");
                    }
                }
                _ => {
                    self.file_finder.index();
                    if let Some(changed) = self.file_finder.input.handle_event(&event)
                        && changed.value
                    {
                        let id = self
                            .file_finder
                            .fuzzy
                            .search(self.file_finder.input.value())
                            .first()
                            .copied()
                            .unwrap_or(0);
                        self.file_finder.explorer.set_selected_idx(id);
                    }
                }
            },
            SendingScreen::Text => match code {
                KeyCode::Tab => self.sending_peers(),
                KeyCode::Enter => self.send_text().await,
                KeyCode::Esc => {
                    self.text = None;
                    self.input.reset();
                    self.sending_files();
                }
                _ => {
                    if let Some(changed) = self.input.handle_event(&event)
                        && changed.value
                    {
                        if self.input.value().is_empty() {
                            self.text = None;
                        } else {
                            self.text = Some(self.input.to_string());
                        }
                    }
                }
            },
        }
    }

    pub async fn exit(&mut self) {
        self.screen.push(CurrentScreen::Stopping);
        self.service.stop().await;
    }

    pub fn send(&mut self) {
        let last = self.screen.last();
        match last {
            Some(CurrentScreen::Sending(_)) => {}
            _ => self
                .screen
                .push(CurrentScreen::Sending(SendingScreen::Files(
                    FileMode::Picking,
                ))),
        }
    }

    pub fn recv(&mut self) {
        let last = self.screen.last();
        match last {
            Some(CurrentScreen::Receiving) => {}
            _ => self.screen.push(CurrentScreen::Receiving),
        }
    }

    pub fn logs(&mut self) {
        let last = self.screen.last();
        match last {
            Some(CurrentScreen::Logging) => {}
            _ => self.screen.push(CurrentScreen::Logging),
        }
    }

    pub fn pop(&mut self) {
        self.screen.pop();
        if self.screen.last().is_none() {
            self.screen.push(CurrentScreen::Main);
        }
    }

    pub fn main(&mut self) {
        let last = self.screen.last();
        match last {
            Some(CurrentScreen::Main) => {}
            _ => self.screen.push(CurrentScreen::Main),
        }
    }

    pub fn help(&mut self) {
        let last = self.screen.last();
        match last {
            Some(CurrentScreen::Help) => {}
            _ => self.screen.push(CurrentScreen::Help),
        }
    }

    fn sending_peers(&mut self) {
        if let CurrentScreen::Sending(mode) = self.screen_mut() {
            *mode = SendingScreen::Peers;
        }
    }

    fn sending_text(&mut self) {
        if let CurrentScreen::Sending(mode) = self.screen_mut() {
            *mode = SendingScreen::Text;
        }
    }

    fn sending_fuzzy(&mut self) {
        if let CurrentScreen::Sending(mode) = self.screen_mut() {
            *mode = SendingScreen::Files(FileMode::Fuzzy);
        }
    }

    fn sending_files(&mut self) {
        let doing_files = self.text.is_none();
        let doing_picking = self.file_finder.input.value().is_empty();
        let screen = self.screen_mut();
        if let CurrentScreen::Sending(mode) = screen {
            if doing_files {
                if doing_picking {
                    *mode = SendingScreen::Files(FileMode::Picking);
                } else {
                    *mode = SendingScreen::Files(FileMode::Fuzzy);
                }
            } else {
                *mode = SendingScreen::Text;
            }
        }
    }

    // accept a content receive request
    fn accept(&mut self) {
        let Some(idx) = self.receiving_state.selected() else {
            return;
        };
        // keys are sorted, so we can use the table selection index
        let keys: Vec<_> = self.receive_requests.keys().collect();
        let Some(key) = keys.get(idx) else {
            warn!("could not get id from selection index {idx}");
            return;
        };
        let Some(req) = self.receive_requests.get(key) else {
            return;
        };
        if let Err(e) = req.tx.send(ReceiveDialog::Approve) {
            error!("got error sending upload confirmation: {e:?}");
        };
    }

    // reject an content receive request
    fn deny(&mut self) {
        let Some(idx) = self.receiving_state.selected() else {
            return;
        };
        // keys are sorted, so we can use the table selection index
        let keys: Vec<_> = self.receive_requests.keys().cloned().collect();
        let Some(key) = keys.get(idx) else {
            warn!("could not get id from selection index {idx}");
            return;
        };
        let Some(req) = self.receive_requests.get(key).cloned() else {
            return;
        };
        if let Err(e) = req.tx.send(ReceiveDialog::Deny) {
            error!("got error sending upload confirmation: {e:?}");
        };
        self.receive_requests.remove(key);
    }

    async fn chdir_or_send_file(&mut self) {
        let file = self.file_finder.explorer.current().path().clone();
        if file.is_dir()
            && let Err(e) = self.file_finder.set_cwd(&file)
        {
            error!("could not list directory {file:?}: {e}");
            return;
        } else if file.is_dir() {
            self.file_finder.input.reset();
            return;
        }

        let Some(peer_idx) = self.peer_state.selected() else {
            warn!("no peer selected to send to");
            return;
        };
        let Some(peer) = self.peers.get(peer_idx) else {
            warn!("invalid peer index {peer_idx}");
            return;
        };

        if file.is_file() {
            debug!("sending {file:?}");
            if let Err(e) = self.service.send_file(&peer.fingerprint, file).await {
                error!("got error sending content: {e:?}");
            }
        }
    }

    // send content to selected peer, or change directories in the file explorer
    async fn send_text(&mut self) {
        debug!("sending text");

        let Some(peer_idx) = self.peer_state.selected() else {
            debug!("no peer selected to send to");
            return;
        };
        let Some(peer) = self.peers.get(peer_idx) else {
            warn!("invalid peer index {peer_idx}");
            return;
        };

        let Some(text) = &self.text else {
            debug!("no text to send");
            return;
        };

        if let Err(e) = self.service.send_text(&peer.fingerprint, text).await {
            error!("got error sending \"{text}\" to {}: {e:?}", peer.alias);
        }
    }

    async fn send_content(&mut self) {
        if self.text.is_some() {
            self.send_text().await;
        } else {
            self.chdir_or_send_file().await;
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum LogDelta {
    Up,
    Down,
}

fn change_log_level(delta: LogDelta) {
    let level = match delta {
        LogDelta::Up => log::max_level().increment_severity(),
        LogDelta::Down => log::max_level().decrement_severity(),
    };
    log::set_max_level(level);
}