Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fbda37a5 authored by Sonny Sasaka's avatar Sonny Sasaka Committed by Automerger Merge Worker
Browse files

Merge "Floss: Implement command completer in btclient" am: e3063627

parents 1262f860 e3063627
Loading
Loading
Loading
Loading
+11 −4
Original line number Diff line number Diff line
@@ -134,6 +134,8 @@ impl IBluetoothCallback for BtCallback {
            Some(_) => print_info!("Removed device: {:?}", remote_device),
            None => (),
        };

        self.context.lock().unwrap().bonded_devices.remove(&remote_device.address);
    }

    fn on_discovering_changed(&self, discovering: bool) {
@@ -209,12 +211,17 @@ impl IBluetoothCallback for BtCallback {
            BtBondState::Bonding => (),
        }

        let device =
            BluetoothDevice { address: address.clone(), name: String::from("Classic device") };

        // If bonded, we should also automatically connect all enabled profiles
        if BtBondState::Bonded == state.into() {
            self.context.lock().unwrap().connect_all_enabled_profiles(BluetoothDevice {
                address,
                name: String::from("Classic device"),
            });
            self.context.lock().unwrap().bonded_devices.insert(address.clone(), device.clone());
            self.context.lock().unwrap().connect_all_enabled_profiles(device.clone());
        }

        if BtBondState::NotBonded == state.into() {
            self.context.lock().unwrap().bonded_devices.remove(&address);
        }
    }
}
+23 −3
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ fn _noop(_handler: &mut CommandHandler, _args: &Vec<String>) {
}

