Skip to content

WIP: add omnibox #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -16,9 +16,11 @@ categories = ["api-bindings", "wasm"]
crate-type = ["cdylib", "rlib"]

[dependencies]
derive_more = "0.99"
gloo-console = "0.2"
gloo-utils = "0.1.5"
js-sys = "0.3.60"
serde = { version = "1.0.147", features = ["derive"] }
serde = {version = "1.0.147", features = ["derive"]}
serde_derive = "1.0.147"
serde_json = "1.0.87"
thiserror = "1.0.37"
@@ -32,3 +34,6 @@ wasm-bindgen-test = "0.3.33"
[features]
default = []
firefox = []

[workspace]
members = ["examples/omnibox/new-tab-search"]
37 changes: 37 additions & 0 deletions examples/omnibox/new-tab-search/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
authors = ["Flier Lu <[email protected]>"]
edition = "2018"
name = "new-tab-search"
version = "0.1.0"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = ["console_error_panic_hook"]

[dependencies]
gloo-console = "0.2"
gloo-utils = "0.1"
js-sys = "0.3"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = {version = "0.1.6", optional = true}

# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
wee_alloc = {version = "0.4", optional = true}

web-extensions = {version = "0.3", path = "../../.."}

[profile.release]
# Tell `rustc` to optimize for small code size.
codegen-units = 1
lto = true
opt-level = "s"
13 changes: 13 additions & 0 deletions examples/omnibox/new-tab-search/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import init, { start } from './pkg/new_tab_search.js';

console.log("WASM module loaded")

async function run() {
await init();

console.log("WASM module initialized")

start();
}

