Skip to content
Open
Show file tree
Hide file tree
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: 7 additions & 0 deletions @types/gecko.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ declare type _ExtensionCommon =
declare type UserSearchEngine = import("../engine/toolkit/components/search/UserSearchEngine.sys.mjs").UserSearchEngine;

declare type GlideHintIPC = import("../src/glide/browser/base/content/hinting.mts").GlideHintIPC;
declare type AppUpdater = import("../engine/toolkit/mozapps/update/AppUpdater.sys.mjs").AppUpdater;
declare type DownloadUtils = typeof import("../engine/toolkit/mozapps/downloads/DownloadUtils.sys.mjs").DownloadUtils;

declare type GlideResolvedHint = GlideHintIPC & {
label: string;
Expand Down Expand Up @@ -233,6 +235,8 @@ declare namespace MockedExports {
"chrome://glide/content/utils/objects.mjs": typeof import("../src/glide/browser/base/content/utils/objects.mts");
"chrome://glide/content/utils/strings.mjs": typeof import("../src/glide/browser/base/content/utils/strings.mts");
"chrome://glide/content/utils/promises.mjs": typeof import("../src/glide/browser/base/content/utils/promises.mts");
"chrome://glide/content/utils/browser-update.mjs":
typeof import("../src/glide/browser/base/content/utils/browser-update.mts");
"chrome://glide/content/utils/browser-ui.mjs":
typeof import("../src/glide/browser/base/content/utils/browser-ui.mts");
"chrome://glide/content/utils/resources.mjs":
Expand Down Expand Up @@ -321,6 +325,9 @@ declare namespace MockedExports {
typeof import("../src/glide/generated/@types/subs/AppConstants.sys.d.ts");
"resource://gre/modules/AppMenuNotifications.sys.mjs":
typeof import("../engine/toolkit/modules/AppMenuNotifications.sys.mjs");
"resource://gre/modules/AppUpdater.sys.mjs": typeof import("../engine/toolkit/mozapps/update/AppUpdater.sys.mjs");
"resource://gre/modules/DownloadUtils.sys.mjs":
typeof import("../engine/toolkit/mozapps/downloads/DownloadUtils.sys.mjs");

"resource://devtools/shared/loader/Loader.sys.mjs":
typeof import("../engine/devtools/shared/loader/Loader.sys.mjs");
Expand Down
2 changes: 2 additions & 0 deletions scripts/polyfill-chromeutils.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ globalThis.ChromeUtils = {
return a_require(`${SRC_DIR}/glide/browser/base/content/please.mts`);
case "chrome://glide/content/event-utils.mjs":
return a_require(`${SRC_DIR}/glide/browser/base/content/event-utils.mts`);
case "chrome://glide/content/utils/browser-update.mjs":
return a_require(`${SRC_DIR}/glide/browser/base/content/utils/browser-update.mts`);

case "chrome://glide/content/browser.mjs":
return a_require(`${SRC_DIR}/glide/browser/base/content/browser.mts`);
Expand Down
194 changes: 194 additions & 0 deletions src/glide/browser/base/content/browser-commandline.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import type { UpdateOption } from "./utils/browser-update.mts";

const DOM = ChromeUtils.importESModule("chrome://glide/content/utils/dom.mjs", { global: "current" });
const DocumentMirror = ChromeUtils.importESModule("chrome://glide/content/document-mirror.mjs", { global: "current" });
const { is_present } = ChromeUtils.importESModule("chrome://glide/content/utils/guards.mjs");
const { AppUpdater } = ChromeUtils.importESModule("resource://gre/modules/AppUpdater.sys.mjs", {});
const { format_download_progress, get_status_text, get_action_label, is_actionable } = ChromeUtils.importESModule(
"chrome://glide/content/utils/browser-update.mjs",
);

export class ExcmdsCompletionSource implements GlideCompletionSource {
id = "excmds";
Expand Down Expand Up @@ -364,3 +370,191 @@ export class CustomCompletionSource implements GlideCompletionSource<CustomCompl
return options;
}
}

export class UpdateCompletionSource implements GlideCompletionSource<UpdateOption> {
id = "update";
readonly container: HTMLElement;

#appUpdater: AppUpdater;
#listener: (status: number, ...args: any[]) => void;
#status_label: HTMLElement;
#action_row: HTMLElement;
#action_label: HTMLElement;
#current_status: number;
#resolved_options: UpdateOption[] | null = null;

constructor() {
this.container = DOM.create_element("div", {
attributes: { anonid: "glide-commandline-completions-update" },
children: [
DOM.create_element("div", { className: "section-header", children: ["update"] }),
DOM.create_element("table", { className: "gcl-table" }),
],
});

this.#status_label = DOM.create_element("span", {
children: ["Initializing…"],
});

this.#action_row = DOM.create_element("tr", {
className: "gcl-option",
});
this.#action_label = DOM.create_element("td", {
colSpan: 3,
children: [""],
});
this.#action_row.appendChild(this.#action_label);
this.#action_row.hidden = true;

