feat: Start making automatic backups for session files, p=#11861

* feat: Start making automatic backups for session files, b=no-bug, c=common

* feat: Allow new links to open in unsynced windows, b=no-bug, c=no-component
This commit is contained in:
mr. m
2026-01-10 22:24:53 +01:00
committed by GitHub
parent 8d25577bdb
commit e8ad33c0f1
5 changed files with 237 additions and 134 deletions

View File

@@ -10,3 +10,6 @@
- name: zen.window-sync.prefer-unsynced-windows
value: false
- name: zen.window-sync.open-link-in-new-unsynced-window
value: true

View File

@@ -1,8 +1,16 @@
diff --git a/browser/modules/URILoadingHelper.sys.mjs b/browser/modules/URILoadingHelper.sys.mjs
index 9175fa820ad6bb75cd125fbfda2bf07d6dba4c90..f673cfd15289401ae425256016bf51303063e0b2 100644
index 9175fa820ad6bb75cd125fbfda2bf07d6dba4c90..62a1a62f304ec8c83e859196861025347e79ba46 100644
--- a/browser/modules/URILoadingHelper.sys.mjs
+++ b/browser/modules/URILoadingHelper.sys.mjs
@@ -542,7 +542,7 @@ export const URILoadingHelper = {
@@ -225,6 +225,7 @@ function openInWindow(url, params, sourceWindow) {
features,
sa
);
+ win._zenStartupSyncFlag = Services.prefs.getBoolPref('zen.window-sync.open-link-in-new-unsynced-window') ? 'unsynced' : 'synced';
}
function openInCurrentTab(targetBrowser, url, uriObj, params) {
@@ -542,7 +543,7 @@ export const URILoadingHelper = {
// page. If a load request bounces off for the currently selected tab,
// we'll open a new tab instead.
let tab = w.gBrowser.getTabForBrowser(targetBrowser);
@@ -11,7 +19,7 @@ index 9175fa820ad6bb75cd125fbfda2bf07d6dba4c90..f673cfd15289401ae425256016bf5130
where = "tab";
targetBrowser = null;
} else if (
@@ -972,7 +972,7 @@ export const URILoadingHelper = {
@@ -972,7 +973,7 @@ export const URILoadingHelper = {
ignoreQueryString || replaceQueryString,
ignoreFragmentWhenComparing
);
@@ -20,7 +28,7 @@ index 9175fa820ad6bb75cd125fbfda2bf07d6dba4c90..f673cfd15289401ae425256016bf5130
for (let i = 0; i < browsers.length; i++) {
let browser = browsers[i];
let browserCompare = cleanURL(
@@ -1018,7 +1018,7 @@ export const URILoadingHelper = {
@@ -1018,7 +1019,7 @@ export const URILoadingHelper = {
}
if (!doAdopt) {

View File

@@ -9,143 +9,148 @@
*
* FOR ANY WEBSITE THAT WOULD NEED TO USE THE ACCENT COLOR, ETC
*/
{
const { AppConstants } = ChromeUtils.importESModule(
'resource://gre/modules/AppConstants.sys.mjs'
);
const kZenThemePrefsList = [
'zen.theme.accent-color',
'zen.theme.border-radius',
'zen.theme.content-element-separation',
];
const kZenMaxElementSeparation = 12;
/**
* ZenThemeModifier controls the application of theme data to the browser,
* for example, it injects the accent color to the document. This is used
* because we need a way to apply the accent color without having to worry about
* shadow roots not inheriting the accent color.
*
* note: It must be a Firefox builtin page with access to the browser's configuration
* and services.
*/
var ZenThemeModifier = {
_inMainBrowserWindow: false,
const kZenThemePrefsList = [
'zen.theme.accent-color',
'zen.theme.border-radius',
'zen.theme.content-element-separation',
];
const kZenMaxElementSeparation = 12;
/**
* Listen for theming updates from the LightweightThemeChild actor, and
* begin listening to changes in preferred color scheme.
* ZenThemeModifier controls the application of theme data to the browser,
* for example, it injects the accent color to the document. This is used
* because we need a way to apply the accent color without having to worry about
* shadow roots not inheriting the accent color.
*
* note: It must be a Firefox builtin page with access to the browser's configuration
* and services.
*/
init() {
this._inMainBrowserWindow = window.location.href == 'chrome://browser/content/browser.xhtml';
this.listenForEvents();
this.updateAllThemeBasics();
},
window.ZenThemeModifier = {
_inMainBrowserWindow: false,
listenForEvents() {
var handleEvent = this.handleEvent.bind(this);
// Listen for changes in the accent color and border radius
for (let pref of kZenThemePrefsList) {
Services.prefs.addObserver(pref, handleEvent);
}
/**
* Listen for theming updates from the LightweightThemeChild actor, and
* begin listening to changes in preferred color scheme.
*/
init() {
this._inMainBrowserWindow = window.location.href == 'chrome://browser/content/browser.xhtml';
this.listenForEvents();
this.updateAllThemeBasics();
},
// Add fullscreen listener to update the theme when going in and out of fullscreen
const eventsForSeparation = [
'ZenViewSplitter:SplitViewDeactivated',
'ZenViewSplitter:SplitViewActivated',
'fullscreen',
'ZenCompactMode:Toggled',
];
const separationHandler = this.updateElementSeparation.bind(this);
for (let eventName of eventsForSeparation) {
window.addEventListener(eventName, separationHandler);
}
window.addEventListener(
'unload',
() => {
for (let pref of kZenThemePrefsList) {
Services.prefs.removeObserver(pref, handleEvent);
}
for (let eventName of eventsForSeparation) {
window.removeEventListener(eventName, separationHandler);
}
},
{ once: true }
);
},
handleEvent() {
// note: even might be undefined, but we shoudnt use it!
this.updateAllThemeBasics();
},
/**
* Update all theme basics, like the accent color.
*/
async updateAllThemeBasics() {
this.updateAccentColor();
this.updateBorderRadius();
this.updateElementSeparation();
},
updateBorderRadius() {
const borderRadius = Services.prefs.getIntPref('zen.theme.border-radius', -1);
// -1 is the default value, will use platform-native values
// otherwise, use the custom value
if (borderRadius == -1) {
if (AppConstants.platform == 'macosx') {
const targetRadius = window.matchMedia('(-moz-mac-tahoe-theme)').matches ? 15 : 10;
document.documentElement.style.setProperty('--zen-border-radius', targetRadius + 'px');
} else if (AppConstants.platform == 'linux') {
// Linux uses GTK CSD titlebar radius, default to 8px
document.documentElement.style.setProperty(
'--zen-border-radius',
'env(-moz-gtk-csd-titlebar-radius, 8px)'
);
} else {
// Windows defaults to 8px
document.documentElement.style.setProperty('--zen-border-radius', '8px');
listenForEvents() {
var handleEvent = this.handleEvent.bind(this);
// Listen for changes in the accent color and border radius
for (let pref of kZenThemePrefsList) {
Services.prefs.addObserver(pref, handleEvent);
}
} else {
// Use the overridden value
document.documentElement.style.setProperty('--zen-border-radius', borderRadius + 'px');
}
},
updateElementSeparation() {
const kMinElementSeparation = 0.1; // in px
let separation = this.elementSeparation;
if (
document.documentElement.hasAttribute('inFullscreen') &&
window.gZenCompactModeManager?.preference &&
!document.getElementById('tabbrowser-tabbox')?.hasAttribute('zen-split-view') &&
Services.prefs.getBoolPref('zen.view.borderless-fullscreen', true)
) {
separation = 0;
}
// In order to still use it on fullscreen, even if it's 0px, add .1px (almost invisible)
separation = Math.max(kMinElementSeparation, separation);
document.documentElement.style.setProperty('--zen-element-separation', separation + 'px');
if (separation == kMinElementSeparation) {
document.documentElement.setAttribute('zen-no-padding', true);
} else {
document.documentElement.removeAttribute('zen-no-padding');
}
},
// Add fullscreen listener to update the theme when going in and out of fullscreen
const eventsForSeparation = [
'ZenViewSplitter:SplitViewDeactivated',
'ZenViewSplitter:SplitViewActivated',
'fullscreen',
'ZenCompactMode:Toggled',
];
const separationHandler = this.updateElementSeparation.bind(this);
for (let eventName of eventsForSeparation) {
window.addEventListener(eventName, separationHandler);
}
get elementSeparation() {
return Math.min(
Services.prefs.getIntPref('zen.theme.content-element-separation'),
kZenMaxElementSeparation
);
},
window.addEventListener(
'unload',
() => {
for (let pref of kZenThemePrefsList) {
Services.prefs.removeObserver(pref, handleEvent);
}
for (let eventName of eventsForSeparation) {
window.removeEventListener(eventName, separationHandler);
}
},
{ once: true }
);
},
/**
* Update the accent color.
*/
updateAccentColor() {
const accentColor = Services.prefs.getStringPref('zen.theme.accent-color');
document.documentElement.style.setProperty('--zen-primary-color', accentColor);
},
};
handleEvent() {
// note: even might be undefined, but we shoudnt use it!
this.updateAllThemeBasics();
},
if (typeof Services !== 'undefined') ZenThemeModifier.init();
/**
* Update all theme basics, like the accent color.
*/
async updateAllThemeBasics() {
this.updateAccentColor();
this.updateBorderRadius();
this.updateElementSeparation();
},
updateBorderRadius() {
const borderRadius = Services.prefs.getIntPref('zen.theme.border-radius', -1);
// -1 is the default value, will use platform-native values
// otherwise, use the custom value
if (borderRadius == -1) {
if (AppConstants.platform == 'macosx') {
const targetRadius = window.matchMedia('(-moz-mac-tahoe-theme)').matches ? 15 : 10;
document.documentElement.style.setProperty('--zen-border-radius', targetRadius + 'px');
} else if (AppConstants.platform == 'linux') {
// Linux uses GTK CSD titlebar radius, default to 8px
document.documentElement.style.setProperty(
'--zen-border-radius',
'env(-moz-gtk-csd-titlebar-radius, 8px)'
);
} else {
// Windows defaults to 8px
document.documentElement.style.setProperty('--zen-border-radius', '8px');
}
} else {
// Use the overridden value
document.documentElement.style.setProperty('--zen-border-radius', borderRadius + 'px');
}
},
updateElementSeparation() {
const kMinElementSeparation = 0.1; // in px
let separation = this.elementSeparation;
if (
document.documentElement.hasAttribute('inFullscreen') &&
window.gZenCompactModeManager?.preference &&
!document.getElementById('tabbrowser-tabbox')?.hasAttribute('zen-split-view') &&
Services.prefs.getBoolPref('zen.view.borderless-fullscreen', true)
) {
separation = 0;
}
// In order to still use it on fullscreen, even if it's 0px, add .1px (almost invisible)
separation = Math.max(kMinElementSeparation, separation);
document.documentElement.style.setProperty('--zen-element-separation', separation + 'px');
if (separation == kMinElementSeparation) {
document.documentElement.setAttribute('zen-no-padding', true);
} else {
document.documentElement.removeAttribute('zen-no-padding');
}
},
get elementSeparation() {
return Math.min(
Services.prefs.getIntPref('zen.theme.content-element-separation'),
kZenMaxElementSeparation
);
},
/**
* Update the accent color.
*/
updateAccentColor() {
const accentColor = Services.prefs.getStringPref('zen.theme.accent-color');
document.documentElement.style.setProperty('--zen-primary-color', accentColor);
},
};
if (typeof Services !== 'undefined') ZenThemeModifier.init();
}

View File

@@ -7,4 +7,5 @@ category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys
category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init
# App shutdown consumers
category browser-quit-application-granted resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.uninit
category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit

View File

@@ -15,9 +15,16 @@ ChromeUtils.defineESModuleGetters(lazy, {
SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs',
setTimeout: 'resource://gre/modules/Timer.sys.mjs',
gWindowSyncEnabled: 'resource:///modules/zen/ZenWindowSync.sys.mjs',
DeferredTask: 'resource://gre/modules/DeferredTask.sys.mjs',
});
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.session-store.log', true);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
'gMaxSessionBackups',
'zen.session-store.max-backups',
10
);
// Note that changing this hidden pref will make the previous session file
// unused, causing a new session file to be created on next write.
@@ -30,6 +37,10 @@ const MIGRATION_PREF = 'zen.ui.migration.session-manager-restore';
// 'browser.startup.page' preference value to resume the previous session.
const BROWSER_STARTUP_RESUME_SESSION = 3;
// The amount of time (in milliseconds) to wait for our backup regeneration
// debouncer to kick off a regeneration.
const REGENERATION_DEBOUNCE_RATE_MS = 20 * 60 * 1000; // 20 minutes
/**
* Class representing the sidebar object stored in the session file.
* This object holds all the data related to tabs, groups, folders
@@ -58,13 +69,17 @@ export class nsZenSessionManager {
* @type {nsZenSidebarObject}
*/
#sidebarObject = new nsZenSidebarObject();
/**
* A deferred task to create backups of the session file.
*/
#deferredBackupTask = null;
// Called from SessionComponents.manifest on app-startup
init() {
let profileDir = Services.dirsvc.get('ProfD', Ci.nsIFile).path;
let backupFile = null;
if (SHOULD_BACKUP_FILE) {
backupFile = PathUtils.join(profileDir, 'zen-sessions-backup', FILE_NAME);
backupFile = PathUtils.join(this.#backupFolderPath, FILE_NAME);
}
let filePath = PathUtils.join(profileDir, FILE_NAME);
this.#file = new JSONFile({
@@ -72,6 +87,15 @@ export class nsZenSessionManager {
compression: SHOULD_COMPRESS_FILE ? 'lz4' : undefined,
backupFile,
});
this.#deferredBackupTask = new lazy.DeferredTask(async () => {
await this.#createBackupsIfNeeded();
}, REGENERATION_DEBOUNCE_RATE_MS);
}
uninit() {
this.#file = null;
this.#deferredBackupTask?.disarm();
this.#deferredBackupTask = null;
}
log(...args) {
@@ -80,6 +104,11 @@ export class nsZenSessionManager {
}
}
get #backupFolderPath() {
let profileDir = Services.dirsvc.get('ProfD', Ci.nsIFile).path;
return PathUtils.join(profileDir, 'zen-sessions-backup');
}
/**
* Gets the spaces data from the Places database for migration.
* This is only called once during the first run after updating
@@ -244,9 +273,66 @@ export class nsZenSessionManager {
// quitting the app.
this.#file.data = this.#sidebar;
this.#file.saveSoon();
this.#debounceRegeneration();
this.log(`Saving Zen session data with ${this.#sidebar.tabs?.length || 0} tabs`);
}
/**
* Called when the last known backup should be deleted and a new one
* created. This uses the #deferredBackupTask to debounce clusters of
* events that might cause such a regeneration to occur.
*/
#debounceRegeneration() {
this.#deferredBackupTask.disarm();
this.#deferredBackupTask.arm();
}
/**
* Creates backups of the session file if needed. We only keep
* a limited number of backups to avoid using too much disk space.
* The way we are doing this is by replacing the file for today's
* date if it already exists, otherwise we create a new one.
* We then delete the oldest backups if we exceed the maximum
* number of backups allowed.
*
* We run the next backup creation after a delay or when idling,
* to avoid blocking the main thread during session saves.
*/
async #createBackupsIfNeeded() {
if (!SHOULD_BACKUP_FILE) {
return;
}
try {
const today = new Date();
const backupFolder = this.#backupFolderPath;
await IOUtils.makeDirectory(backupFolder, {
ignoreExisting: true,
createAncestors: true,
});
const todayFileName = `zen-sessions-${today.getFullYear()}-${(today.getMonth() + 1)
.toString()
.padStart(2, '0')}-${today.getDate().toString().padStart(2, '0')}.json${
SHOULD_COMPRESS_FILE ? 'lz4' : ''
}`;
const todayFilePath = PathUtils.join(backupFolder, todayFileName);
const sessionFilePath = this.#file.path;
this.log(`Backing up session file to ${todayFileName}`);
await IOUtils.copy(sessionFilePath, todayFilePath, { noOverwrite: false });
// Now we need to check if we have exceeded the maximum
// number of backups allowed, and delete the oldest ones
// if needed.
let files = await IOUtils.getChildren(backupFolder);
files = files.filter((file) => file.startsWith('zen-sessions-')).sort();
for (let i = 0; i < files.length - lazy.gMaxSessionBackups; i++) {
const fileToDelete = PathUtils.join(backupFolder, files[i].name);
this.log(`Deleting old backup file ${files[i].name}`);
await IOUtils.remove(fileToDelete);
}
} catch (e) {
console.error('ZenSessionManager: Failed to create session file backups', e);
}
}
/**
* Saves the session data for a closed window if it meets the criteria.
* See SessionStoreInternal.maybeSaveClosedWindow for more details.