run();
46 changes: 46 additions & 0 deletions examples/omnibox/new-tab-search/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "Omnibox - New Tab Search",
"description": "Type 'nt' plus a search term into the Omnibox to open search in new tab.",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": [
"activeTab",
"tabs",
"scripting"
],
"host_permissions": [
"https://*/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"web_accessible_resources": [
{
"resources": [
"pkg/new_tab_search_bg.wasm"
],
"matches": [
"https://*/*"
]
}
],
"omnibox": {
"keyword": "nt"
},
"action": {
"default_icon": {
"16": "newtab_search16.png",
"32": "newtab_search32.png"
}
},
"icons": {
"16": "newtab_search16.png",
"32": "newtab_search32.png",
"48": "newtab_search48.png",
"128": "newtab_search128.png"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/omnibox/new-tab-search/newtab_search16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/omnibox/new-tab-search/newtab_search32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 changes: 128 additions & 0 deletions examples/omnibox/new-tab-search/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use gloo_console as console;
use js_sys as js;
use wasm_bindgen::prelude::*;

use web_extensions::{self as ext, omnibox::OnInputEnteredDisposition::*};

mod utils;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
pub fn start() {
utils::set_panic_hook();

console::info!("Starting background script");

ext::omnibox::set_default_suggestion(&ext::omnibox::DefaultSuggestResult {
description: "Type anything to search",
})
.unwrap();

ext::omnibox::on_input_started()
.add_listener(|| {
console::debug!("Input started");
})
.forget();

ext::omnibox::on_input_cancelled()
.add_listener(|| {
console::debug!("Input cancelled");
})
.forget();

ext::omnibox::on_input_changed()
.add_listener(|text, suggest| {
console::debug!("Input changed", text);
})
.forget();

ext::omnibox::on_input_entered()
.add_listener(|text, disposition| {
console::debug!("Input entered", text, disposition.to_string());

let url = format!(
"https://www.google.com/search?q={}",
js::encode_uri_component(text).to_string(),
);

wasm_bindgen_futures::spawn_local(async move {
let mut tab_id = None;

if disposition == CurrentTab {
let query = ext::tabs::QueryDetails {
active: Some(true),
last_focused_window: Some(true),
..Default::default()
};

match ext::tabs::query(&query).await {
Ok(tabs) => {
if let [tab, ..] = &tabs[..] {
console::debug!(
"current tab",
tab.id.map_or(-1, Into::<i32>::into)
);

tab_id = tab.id;
}
}
Err(err) => {
console::error!("query tabs failed", err.to_string());
}
}
}

if disposition == CurrentTab {
console::info!(
"open on the current tab",
&url,
tab_id.map_or(-1, Into::<i32>::into)
);

match ext::tabs::update(
tab_id,
ext::tabs::UpdateProperties {
url: Some(&url),
..Default::default()
},
)
.await
{
Ok(tab) => {
console::info!(
"opened on the current tab",
&url,
tab.id.map_or(-1, Into::<i32>::into)
)
}
Err(err) => console::error!("update tabs failed", err.to_string()),
}
} else {
console::info!("open on a new tab", &url);

match ext::tabs::create(ext::tabs::CreateProperties {
active: disposition == NewForegroundTab,
url: &url,
})
.await
{
Ok(tab) => {
console::info!(
"open on a new tab",
tab.id.map_or(-1, Into::<i32>::into)
)
}
Err(err) => {
console::error!("create tab failed", err.to_string());
}
};
}
})
})
.forget();
}
10 changes: 10 additions & 0 deletions examples/omnibox/new-tab-search/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ pub use crate::error::*;
pub mod bookmarks;
pub mod downloads;
pub mod history;
pub mod omnibox;
pub mod tabs;

#[cfg(feature = "firefox")]
278 changes: 278 additions & 0 deletions src/omnibox.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
//! Wrapper for the [`chrome.omnibox` API](https://developer.chrome.com/docs/extensions/reference/omnibox/).
use derive_more::Display;
use gloo_console as console;
use js_sys::Function;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use web_extensions_sys as sys;

use crate::{event_listener::EventListener, util::*, Error};

/// Sets the description and styling for the default suggestion.
///
/// The default suggestion is the text that is displayed in the first suggestion row underneath the URL bar.
///
/// <https://developer.chrome.com/docs/extensions/reference/omnibox/#method-setDefaultSuggestion>
pub fn set_default_suggestion(suggestion: &DefaultSuggestResult<'_>) -> Result<(), Error> {
let js_suggestion = js_from_serde(suggestion)?;

sys::chrome()
.omnibox()
.set_default_suggestion(&js_suggestion);

Ok(())
}

/// A suggest result.
///
/// <https://developer.chrome.com/docs/extensions/reference/history/#type-search-query>
#[derive(Debug, Clone, Serialize)]
pub struct DefaultSuggestResult<'a> {
/// The text that is displayed in the URL dropdown.
pub description: &'a str,
}

/// The style type.
///
/// <https://developer.chrome.com/docs/extensions/reference/history/#type-search-query>
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum DescriptionStyleType {
Url,
Match,
Dim,
}

impl DescriptionStyleType {
pub const DIM: &str = "dim";
pub const MATCH: &str = "match";
pub const URL: &str = "url";
}

impl TryFrom<JsValue> for DescriptionStyleType {
type Error = Error;

fn try_from(v: JsValue) -> Result<Self, Self::Error> {
v.as_string()
.and_then(|s| match s.as_str() {
Self::DIM => Some(DescriptionStyleType::Dim),
Self::MATCH => Some(DescriptionStyleType::Match),
Self::URL => Some(DescriptionStyleType::Url),
_ => None,
})
.ok_or(Error::Js(v))
}
}
/// The window disposition for the omnibox query.
///
/// This is the recommended context to display results.
/// For example, if the omnibox command is to navigate to a certain URL,
/// a disposition of 'newForegroundTab' means the navigation should take place in a new selected tab.
///
/// <https://developer.chrome.com/docs/extensions/reference/omnibox/#type-OnInputEnteredDisposition>
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum OnInputEnteredDisposition {
CurrentTab,
NewForegroundTab,
NewBackgroundTab,
}