this.#appUpdater = new AppUpdater();
this.#current_status = AppUpdater.STATUS.NEVER_CHECKED;

this.#listener = (status: number, ...args: any[]) => {
this.#on_status(status, ...args);
};
this.#appUpdater.addListener(this.#listener);
}

#on_status(status: number, ...args: any[]) {
const STATUS = AppUpdater.STATUS;
this.#current_status = status;

if (status === STATUS.DOWNLOADING && args.length >= 2) {
const [progress, progressMax] = args as [number, number];
this.#status_label.textContent = format_download_progress(progress, progressMax);
} else {
this.#status_label.textContent = get_status_text(STATUS, status, this.#appUpdater.update);
}

if (is_actionable(STATUS, status)) {
this.#action_label.textContent = get_action_label(STATUS, status, this.#appUpdater.update);
this.#action_row.hidden = false;
} else {
this.#action_row.hidden = true;
}
}

check() {
if (this.#current_status === AppUpdater.STATUS.NEVER_CHECKED) {
this.#appUpdater.check();
}
}

destroy() {
this.#appUpdater.removeListener(this.#listener);
this.#appUpdater.stop();
}

get appUpdater(): AppUpdater {
return this.#appUpdater;
}

get currentStatus(): number {
return this.#current_status;
}

is_enabled({ input }: GlideCompletionContext) {
return input.toLowerCase().startsWith("update");
}