pub struct CommandOption {
    rules: Vec<String>,
    description: String,
    function_pointer: CommandFunction,
}
@@ -84,6 +85,7 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("adapter"),
        CommandOption {
            rules: vec![String::from("adapter <enable|disable|show>")],
            description: String::from(
                "Enable/Disable/Show default bluetooth adapter. (e.g. adapter enable)\n
                 Discoverable On/Off (e.g. adapter discoverable on)",
@@ -94,6 +96,7 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("bond"),
        CommandOption {
            rules: vec![String::from("bond <add|remove|cancel> <address>")],
            description: String::from("Creates a bond with a device."),
            function_pointer: CommandHandler::cmd_bond,
        },
@@ -101,6 +104,7 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("device"),
        CommandOption {
            rules: vec![String::from("device <connect|disconnect|info|set-alias> <address>")],
            description: String::from("Take action on a remote device. (i.e. info)"),
            function_pointer: CommandHandler::cmd_device,
        },
@@ -108,6 +112,7 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("discovery"),
        CommandOption {
            rules: vec![String::from("discovery <start|stop>")],
            description: String::from("Start and stop device discovery. (e.g. discovery start)"),
            function_pointer: CommandHandler::cmd_discovery,
        },
@@ -115,6 +120,7 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("floss"),
        CommandOption {
            rules: vec![String::from("floss <enable|disable>")],
            description: String::from("Enable or disable Floss for dogfood."),
            function_pointer: CommandHandler::cmd_floss,
        },
@@ -122,6 +128,12 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("gatt"),
        CommandOption {
            rules: vec![
                String::from("gatt register-client"),
                String::from("gatt client-connect <address>"),
                String::from("gatt client-read-phy <address>"),
                String::from("gatt client-discover-services <address>"),
            ],
            description: String::from("GATT tools"),
            function_pointer: CommandHandler::cmd_gatt,
        },
@@ -129,6 +141,10 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("le-scan"),
        CommandOption {
            rules: vec![
                String::from("le-scan register-scanner"),
                String::from("le-scan unregister-scanner <scanner-id>"),
            ],
            description: String::from("LE scanning utilities."),
            function_pointer: CommandHandler::cmd_le_scan,
        },
@@ -136,6 +152,7 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("get-address"),
        CommandOption {
            rules: vec![String::from("get-address")],
            description: String::from("Gets the local device address."),
            function_pointer: CommandHandler::cmd_get_address,
        },
@@ -143,6 +160,7 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("help"),
        CommandOption {
            rules: vec![String::from("help")],
            description: String::from("Shows this menu."),
            function_pointer: CommandHandler::cmd_help,
        },
@@ -150,6 +168,7 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("list"),
        CommandOption {
            rules: vec![String::from("list <bonded|found>")],
            description: String::from(
                "List bonded or found remote devices. Use: list <bonded|found>",
            ),
@@ -159,6 +178,7 @@ fn build_commands() -> HashMap<String, CommandOption> {
    command_options.insert(
        String::from("quit"),
        CommandOption {
            rules: vec![String::from("quit")],
            description: String::from("Quit out of the interactive shell."),
            function_pointer: _noop,
        },
@@ -713,9 +733,9 @@ impl CommandHandler {
        });
    }

    /// Get the list of currently supported commands
    pub fn get_command_list(&self) -> Vec<String> {
        self.command_options.keys().map(|key| String::from(key)).collect::<Vec<String>>()
    /// Get the list of rules of supported commands
    pub fn get_command_rule_list(&self) -> Vec<String> {
        self.command_options.values().flat_map(|cmd| cmd.rules.clone()).collect()
    }

    fn cmd_list_devices(&mut self, args: &Vec<String>) {
+94 −12
Original line number Diff line number Diff line
@@ -10,22 +10,44 @@ use rustyline::validate::Validator;
use rustyline::{CompletionType, Config, Editor};
use rustyline_derive::Helper;

use std::collections::HashSet;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};

use crate::console_blue;
use crate::ClientContext;

#[derive(Helper)]
struct BtHelper {
    commands: Vec<String>,
    // Command rules must follow below format:
    // cmd arg1 arg2 arg3 ...
    // where each argument could have multiple options separated by a single '|'
    //
    // It is not required to put an argument in angle brackets.
    //
    // "address" in options is a keyword, which will be matched by any of the founded
    // and bonded devices.
    //
    // Example:
    // list <found|bonded> <address>
    // This will match
    //     list found any-cached-address
    // and
    //     list bond any-cached-address
    command_rules: Vec<String>,
    client_context: Arc<Mutex<ClientContext>>,
}

#[derive(Hash, Eq, PartialEq)]
struct CommandCandidate {
    suggest_word: String,
    matched_len: usize,
}

impl Completer for BtHelper {
    type Candidate = String;

    // Returns completion based on supported commands.
    // TODO: Add support to autocomplete BT address, command parameters, etc.
    fn complete(
        &self,
        line: &str,
@@ -33,13 +55,22 @@ impl Completer for BtHelper {
        _ctx: &rustyline::Context<'_>,
    ) -> Result<(usize, Vec<String>), ReadlineError> {
        let slice = &line[..pos];
        let mut completions = vec![];

        for cmd in self.commands.iter() {
            if cmd.starts_with(slice) {
                completions.push(cmd.clone());
            }
        let candidates = self.get_candidates(slice.to_string().clone());
        let mut completions = candidates
            .iter()
            .map(|c| {
                if candidates.len() == 1 {
                    // If only one candidate, Completer will replace the input by
                    // the returned string. Return the complete string here to avoid
                    // input being replaced by the suggested word.
                    slice.to_string() + &c.suggest_word[c.matched_len..] + " "
                } else {
                    c.suggest_word.clone()
                }
            })
            .collect::<Vec<String>>();

        completions.sort();

        Ok((0, completions))
    }
@@ -53,6 +84,54 @@ impl Highlighter for BtHelper {}

impl Validator for BtHelper {}

impl BtHelper {
    fn get_candidates(&self, cmd: String) -> HashSet<CommandCandidate> {
        let mut result = HashSet::<CommandCandidate>::new();

        for rule in self.command_rules.iter() {
            for (i, (rule_token, cmd_token)) in rule.split(" ").zip(cmd.split(" ")).enumerate() {
                let mut candidates = Vec::<String>::new();
                let mut match_some = false;
                let n_cmd = cmd.split(" ").count();

                for opt in rule_token.replace("<", "").replace(">", "").split("|") {
                    if opt.eq("address") {
                        let devices = self.client_context.lock().unwrap().get_devices();
                        candidates.extend(devices);
                    } else {
                        candidates.push(opt.to_string());
                    }
                }

                if cmd_token.len() == 0 {
                    candidates.iter().for_each(|s| {
                        result.insert(CommandCandidate { suggest_word: s.clone(), matched_len: 0 });
                    });
                    break;
                }

                for opt in candidates {
                    if opt.starts_with(cmd_token) {
                        match_some = true;
                        if i == n_cmd - 1 {
                            // we add candidates only if it's the last word
                            result.insert(CommandCandidate {
                                suggest_word: opt.clone(),
                                matched_len: cmd_token.len(),
                            });
                        }
                    }
                }

                if !match_some {
                    break;
                }
            }
        }
        result
    }
}

/// A future that does async readline().
///
/// async readline() is implemented by spawning a thread for the blocking readline(). While this
@@ -94,14 +173,17 @@ impl AsyncEditor {
    /// Creates new async rustyline editor.
    ///
    /// * `commands` - List of commands for autocomplete.
    pub fn new(commands: Vec<String>) -> AsyncEditor {
    pub(crate) fn new(
        command_rules: Vec<String>,
        client_context: Arc<Mutex<ClientContext>>,
    ) -> AsyncEditor {
        let builder = Config::builder()
            .auto_add_history(true)
            .history_ignore_dups(true)
            .completion_type(CompletionType::List);
        let config = builder.build();
        let mut rl = rustyline::Editor::with_config(config);
        let helper = BtHelper { commands };
        let helper = BtHelper { command_rules, client_context };
        rl.set_helper(Some(helper));
        AsyncEditor { rl: Arc::new(Mutex::new(rl)) }
    }
+35 −2
Original line number Diff line number Diff line
@@ -55,6 +55,9 @@ pub(crate) struct ClientContext {
    /// session starts so that previous results don't pollute current search.
    pub(crate) found_devices: HashMap<String, BluetoothDevice>,

    /// List of bonded devices.
    pub(crate) bonded_devices: HashMap<String, BluetoothDevice>,

    /// If set, the registered GATT client id. None otherwise.
    pub(crate) gatt_client_id: Option<i32>,

@@ -102,6 +105,7 @@ impl ClientContext {
            bonding_attempt: None,
            discovering_state: false,
            found_devices: HashMap::new(),
            bonded_devices: HashMap::new(),
            gatt_client_id: None,
            manager_dbus,
            adapter_dbus: None,
@@ -163,6 +167,15 @@ impl ClientContext {
        address
    }

    // Foreground-only: Updates bonded devices.
    fn update_bonded_devices(&mut self) {
        let bonded_devices = self.adapter_dbus.as_ref().unwrap().get_bonded_devices();

        for device in bonded_devices {
            self.bonded_devices.insert(device.address.clone(), device.clone());
        }
    }

    fn connect_all_enabled_profiles(&mut self, device: BluetoothDevice) {
        let fg = self.fg.clone();
        tokio::spawn(async move {
@@ -176,6 +189,23 @@ impl ClientContext {
            let _ = fg.send(ForegroundActions::RunCallback(callback)).await;
        });
    }

    fn get_devices(&self) -> Vec<String> {
        let mut result: Vec<String> = vec![];

        result.extend(
            self.found_devices.keys().map(|key| String::from(key)).collect::<Vec<String>>(),
        );
        result.extend(
            self.bonded_devices
                .keys()
                .filter(|key| !self.found_devices.contains_key(&String::from(*key)))
                .map(|key| String::from(key))
                .collect::<Vec<String>>(),
        );

        result
    }
}

/// Actions to take on the foreground loop. This allows us to queue actions in
@@ -282,14 +312,15 @@ async fn start_interactive_shell(
    mut rx: mpsc::Receiver<ForegroundActions>,
    context: Arc<Mutex<ClientContext>>,
) {
    let command_list = handler.get_command_list().clone();
    let command_rule_list = handler.get_command_rule_list().clone();
    let context_for_closure = context.clone();

    let semaphore_fg = Arc::new(tokio::sync::Semaphore::new(1));

    // Async task to keep reading new lines from user
    let semaphore = semaphore_fg.clone();
    tokio::spawn(async move {
        let editor = AsyncEditor::new(command_list);
        let editor = AsyncEditor::new(command_rule_list, context_for_closure);

        loop {
            // Wait until ForegroundAction::Readline finishes its task.
@@ -403,6 +434,8 @@ async fn start_interactive_shell(

                context.lock().unwrap().adapter_ready = true;
                let adapter_address = context.lock().unwrap().update_adapter_address();
                context.lock().unwrap().update_bonded_devices();

                print_info!("Adapter {} is ready", adapter_address);
            }
            ForegroundActions::Readline(result) => match result {