impl OnInputEnteredDisposition {
pub const CURRENT_TAB: &str = "currentTab";
pub const NEW_BACKGROUND_TAB: &str = "newBackgroundTab";
pub const NEW_FOREGROUND_TAB: &str = "newForegroundTab";
}

impl TryFrom<JsValue> for OnInputEnteredDisposition {
type Error = Error;

fn try_from(v: JsValue) -> Result<Self, Self::Error> {
v.as_string()
.and_then(|s| match s.as_str() {
Self::CURRENT_TAB => Some(OnInputEnteredDisposition::CurrentTab),
Self::NEW_BACKGROUND_TAB => Some(OnInputEnteredDisposition::NewBackgroundTab),
Self::NEW_FOREGROUND_TAB => Some(OnInputEnteredDisposition::NewForegroundTab),
_ => None,
})
.ok_or(Error::Js(v))
}
}

/// A suggest result.
#[derive(Default, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SuggestResult<'a> {
/// The text that is put into the URL bar, and that is sent to the extension when the user chooses this entry.
pub content: &'a str,
/// Whether the suggest result can be deleted by the user.
pub deletable: bool,
/// The text that is displayed in the URL dropdown.
pub description: &'a str,
}

/// User has deleted a suggested result.
pub fn on_delete_suggestion() -> OnDeleteSuggestion {
OnDeleteSuggestion(sys::chrome().omnibox().on_delete_suggestion())
}

pub struct OnDeleteSuggestion(sys::EventTarget);

impl OnDeleteSuggestion {
pub fn add_listener<L>(&self, mut listener: L) -> OnDeleteSuggestionListener
where
L: FnMut(&str) + 'static,
{
let listener = Closure::new(move |text: String| {
console::debug!("on delete suggestion", sys::chrome().omnibox(), &text);

listener(text.as_str())
});

OnDeleteSuggestionListener(EventListener::raw_new(&self.0, listener))
}
}

pub struct OnDeleteSuggestionListener<'a>(EventListener<'a, dyn FnMut(String)>);

impl OnDeleteSuggestionListener<'_> {
pub fn forget(self) {
self.0.forget()
}
}

/// User has ended the keyword input session without accepting the input.
pub fn on_input_cancelled() -> OnInputCancelled {
OnInputCancelled(sys::chrome().omnibox().on_input_cancelled())
}

pub struct OnInputCancelled(sys::EventTarget);

impl OnInputCancelled {
pub fn add_listener<L>(&self, mut listener: L) -> OnInputCancelledListener
where
L: FnMut() + 'static,
{
let listener = Closure::new(move || {
console::debug!("on input cancelled", sys::chrome().omnibox());

listener()
});

OnInputCancelledListener(EventListener::raw_new(&self.0, listener))
}
}

pub struct OnInputCancelledListener<'a>(EventListener<'a, dyn FnMut()>);

impl OnInputCancelledListener<'_> {
pub fn forget(self) {
self.0.forget()
}
}

/// User has changed what is typed into the omnibox.
pub fn on_input_changed() -> OnInputChanged {
OnInputChanged(sys::chrome().omnibox().on_input_changed())
}

pub struct OnInputChanged(sys::EventTarget);

impl OnInputChanged {
pub fn add_listener<L>(&self, mut listener: L) -> OnInputChangedListener
where
L: FnMut(&str, &mut (dyn FnMut(Vec<SuggestResult>) -> Result<(), Error> + 'static))
+ 'static,
{
let listener = Closure::new(move |text: String, suggest: Function| {
console::debug!("on input changed", sys::chrome().omnibox(), &text, &suggest);

let mut f = move |results: Vec<SuggestResult>| -> Result<(), Error> {
let this = JsValue::null();
let js_results = js_from_serde(&results).unwrap();

suggest.call1(&this, &js_results)?;

Ok(())
};

listener(text.as_str(), &mut f)
});

OnInputChangedListener(EventListener::raw_new(&self.0, listener))
}
}

pub struct OnInputChangedListener<'a>(EventListener<'a, dyn FnMut(String, Function)>);

impl OnInputChangedListener<'_> {
pub fn forget(self) {
self.0.forget()
}
}