search(_ctx: GlideCompletionContext, options: UpdateOption[]) {
for (const option of options) {
option.set_hidden(option.kind === "action" && !!this.#action_row.hidden);
}
}

resolve_options(): UpdateOption[] {
const source = this;
const STATUS = AppUpdater.STATUS;

this.#on_status(this.#current_status);

const status_option: UpdateOption = {
kind: "status",
element: DOM.create_element("tr", {
className: "gcl-option",
children: [
DOM.create_element("td", {
colSpan: 3,
children: [this.#status_label],
}),
],
}),
async accept() {},
async delete() {},
matches() {
return true;
},
is_focused() {
return this.element.classList.contains("focused");
},
set_focused(focused) {
if (focused === this.is_focused()) return;
if (focused) {
this.element.classList.add("focused");
} else {
this.element.classList.remove("focused");
}
},
is_hidden() {
return !!source.container.hidden;
},
set_hidden() {},
};

const action_option: UpdateOption = {
kind: "action",
element: this.#action_row,
async accept() {
const current = source.#current_status;

if (current === STATUS.DOWNLOAD_AND_INSTALL) {
source.#appUpdater.allowUpdateDownload();
await GlideBrowser.upsert_commandline({ prefill: "update" });
return;
}

if (current === STATUS.READY_FOR_RESTART) {
const cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]!.createInstance(Ci.nsISupportsPRBool);
Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
if (cancelQuit.data) {
return;
}

if (Services.appinfo.inSafeMode) {
Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
return;
}

Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
}
},
async delete() {},
matches() {
return true;
},
is_focused() {
return this.element.classList.contains("focused");
},
set_focused(focused) {
if (focused === this.is_focused()) return;
if (focused) {
this.element.classList.add("focused");
} else {
this.element.classList.remove("focused");
}
},
is_hidden() {
return !!source.container.hidden || !!source.#action_row.hidden;
},
set_hidden(hidden) {
if (hidden === this.is_hidden()) return;
source.#action_row.hidden = hidden;
},
};

this.#resolved_options = [status_option, action_option];
this.check();
return this.#resolved_options;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const GLIDE_EXCOMMANDS = [

{ name: "quit", description: "Close all windows", content: false, repeatable: false },
{ name: "clear", description: "Clear all notifications", content: false, repeatable: false },
{ name: "update", description: "Check for Glide updates", content: false, repeatable: false },

{
name: "set",
Expand Down
5 changes: 5 additions & 0 deletions src/glide/browser/base/content/browser-excmds.mts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ class GlideExcmdsClass {
break;
}

case "update": {
await GlideBrowser.upsert_commandline({ prefill: "update" });
break;
}

case "repl": {
const { require } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
const { BrowserConsoleManager } = require("devtools/client/webconsole/browser-console-manager");
Expand Down
1 change: 1 addition & 0 deletions src/glide/browser/base/content/browser.mts
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,7 @@ class GlideBrowserClass {

get commandline_sources(): GlideCompletionSource[] {
return redefine_getter(this, "commandline_sources", [
new CommandLine.UpdateCompletionSource(),
new CommandLine.TabsCompletionSource(),
new CommandLine.ExcmdsCompletionSource(),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -725,3 +725,25 @@ add_task(async function test_commandline_close() {
is(result, true, "close() should return true when the commandline was open");
await wait_for_mode("normal");
});

add_task(async function test_update_commandline() {
await reload_config(function _() {});

await BrowserTestUtils.withNewTab(FILE, async () => {
await GlideTestUtils.commandline.open();
await new Promise(r => requestAnimationFrame(r));

await keys("update");

is(
GlideTestUtils.commandline.current_source_header(),
"update",
"entering `update` should result in update completions",
);
let visible_rows = GlideTestUtils.commandline.visible_rows();
// Note: Updates are disabled in the test environment
// There will be 1 visible row stating that "Updates are disabled by policy"
await waiter(() => visible_rows.length).is(1, "there should only be 1 update option present");
await keys("<esc>");
});
});
76 changes: 76 additions & 0 deletions src/glide/browser/base/content/utils/browser-update.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const { AppUpdater } = ChromeUtils.importESModule("resource://gre/modules/AppUpdater.sys.mjs", {});
const { DownloadUtils } = ChromeUtils.importESModule("resource://gre/modules/DownloadUtils.sys.mjs", {
global: "current",
});

export function format_download_progress(progress: number, progressMax: number): string {
const transfer = DownloadUtils.getTransferTotal(progress, progressMax);
const pct = progressMax > 0 ? Math.round((progress / progressMax) * 100) : 0;
return `Downloading update… ${pct}% (${transfer})`;
}

export function is_actionable(STATUS: typeof AppUpdater.STATUS, status: number): boolean {
return status === STATUS.DOWNLOAD_AND_INSTALL || status === STATUS.READY_FOR_RESTART;
}

export function get_action_label(
STATUS: typeof AppUpdater.STATUS,
status: number,
update: nsIUpdate | null,
): string {
if (status === STATUS.DOWNLOAD_AND_INSTALL) {
const version = update?.displayVersion ?? "";
return version ? `Download and install ${version}` : "Download and install";
}
if (status === STATUS.READY_FOR_RESTART) {
return "Restart to apply update";
}
return "";
}

export interface UpdateOption extends GlideCompletionOption {
kind: "status" | "action";
}

export function get_status_text(STATUS: typeof AppUpdater.STATUS, status: number, update: nsIUpdate | null): string {
switch (status) {
case STATUS.NEVER_CHECKED:
return "Ready to check for updates…";
case STATUS.CHECKING:
return "Checking for updates…";
case STATUS.CHECKING_FAILED:
return "Failed to check for updates";
case STATUS.NO_UPDATES_FOUND:
return "Glide is up to date";
case STATUS.NO_UPDATER:
return "Update system is not available";
case STATUS.UPDATE_DISABLED_BY_POLICY:
return "Updates are disabled by policy";
case STATUS.OTHER_INSTANCE_HANDLING_UPDATES:
return "Another instance is handling updates";
case STATUS.UNSUPPORTED_SYSTEM:
return "Updates are not supported on this system";
case STATUS.MANUAL_UPDATE:
return "Please download the update manually";
case STATUS.DOWNLOAD_AND_INSTALL: {
const version = update?.displayVersion ?? "unknown";
return `Update ${version} available`;
}
case STATUS.DOWNLOADING:
return "Downloading update…";
case STATUS.DOWNLOAD_FAILED:
return "Download failed";
case STATUS.STAGING:
return "Applying update…";
case STATUS.READY_FOR_RESTART:
return "Update ready — restart to apply";
case STATUS.INTERNAL_ERROR:
return "An internal error occurred";
default:
return "Unknown update state";
}
}
3 changes: 2 additions & 1 deletion src/glide/browser/base/jar.mn
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ glide.jar:
content/utils/strings.mjs (content/utils/dist/strings.mjs)
content/utils/promises.mjs (content/utils/dist/promises.mjs)
content/utils/resources.mjs (content/utils/dist/resources.mjs)
content/utils/browser-ui.mjs (content/utils/dist/browser-ui.mjs)
content/utils/browser-ui.mjs (content/utils/dist/browser-ui.mjs)
content/utils/browser-update.mjs (content/utils/dist/browser-update.mjs)

# default plugins
content/plugins/shims.mjs (content/plugins/dist/shims.mjs)
Expand Down