/// User has accepted what is typed into the omnibox.
pub fn on_input_entered() -> OnInputEntered {
OnInputEntered(sys::chrome().omnibox().on_input_entered())
}

pub struct OnInputEntered(sys::EventTarget);

impl OnInputEntered {
pub fn add_listener<L>(&self, mut listener: L) -> OnInputEnteredListener
where
L: FnMut(&str, OnInputEnteredDisposition) + 'static,
{
let callback = Closure::new(move |text: String, disposition: JsValue| {
console::debug!(
"on input entered",
sys::chrome().omnibox(),
&text,
&disposition
);

listener(text.as_str(), disposition.try_into().unwrap())
});

OnInputEnteredListener(EventListener::raw_new(&self.0, callback))
}
}

pub struct OnInputEnteredListener<'a>(EventListener<'a, dyn FnMut(String, JsValue)>);

impl OnInputEnteredListener<'_> {
pub fn forget(self) {
self.0.forget()
}
}

/// User has ended the keyword input session without accepting the input.
pub fn on_input_started() -> OnInputStarted {
OnInputStarted(sys::chrome().omnibox().on_input_started())
}

pub struct OnInputStarted(sys::EventTarget);

impl OnInputStarted {
pub fn add_listener<L>(&self, mut listener: L) -> OnInputStartedListener
where
L: FnMut() + 'static,
{
let listener = Closure::new(move || {
console::debug!("on input started", sys::chrome().omnibox());

listener()
});

OnInputStartedListener(EventListener::raw_new(&self.0, listener))
}
}

pub struct OnInputStartedListener<'a>(EventListener<'a, dyn FnMut()>);

impl OnInputStartedListener<'_> {
pub fn forget(self) {
self.0.forget()
}
}
41 changes: 41 additions & 0 deletions src/tabs/mod.rs
Original file line number Diff line number Diff line change
@@ -30,6 +30,12 @@ impl From<i32> for TabId {
}
}

impl From<TabId> for i32 {
fn from(id: TabId) -> Self {
id.0
}
}

mod on_activated;
mod on_attached;
mod on_created;
@@ -93,3 +99,38 @@ pub struct CreateProperties<'a> {
pub active: bool,
pub url: &'a str,
}

/// Modifies the properties of a tab.
///
/// Properties that are not specified in updateProperties are not modified.
///
/// <https://developer.chrome.com/docs/extensions/reference/tabs/#method-update>
pub async fn update(tab_id: Option<TabId>, props: UpdateProperties<'_>) -> Result<Tab, Error> {
let js_props = js_from_serde(&props)?;
let result = tabs()
.update(tab_id.map(|id| id.0), object_from_js(&js_props)?)
.await;
serde_from_js_result(result)
}

/// Information necessary to open a new tab.
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateProperties<'a> {
/// Whether the tab should be active.
pub active: Option<bool>,
/// Whether the tab should be discarded automatically by the browser when resources are low.
pub auto_discardable: Option<bool>,
/// Adds or removes the tab from the current selection.
pub highlighted: Option<bool>,
/// Whether the tab should be muted.
pub muted: Option<bool>,
/// The ID of the tab that opened this tab.
pub opener_tab_id: Option<TabId>,
/// Whether the tab should be pinned.
pub pinned: Option<bool>,
/// Whether the tab should be selected.
pub selected: Option<bool>,
/// A URL to navigate the tab to.
pub url: Option<&'a str>,
}
2 changes: 1 addition & 1 deletion src/tabs/query_details.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::{prelude::*, Status, WindowType};

/// <https://developer.chrome.com/docs/extensions/reference/tabs/#type-query-queryInfo>
#[derive(Debug, Serialize)]
#[derive(Default, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryDetails<'a> {
pub active: Option<bool>,
2 changes: 2 additions & 0 deletions tests/contextual_identities.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg(feature = "firefox")]

use web_extensions::contextual_identities::*;

mod util;