diff --git a/ivette/.dome-pkg-app.lock b/ivette/.dome-pkg-app.lock
index 7282f0cc447a1caf3cb155d03008966d7b2f41ab..f9677904486a71fb448d4a6db632128c0d3013ac 100644
--- a/ivette/.dome-pkg-app.lock
+++ b/ivette/.dome-pkg-app.lock
@@ -1 +1 @@
-react@^16.8 react-dom source-map-support lodash react-virtualized react-draggable codemirror
+react@^16.8 react-dom source-map-support lodash react-virtualized react-draggable react-fast-compare codemirror
diff --git a/ivette/.gitignore b/ivette/.gitignore
index da037a9e2fa9fa6eb676dcadabf0f8486c4b79bf..194fc7ea7ce2c5c7ffb30e127542107772220cd0 100644
--- a/ivette/.gitignore
+++ b/ivette/.gitignore
@@ -2,6 +2,7 @@
 # --- Template .gitignore file for Dome
 # --------------------------------------------------------------------------
 
+.ivette
 .dome-*.stamp
 .dome-*.back
 node_modules
diff --git a/ivette/package.json b/ivette/package.json
index d77d420e12005ff11011b27dd7bca04d6531db44..c64081d369b41efd624bd2c93e58da8180866fa5 100644
--- a/ivette/package.json
+++ b/ivette/package.json
@@ -44,7 +44,6 @@
     "eslint-plugin-react-hooks": "^3.0.0",
     "html-loader": "1.0.0-alpha.0",
     "jsdoc": "^3.6.3",
-    "react-fast-compare": "^3.2.0",
     "react-hot-loader": "^4.12.20",
     "serve": "^11.3.0",
     "typedoc": "^0.17.6",
@@ -62,6 +61,7 @@
     "react-draggable": "^4.2.0",
     "react-virtualized": "^9.21.2",
     "source-map-support": "^0.5.16",
+    "react-fast-compare": "^3.2.0",
     "zeromq": "^6.0.0-beta.5"
   }
 }
diff --git a/ivette/src/dome/src/main/dome.js b/ivette/src/dome/src/main/dome.js
index 1385eb6bc5e50f273d46a85201ae79d68e74e093..cd01363afb0b82961da1f77f1baed2b3865e5b7c 100644
--- a/ivette/src/dome/src/main/dome.js
+++ b/ivette/src/dome/src/main/dome.js
@@ -50,83 +50,136 @@ export const platform = System.platform ;
 // --- Settings
 // --------------------------------------------------------------------------
 
-const APP_DIR = app.getPath('userData');
-const APP_SETTINGS = path.join( APP_DIR , 'Settings.json' );
-
-var s_frames = {} ;
-var s_globals = {} ;
-var s_application = {} ;
-var s_preferences = {} ;
+function loadSettings( file ) {
+  try {
+    if (!fstat(file))
+      return {};
+    const text = fs.readFileSync(file, { encoding: 'utf8' } );
+    return Object.assign({}, JSON.parse(text));
+  } catch(err) {
+    console.error("[Dome] Unable to load settings", file, err);
+    return {};
+  }
+}
 
-function loadSettings() {
+function saveSettings( file, data={} ) {
   try {
-    if (!fstat( APP_SETTINGS )) return;
-    const content = fs.readFileSync( APP_SETTINGS, { encoding: 'utf8' } );
-    const loaded = JSON.parse( content );
-    const MERGE = (store,field) => _.merge( store , _.get( loaded , field ));
-    s_frames = MERGE( s_frames , 'frames' );
-    s_globals = MERGE( s_globals, 'globals' );
-    s_application = MERGE( s_application, 'application' );
-    s_preferences = MERGE( s_preferences, 'preferences' );
+    const text = JSON.stringify( data, undefined, DEVEL ? 2 : 0 );
+    fs.writeFileSync( file, text, { encoding: 'utf8' }, (err) => { throw(err); } );
   } catch(err) {
-    console.error("[Dome] Can not load application settings\n" + err);
+    console.error("[Dome] Unable to save settings", file, err);
   }
 }
 
-function saveSettings() {
+// --------------------------------------------------------------------------
+// --- Global Settings
+// --------------------------------------------------------------------------
+
+var GlobalSettings; // Current Dictionnary
+
+const APP_DIR = app.getPath('userData');
+const PATH_WINDOW_SETTINGS = path.join( APP_DIR, 'WindowSettings.json' );
+const PATH_GLOBAL_SETTINGS = path.join( APP_DIR, 'GlobalSettings.json' );
+
+function saveGlobalSettings() {
   try {
     if (!fstat( APP_DIR )) fs.mkdirSync( APP_DIR );
-    const saved = {
-      globals: s_globals,
-      application: s_application,
-      preferences: s_preferences,
-      frames: s_frames
-    };
-    const content = JSON.stringify( saved, undefined, DEVEL ? 2 : 0 );
-    fs.writeFileSync( APP_SETTINGS, content, { encoding: 'utf8' }, errorSettings );
+    saveSettings( PATH_GLOBAL_SETTINGS, GlobalSettings );
   } catch(err) {
-    errorSettings(err);
+    console.error("[Dome] Unable to save global settings", err);
   }
 }
 
-const fireSaveSettings = _.debounce( saveSettings , 50 );
+function obtainGlobalSettings() {
+  if (!GlobalSettings) {
+    GlobalSettings = loadSettings( PATH_GLOBAL_SETTINGS );
+  }
+  return GlobalSettings;
+}
+
+// --------------------------------------------------------------------------
+// --- Window Settings & Frames
+// --------------------------------------------------------------------------
 
-function errorSettings(err) {
-  if (err) console.error("[Dome] Can not save application settings\n" + err);
+/* Window Handle:
+   {
+     window: BrowserWindow ; // Also prevents Gc
+     config: path;           // Path to config file
+     frame: { x,y,w,h };     // Frame position
+     settings: object;       // Current settings
+     reload: boolean;        // Reloaded window
+   }
+ */
+
+const WindowHandles = {}; // Indexed by *webContents* id
+
+function saveWindowConfig(handle) {
+  const settings = {
+    frame: handle.frame,
+    settings: handle.settings,
+    devtools: handle.devtools
+  };
+  saveSettings( handle.config, settings );
 }
 
-function remoteSyncSettings(event)
-{
-  const isSetting = windowSettings && windowSettings.id === event.frameId ;
+function windowSyncSettings(event) {
+  const handle = WindowHandles[event.sender.id];
   event.returnValue = {
-    globals: s_globals,
-    settings: isSetting ? s_preferences : s_application
+    globals: obtainGlobalSettings(),
+    settings: handle && handle.settings
   };
 }
 
-function remoteSaveWindowSettings(event,patches)
-{
-  const isSetting = windowSettings && windowSettings.id === event.frameId ;
-  _.merge( isSetting ? s_preferences : s_application , patches );
-  saveSettings();
+ipcMain.on('dome.ipc.settings.sync', windowSyncSettings );
+
+// --------------------------------------------------------------------------
+// --- Patching Settings
+// --------------------------------------------------------------------------
+
+function applyPatches( data, args ) {
+  args.forEach(({ key, value }) => {
+    if (value === null) {
+      delete data[key];
+    } else {
+      data[key] = value;
+    }
+  });
 }
 
-function remoteSaveGlobalSettings(event,patches)
-{
-  _.merge( s_globals , patches );
-  saveSettings();
-  BrowserWindow.getAllWindows().forEach((win) => {
-    if (win.id !== event.frameId)
-      win.send('dome.ipc.settings.update',patches);
+function applyWindowSettings(event,args) {
+  const handle = WindowHandles[event.sender.id];
+  if (handle) {
+    applyPatches( handle.settings, args );
+    if (DEVEL) saveWindowConfig( handle );
+  }
+}
+
+function applyGlobalSettings(event,args) {
+  applyPatches( obtainGlobalSettings(), args );
+  BrowserWindow.getAllWindows().forEach((w) => {
+    if (w.webContents.id !== event.sender.id) {
+      w.send('dome.ipc.settings.update',args);
+    }
   });
+  if (DEVEL) saveGlobalSettings();
 }
 
-ipcMain.on('dome.ipc.settings.sync', remoteSyncSettings );
-ipcMain.on('dome.ipc.settings.window', remoteSaveWindowSettings );
-ipcMain.on('dome.ipc.settings.global', remoteSaveGlobalSettings );
+ipcMain.on('dome.ipc.settings.window', applyWindowSettings );
+ipcMain.on('dome.ipc.settings.global', applyGlobalSettings );
 
 // --------------------------------------------------------------------------
-// --- Active Windows
+// --- Renderer-Process Communication
+// --------------------------------------------------------------------------
+
+function broadcast( event, ...args )
+{
+  BrowserWindow.getAllWindows().forEach((w) => {
+    w.send( event, ...args );
+  });
+}
+
+// --------------------------------------------------------------------------
+// --- Window Activities
 // --------------------------------------------------------------------------
 
 var appName = 'Dome' ;
@@ -141,21 +194,24 @@ export function setName(title) {
 }
 
 function setTitle(event,title) {
-  let w = BrowserWindow.fromId( event.frameId );
-  w.setTitle( title || appName );
+  let handle = WindowHandles[event.sender.id];
+  handle && handle.setTitle( title || appName );
 }
 
 function setModified(event,modified) {
-  let w = BrowserWindow.frameId( event.frameId );
-  if (platform == 'macos')
-    w.setDocumentEdited( modified );
-  else {
-    let title = w.getTitle();
-    if (title.startsWith(MODIFIED))
-      title = title.substring(MODIFIED.length);
-    if (modified)
-      title = MODIFIED + title ;
-    w.setTitle(title);
+  let handle = WindowHandles[event.sender.id];
+  if (handle) {
+    const w = handle.window;
+    if (platform == 'macos')
+      w.setDocumentEdited( modified );
+    else {
+      let title = w.getTitle();
+      if (title.startsWith(MODIFIED))
+        title = title.substring(MODIFIED.length);
+      if (modified)
+        title = MODIFIED + title ;
+      w.setTitle(title);
+    }
   }
 }
 
@@ -192,34 +248,55 @@ function navigateURL( event , url ) {
 }
 
 // --------------------------------------------------------------------------
-// --- Browser Window SetUp
+// --- Lookup for config file
 // --------------------------------------------------------------------------
 
-const windowsHandle = {} ; // Prevent live windows to be garbage collected
-const windowsReload = {} ; // Reloaded windows
+function lookupConfig(wdir) {
+  let cwd = wdir = path.resolve(wdir);
+  let cfg = '.' + appName.toLowerCase();
+  for(;;) {
+    const here = path.join(cwd,cfg);
+    if (fstat(here)) return here;
+    let up = path.dirname(cwd);
+    if (up === cwd) break;
+    cwd = up;
+  }
+  const home = path.resolve(app.getPath('home'));
+  const user = wdir.startsWith(home) ? wdir : home ;
+  return path.join( user, cfg );
+}
+
+// --------------------------------------------------------------------------
+// --- Browser Window SetUp
+// --------------------------------------------------------------------------
 
-function createBrowserWindow( config, isMain=true )
+function createBrowserWindow( config, argv, wdir )
 {
 
-  const argv = isMain
+  const isAppWindow = (argv !== undefined && wdir !== undefined);
+
+  const browserArguments = isAppWindow
         ? SYS.WINDOW_APPLICATION_ARGV
         : SYS.WINDOW_PREFERENCES_ARGV ;
 
-  const options = _.merge(
+  const options = Object.assign(
     {
       show: false,
       backgroundColor: '#f0f0f0',
       webPreferences: {
         nodeIntegration:true,
-        additionalArguments: [ argv ]
+        additionalArguments: [ browserArguments ]
       }
-    }
-    , config );
+    },
+    config
+  );
 
-  const frameId = isMain ? 'application' : 'preferences' ;
-  const frame = _.get( s_frames, frameId );
-  const getInt = (v) => v && _.toSafeInteger(v);
+  const configFile = isAppWindow ? lookupConfig( wdir ) : PATH_WINDOW_SETTINGS ;
+  const configData = loadSettings( configFile );
+
+  const { frame, devtools, settings={} } = configData;
   if (frame) {
+    const getInt = (v) => v && _.toSafeInteger(v);
     options.x = getInt(frame.x);
     options.y = getInt(frame.y);
     options.width = getInt(frame.width);
@@ -227,8 +304,25 @@ function createBrowserWindow( config, isMain=true )
   }
 
   const theWindow = new BrowserWindow( options );
-  const wid = theWindow.id;
-  
+  const wid = theWindow.webContents.id;
+
+  const handle = {
+    window: theWindow,
+    config: configFile,
+    frame, settings, devtools,
+    reload: false
+  };
+
+  // Keep the window reference (prevent garbage collection)
+  WindowHandles[wid] = handle;
+
+  // Emitted when the window is closed.
+  theWindow.on('closed', () => {
+    saveWindowConfig(handle);
+    // Dereference the window object (allow garbage collection)
+    delete WindowHandles[wid] ;
+  });
+
   // Load the index.html of the app.
   if (DEVEL || LOCAL)
     process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true';
@@ -239,7 +333,7 @@ function createBrowserWindow( config, isMain=true )
   theWindow.once('ready-to-show' , () => {
     if (DEVEL || LOCAL)
       process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'false';
-    if (DEVEL)
+    if (DEVEL && devtools)
       theWindow.openDevTools();
     theWindow.show();
   });
@@ -251,57 +345,44 @@ function createBrowserWindow( config, isMain=true )
   // URL Navigation
   theWindow.webContents.on('will-navigate', navigateURL );
   theWindow.webContents.on('did-navigate-in-page', navigateURL );
+
+  // Application Startup
   theWindow.webContents.on('did-finish-load', () => {
-    const isLoaded = windowsReload[wid];
-    if (!isLoaded) {
-      windowsReload[wid] = true;
+    if (!handle.reload) {
+      handle.reload = true;
     } else {
       broadcast('dome.ipc.reload');
     }
+    theWindow.send('dome.ipc.command',argv,wdir);
   });
 
   // Emitted when the window want's to close.
   theWindow.on('close', (evt) => {
+    handle.frame = theWindow.getBounds();
+    handle.devtools = theWindow.isDevToolsOpened();
     theWindow.send('dome.ipc.closing');
-    const frame = theWindow.getBounds();
-    _.set( s_frames, frameId , frame );
   });
 
   // Keep track of frame positions (in DEVEL)
   if (DEVEL) {
-    const reframe = _.debounce( (evt) => {
-      const frame = theWindow.getBounds();
-      _.set( s_frames, frameId , frame );
-      saveSettings();
+    const saveFrame = _.debounce( (evt) => {
+      handle.frame = theWindow.getBounds();
+      handle.devtools = theWindow.isDevToolsOpened();
+      saveWindowConfig(handle);
     } , 300);
-    theWindow.on('resize',reframe);
-    theWindow.on('moved',reframe);
+    theWindow.on('resize',saveFrame);
+    theWindow.on('moved',saveFrame);
   }
 
-  // Keep the window reference to prevent destruction
-  windowsHandle[ wid ] = theWindow ;
-
-  // Emitted when the window is closed.
-  theWindow.on('closed', () => {
-    // Dereference the window object to actually destroy it
-    delete windowsHandle[ wid ] ;
-  });
-
   return theWindow ;
 }
 
 // --------------------------------------------------------------------------
-// --- Application Window(s)
+// --- Application Window(s) & Command Line
 // --------------------------------------------------------------------------
 
-function filterArgv( argv ) {
-  return argv.slice( DEVEL ? 3 : (LOCAL ? 2 : 1) ).filter((p) => p);
-}
-
-function sendCommand( win, argv, wdir ) {
-  win.webContents.on('did-finish-load', () => {
-    win.webContents.send('dome.ipc.command',argv,wdir);
-  });
+function stripElectronArgv( argv ) {
+  return argv.slice( DEVEL ? 3 : (LOCAL ? 2 : 1) ).filter((p) => !!p);
 }
 
 function createPrimaryWindow()
@@ -309,31 +390,28 @@ function createPrimaryWindow()
   // Initialize Menubar
   Menubar.install();
 
-  // Initialize Settings
-  loadSettings();
-
   // React Developper Tools
   if (DEVEL)
     installExtension(REACT_DEVELOPER_TOOLS,true);
-
-  const primary = createBrowserWindow({ title: appName } , true);
-  const wdir = process.cwd() === '/' ? app.getPath('home') : process.cwd() ;
-  sendCommand( primary , filterArgv(process.argv) , wdir );
+  const cwd = process.cwd();
+  const wdir = cwd === '/' ? app.getPath('home') : cwd ;
+  const argv = stripElectronArgv(process.argv);
+  createBrowserWindow({ title: appName } , argv, wdir );
 }
 
 var appCount = 1;
 
-function createSecondaryWindow(_event,argv,wdir)
+function createSecondaryWindow(_event,process_argv,wdir)
 {
-  const secondary = createBrowserWindow({ title: `${appName} #${++appCount}` }, true);
-  sendCommand( secondary, filterArgv(argv), wdir );
+  const argv = stripElectronArgv(process_argv);
+  createBrowserWindow({ title: `${appName} #${++appCount}` }, argv, wdir);
 }
 
 function createDesktopWindow()
 {
   const instance = appCount++ ;
-  const secondary = createBrowserWindow({ title: `${appName} #${++appCount}` }, true);
-  sendCommand( secondary , [] , app.getPath('home') );
+  const wdir = app.getPath('home');
+  createBrowserWindow({ title: `${appName} #${++appCount}` }, [], wdir);
 }
 
 // --------------------------------------------------------------------------
@@ -361,31 +439,35 @@ function activateWindows() {
 // --- Settings Window
 // --------------------------------------------------------------------------
 
-var windowSettings = undefined ; // Preference Window
+var PreferenceWindow = undefined ; // Preference Window
 
 function showSettingsWindow()
 {
-  if (!windowSettings)
-    windowSettings = createBrowserWindow({
+  if (!PreferenceWindow)
+    PreferenceWindow = createBrowserWindow({
       title: appName + ' Settings',
       width: 256,
       height: 248,
       fullscreen: false,
       maximizable: false,
       minimizable: false
-    }, false);
-  windowSettings.show();
-  windowSettings.on('closed',() => windowSettings = undefined);
+    });
+  PreferenceWindow.show();
+  PreferenceWindow.on('closed',() => PreferenceWindow = undefined);
 }
 
 function restoreDefaultSettings()
 {
-  s_globals = {} ;
-  s_preferences = {} ;
-  s_application = {} ;
-  s_frames = {} ;
-  fireSaveSettings();
-  fireSaveSettings.flush();
+  GlobalSettings = {};
+  if (DEVEL) saveGlobalSettings();
+
+  _.forEach( WindowHandles, (handle) => {
+    // Keep frame for user comfort
+    handle.settings = {};
+    handle.devtools = handle.window.isDevToolsOpened();
+    if (DEVEL) saveWindowConfig(handle);
+  });
+
   broadcast( 'dome.ipc.settings.defaults' );
 }
 
@@ -402,22 +484,21 @@ export function start() {
   // Change default locale
   app.commandLine.appendSwitch('lang','en');
 
-  // Listen to window events
+  // Listen to application events
   app.on( 'ready', createPrimaryWindow ); // Wait for Electron init
   app.on( 'activate', activateWindows ); // Mac OSX response to dock
   app.on( 'second-instance', createSecondaryWindow );
   app.on( 'dome.menu.settings', showSettingsWindow );
   app.on( 'dome.menu.defaults', restoreDefaultSettings );
 
-  // Performing on-exit callbacks
+  // At-exit callbacks
   app.on( 'will-quit' , () => {
+    saveGlobalSettings();
     System.doExit() ;
-    fireSaveSettings();
-    fireSaveSettings.flush();
   });
 
-  // On OS X menu bar stay active until the user quits explicitly from menu.
-  // On other systems, quit when all windows are closed.
+  // On macOS the menu bar stays active until the user explicitly quits.
+  // On other systems, automatically quit when all windows are closed.
   // Warning: when no event handler is registered, the app automatically
   // quit when all windows are closed.
   app.on( 'window-all-closed', () => {
@@ -426,17 +507,6 @@ export function start() {
 
 }
 
-// --------------------------------------------------------------------------
-// --- Renderer-Process Communication
-// --------------------------------------------------------------------------
-
-function broadcast( event, ...args )
-{
-  BrowserWindow.getAllWindows().forEach((w) => {
-    w.send( event, ...args );
-  });
-}
-
 // --------------------------------------------------------------------------
 // --- MenuBar Management
 // --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/misc/plugins.js b/ivette/src/dome/src/misc/plugins.js
index 5d468d4017a308f65f0dfad93f3ef2dcc6c17aea..87a08c0c836e960d70158825db1a98a685f2282c 100644
--- a/ivette/src/dome/src/misc/plugins.js
+++ b/ivette/src/dome/src/misc/plugins.js
@@ -34,7 +34,7 @@ export function install( name )
   let config ;
   try { config = JSON.pargse(fs.readFileSync( pkg , 'UTF-8' )); }
   catch(err) {
-    console.error( `[Dome] reading '${pkg}':\n`, err );
+    console.error( `[Dome] Reading '${pkg}':\n`, err );
     throw `Plugin '${name}' has invalid 'package.json' file` ;
   }
 
@@ -49,7 +49,7 @@ export function install( name )
   let bundle ;
   try { bundle = fs.readFileSync( bundlejs , 'UTF-8' ); }
   catch(err) {
-    console.error( `[Dome] loading '${bundlejs}':\n`, err );
+    console.error( `[Dome] Loading '${bundlejs}':\n`, err );
     throw `Plugin '${name}' can not load its entry point` ;
   }
 
@@ -63,7 +63,7 @@ export function install( name )
     let module = { id, exports };
     compiled( module, require, static_d );
   } catch(err) {
-    console.error( `[Dome] running '${bundlejs}':\n`, err );
+    console.error( `[Dome] Running '${bundlejs}':\n`, err );
     throw `Plugin '${name}' can not install bundle` ;
   }
   register( id, exports ); // final exports
diff --git a/ivette/src/dome/src/misc/system.js b/ivette/src/dome/src/misc/system.js
index 48808f72562593c59043fcd977d147a4f5abee93..3a4deac841f74817b387562192b72de5108edd91 100644
--- a/ivette/src/dome/src/misc/system.js
+++ b/ivette/src/dome/src/misc/system.js
@@ -593,7 +593,7 @@ export function spawn(command,args,options) {
     }
 
     if ( !process ) {
-      throw "[Dome] Can not create process ('"+command+"')";
+      throw `[Dome] Unable to create process ('${command}')`;
       return;
     }
 
diff --git a/ivette/src/dome/src/renderer/controls/buttons.tsx b/ivette/src/dome/src/renderer/controls/buttons.tsx
index da254148990e172ace57336361370f2a9fb42bf1..0789962f9c941cebc455ec5c53ea11d0b2cce94d 100644
--- a/ivette/src/dome/src/renderer/controls/buttons.tsx
+++ b/ivette/src/dome/src/renderer/controls/buttons.tsx
@@ -161,8 +161,6 @@ export function Button(props: ButtonProps) {
     + (display ? '' : ' dome-control-erased')
     + (className ? ' ' + className : '');
   const nofocus = focusable ? undefined : true;
-  console.log('ICON', Icon);
-  console.log('LABEL', LABEL);
   return (
     <button type='button'
       className={theClass}
diff --git a/ivette/src/dome/src/renderer/data/compare.ts b/ivette/src/dome/src/renderer/data/compare.ts
index 6dcbec1d018875f4d3eceff7f1eafb345d04b700..5f643b4cc8598d834e39ed8eb593616f6345294d 100644
--- a/ivette/src/dome/src/renderer/data/compare.ts
+++ b/ivette/src/dome/src/renderer/data/compare.ts
@@ -8,6 +8,8 @@
    @module dome/data/compare
 */
 
+import FastCompare from 'react-fast-compare';
+
 /**
    Interface for comparison functions.
    These function shall fullfill the following contract:
@@ -22,11 +24,19 @@ export interface Order<A> {
   (x: A, y: A): number;
 }
 
+/**
+   Deep structural equality.
+   Provided by [react-fast-compare]().
+*/
+export const isEqual = FastCompare;
+
+/** Always returns 0. */
 export function equal(_x: any, _y: any): 0 { return 0; }
 
+/** Primitive comparison works on this type. */
 export type bignum = bigint | number;
 
-/** Non-NaN numbers and big-ints */
+/** Detect Non-NaN numbers and big-ints. */
 export function isBigNum(x: any): x is bignum {
   return typeof (x) === 'bigint' || (typeof (x) === 'number' && !Number.isNaN(x));
 }
@@ -222,6 +232,34 @@ export function byAllFields<A>(order: ByAllFields<A>): Order<A> {
   };
 }
 
+export type dict<A> = undefined | null | { [key: string]: A };
+
+/**
+   Compare dictionaries _wrt_ lexicographic order of entries.
+*/
+export function dictionary<A>(order: Order<A>): Order<dict<A>> {
+  return (x: dict<A>, y: dict<A>) => {
+    if (x === y) return 0;
+    const dx = x ?? {};
+    const dy = y ?? {};
+    const phi = option(order);
+    const fs = Object.getOwnPropertyNames(dx).sort();
+    const gs = Object.getOwnPropertyNames(dy).sort();
+    const p = fs.length;
+    const q = gs.length;
+    for (let i = 0, j = 0; i < p && j < q;) {
+      let a = undefined, b = undefined;
+      const f = fs[i];
+      const g = gs[j];
+      if (f <= g) { a = dx[f]; i++; }
+      if (g <= f) { b = dy[g]; j++; }
+      const cmp = phi(a, b);
+      if (cmp != 0) return cmp;
+    }
+    return p - q;
+  };
+}
+
 /** Pair comparison. */
 export function pair<A, B>(ordA: Order<A>, ordB: Order<B>): Order<[A, B]> {
   return (u, v) => {
diff --git a/ivette/src/dome/src/renderer/data/json.ts b/ivette/src/dome/src/renderer/data/json.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4c1d36796e57d508ffef52d80abc2810e21e8744
--- /dev/null
+++ b/ivette/src/dome/src/renderer/data/json.ts
@@ -0,0 +1,422 @@
+// --------------------------------------------------------------------------
+// --- JSON Utilities
+// --------------------------------------------------------------------------
+
+/**
+   Safe JSON utilities.
+   @packageDocumentation
+   @module dome/data/json
+*/
+
+import { DEVEL } from 'dome/system';
+
+export type json =
+  undefined | null | number | string | json[] | { [key: string]: json }
+
+/**
+   Parse without _revivals_.
+   Returned data is guaranteed to have only [[json]] type.
+   If an error occurs and `noError` is set to `true`,
+   the function returns `undefined` and logs the error in console
+   (DEVEL mode only).
+ */
+export function parse(text: string, noError = false): json {
+  if (noError) {
+    try {
+      return JSON.parse(text);
+    } catch (err) {
+      if (DEVEL) console.error('[Dome.json] Invalid format:', err);
+      return undefined;
+    }
+  } else
+    return JSON.parse(text);
+}
+
+/**
+   Export JSON (or any data) as a compact string.
+*/
+export function stringify(js: any) {
+  return JSON.stringify(js, undefined, 0);
+}
+
+/**
+   Export JSON (or any data) as a string with indentation.
+ */
+export function pretty(js: any) {
+  return JSON.stringify(js, undefined, 2);
+}
+
+// --------------------------------------------------------------------------
+// --- SAFE Decoder
+// --------------------------------------------------------------------------
+
+/** Decoder for values of type `D`.
+    You can abbreviate `Safe<D | undefined>` with `Loose<D>`. */
+export type Safe<D> = (js?: json) => D;
+
+/** Decoder for values of type `D`, if any.
+    Same as `Safe<D | undefined>`. */
+export type Loose<D> = (js?: json) => D | undefined;
+
+/**
+   Encoder for value of type `D`.
+   In most cases, you only need [[identity]].
+ */
+export type Encoder<D> = (v: D) => json;
+
+/** Can be used for most encoders. */
+export function identity<A>(v: A): A { return v; };
+
+// --------------------------------------------------------------------------
+// --- Primitives
+// --------------------------------------------------------------------------
+
+/** Primitive JSON number or `undefined`. */
+export const jNumber: Loose<number> = (js: json) => (
+  typeof js === 'number' && !Number.isNaN(js) ? js : undefined
+);
+
+/** Primitive JSON number, rounded to integer, or `undefined`. */
+export const jInt: Loose<number> = (js: json) => (
+  typeof js === 'number' && !Number.isNaN(js) ? Math.round(js) : undefined
+);
+
+/** Primitive JSON number or `0`. */
+export const jZero: Safe<number> = (js: json) => (
+  typeof js === 'number' && !Number.isNaN(js) ? js : 0
+);
+
+/** Primitive JSON boolean or `undefined`. */
+export const jBoolean: Loose<boolean> = (js: json) => (
+  typeof js === 'boolean' ? js : undefined
+);
+
+/** Primitive JSON boolean or `true`. */
+export const jTrue: Safe<boolean> = (js: json) => (
+  typeof js === 'boolean' ? js : true
+);
+
+/** Primitive JSON boolean or `false`. */
+export const jFalse: Safe<boolean> = (js: json) => (
+  typeof js === 'boolean' ? js : false
+);
+
+/** Primitive JSON string or `undefined`. */
+export const jString: Loose<string> = (js: json) => (
+  typeof js === 'string' ? js : undefined
+);
+
+/**
+   One of the enumerated _constants_ or `undefined`.
+   The typechecker will prevent you from listing values that are not in
+   type `A`. However, it will not protect you from missings constants in `A`.
+*/
+export function jEnum<A>(...values: ((string | number) & A)[]): Loose<A> {
+  var m = new Map<string | number, A>();
+  values.forEach(v => m.set(v, v));
+  return (v: json) => (typeof v === 'string' ? m.get(v) : undefined);
+}
+
+/**
+   Refine a loose decoder with some default value.
+   The default value is returned when the provided JSON is `undefined` or
+   when the loose decoder returns `undefined`.
+ */
+export function jDefault<A>(
+  fn: Loose<A>,
+  defaultValue: A,
+): Safe<A> {
+  return (js: json) =>
+    js === undefined ? defaultValue : (fn(js) ?? defaultValue);
+}
+
+/**
+   Force returning `undefined` or a default value for `undefined` JSON input.
+   Typically useful to leverage an existing `Safe<A>` decoder.
+ */
+export function jOption<A>(fn: Safe<A>, defaultValue?: A): Loose<A> {
+  return (js: json) => (js === undefined ? defaultValue : fn(js));
+}
+
+/**
+   Force returning `undefined` or a default value for `undefined` _or_ `null`
+   JSON input. Typically useful to leverage an existing `Safe<A>` decoder.
+ */
+export function jNull<A>(fn: Safe<A>, defaultValue?: A): Loose<A> {
+  return (js: json) => (js === undefined || js === null ? defaultValue : fn(js));
+}
+
+/**
+   Fail when the loose decoder returns `undefined`.
+   See also [[jCatch]] and [[jTry]].
+ */
+export function jFail<A>(fn: Loose<A>, error: Error): Safe<A> {
+  return (js: json) => {
+    const d = fn(js);
+    if (d !== undefined) return d;
+    throw error;
+  };
+}
+
+/**
+   Provide a fallback value in case of undefined value or error.
+   See also [[jFail]] and [[jTry]].
+ */
+export function jCatch<A>(fn: Loose<A>, fallBack: A): Safe<A> {
+  return (js: json) => {
+    try {
+      return fn(js) ?? fallBack;
+    } catch (err) {
+      if (DEVEL) console.error('[Dome.json]', err);
+      return fallBack;
+    }
+  };
+}
+
+/**
+   Provides an (optional) default value in case of error or undefined value.
+   See also [[jFail]] and [[jCatch]].
+ */
+export function jTry<A>(fn: Loose<A>, defaultValue?: A): Loose<A> {
+  return (js: json) => {
+    try {
+      return fn(js) ?? defaultValue;
+    } catch (_err) {
+      return defaultValue;
+    }
+  };
+}
+
+/**
+   Converts maps to dictionaries.
+ */
+export function jMap<A>(fn: Loose<A>): Safe<Map<string, A>> {
+  return (js: json) => {
+    const m = new Map<string, A>();
+    if (js !== null && typeof js === 'object' && !Array.isArray(js)) {
+      for (let k of Object.keys(js)) {
+        const v = fn(js[k]);
+        if (v !== undefined) m.set(k, v);
+      }
+    }
+    return m;
+  };
+}
+
+/**
+   Converts dictionaries to maps.
+ */
+export function eMap<A>(fn: Encoder<A>): Encoder<Map<string, undefined | A>> {
+  return m => {
+    const js: json = {};
+    m.forEach((v, k) => {
+      if (v !== undefined) {
+        const u = fn(v);
+        if (u !== undefined) js[k] = u;
+      }
+    });
+    return js;
+  };
+}
+
+/**
+   Apply the decoder on each item of a JSON array, or return `[]` otherwise.
+   Can be also applied on a _loose_ decoder, but you will get
+   an array with possibly `undefined` elements. Use [[jList]]
+   to discard undefined elements, or use a true _safe_ decoder.
+ */
+export function jArray<A>(fn: Safe<A>): Safe<A[]> {
+  return (js: json) => Array.isArray(js) ? js.map(fn) : [];
+}
+
+/**
+   Apply the loose decoder on each item of a JSON array, discarding
+   all `undefined` elements. To keep the all possibly undefined array entries,
+   use [[jArray]] instead.
+ */
+export function jList<A>(fn: Loose<A>): Safe<A[]> {
+  return (js: json) => {
+    const buffer: A[] = [];
+    if (Array.isArray(js)) js.forEach(vj => {
+      const d = fn(vj);
+      if (d !== undefined) buffer.push(d);
+    });
+    return buffer;
+  };
+}
+
+/**
+   Exports all non-undefined elements.
+ */
+export function eList<A>(fn: Encoder<A>): Encoder<(A | undefined)[]> {
+  return m => {
+    const js: json[] = [];
+    m.forEach(v => {
+      if (v !== undefined) {
+        const u = fn(v);
+        if (u !== undefined) js.push(u);
+      }
+    });
+    return js;
+  };
+}
+
+/** Apply a pair of decoders to JSON pairs, or return `undefined`. */
+export function jPair<A, B>(
+  fa: Safe<A>,
+  fb: Safe<B>,
+): Loose<[A, B]> {
+  return (js: json) => Array.isArray(js) ? [
+    fa(js[0]),
+    fb(js[1]),
+  ] : undefined;
+}
+
+/** Similar to [[jPair]]. */
+export function jTriple<A, B, C>(
+  fa: Safe<A>,
+  fb: Safe<B>,
+  fc: Safe<C>,
+): Loose<[A, B, C]> {
+  return (js: json) => Array.isArray(js) ? [
+    fa(js[0]),
+    fb(js[1]),
+    fc(js[2]),
+  ] : undefined;
+}
+
+/** Similar to [[jPair]]. */
+export function jTuple4<A, B, C, D>(
+  fa: Safe<A>,
+  fb: Safe<B>,
+  fc: Safe<C>,
+  fd: Safe<D>,
+): Loose<[A, B, C, D]> {
+  return (js: json) => Array.isArray(js) ? [
+    fa(js[0]),
+    fb(js[1]),
+    fc(js[2]),
+    fd(js[3]),
+  ] : undefined;
+}
+
+/** Similar to [[jPair]]. */
+export function jTuple5<A, B, C, D, E>(
+  fa: Safe<A>,
+  fb: Safe<B>,
+  fc: Safe<C>,
+  fd: Safe<D>,
+  fe: Safe<E>,
+): Loose<[A, B, C, D, E]> {
+  return (js: json) => Array.isArray(js) ? [
+    fa(js[0]),
+    fb(js[1]),
+    fc(js[2]),
+    fd(js[3]),
+    fe(js[4]),
+  ] : undefined;
+}
+
+/**
+   Decoders for each property of object type `A`.
+   Optional fields in `A` can be assigned a loose decoder.
+*/
+export type Props<A> = {
+  [P in keyof A]: Safe<A[P]>;
+}
+
+/**
+   Decode an object given the decoders of its fields.
+   Returns `undefined` for non-object JSON.
+ */
+export function jObject<A>(fp: Props<A>): Loose<A> {
+  return (js: json) => {
+    if (js !== null && typeof js === 'object' && !Array.isArray(js)) {
+      const buffer = {} as A;
+      for (var k of Object.keys(fp)) {
+        const fn = fp[k as keyof A];
+        if (fn !== undefined) {
+          const fj = js[k];
+          if (fj !== undefined) {
+            const fv = fn(fj);
+            if (fv !== undefined) buffer[k as keyof A] = fv;
+          }
+        }
+      }
+      return buffer;
+    }
+    return undefined;
+  };
+}
+
+/**
+   Encoders for each property of object type `A`.
+*/
+export type EProps<A> = {
+  [P in keyof A]?: Encoder<A[P]>;
+}
+
+/**
+   Encode an object given the provided encoders by fields.
+   The exported JSON object has only original
+   fields with some specified encoder.
+ */
+export function eObject<A>(fp: EProps<A>): Encoder<A> {
+  return (m: A) => {
+    const js: json = {};
+    for (var k of Object.keys(fp)) {
+      const fn = fp[k as keyof A];
+      if (fn !== undefined) {
+        const fv = m[k as keyof A];
+        if (fv !== undefined) {
+          const r = fn(fv);
+          if (r !== undefined) js[k] = r;
+        }
+      }
+    }
+    return js;
+  }
+}
+
+/** Type of dictionaries. */
+export type dict<A> = { [key: string]: A };
+
+/**
+   Decode a JSON dictionary, discarding all inconsistent entries.
+   If the JSON contains no valid entry, still returns `{}`.
+*/
+export function jDictionary<A>(fn: Loose<A>): Safe<dict<A>> {
+  return (js: json) => {
+    const buffer: dict<A> = {};
+    if (js !== null && typeof js === 'object' && !Array.isArray(js)) {
+      for (var k of Object.keys(js)) {
+        const fd = js[k];
+        if (fd !== undefined) {
+          const fv = fn(fd);
+          if (fv !== undefined) buffer[k] = fv;
+        }
+      }
+    }
+    return buffer;
+  };
+}
+
+/**
+   Encode a dictionary into JSON, discarding all inconsistent entries.
+   If the dictionary contains no valid entry, still returns `{}`.
+*/
+export function eDictionary<A>(fn: Encoder<A>): Encoder<dict<A>> {
+  return (d: dict<A>) => {
+    const js: json = {};
+    for (var k of Object.keys(d)) {
+      const fv = d[k];
+      if (fv !== undefined) {
+        const fv = fn(d[k]);
+        if (fv !== undefined) js[k] = fv;
+      }
+    }
+    return js;
+  };
+}
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/renderer/data/states.ts b/ivette/src/dome/src/renderer/data/states.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b9e0de2a648e5d5fee1d0081ada3a9d4f58f7308
--- /dev/null
+++ b/ivette/src/dome/src/renderer/data/states.ts
@@ -0,0 +1,328 @@
+// --------------------------------------------------------------------------
+// --- States
+// --------------------------------------------------------------------------
+
+/**
+   Typed States & Settings
+   @packageDocumentation
+   @module dome/data/states
+*/
+
+import React from 'react';
+import isEqual from 'react-fast-compare';
+import { DEVEL } from 'dome/misc/system';
+import * as Dome from 'dome';
+import * as JSON from './json';
+
+export type NonFunction =
+  undefined | null | boolean | number | string
+  | object | any[] | bigint | symbol;
+
+/** State updater. New value or updating function applied to the current,
+    lastly updated value. Use `null` to restore default value. */
+export type updateAction<A extends NonFunction> =
+  null | A | ((current: A) => A);
+
+/** The type of updater callbacks. Typically used for `[A, setState<A>]`
+    hooks. */
+export type setState<A extends NonFunction> = (action: updateAction<A>) => void;
+
+/** Base state interface. */
+export interface State<A extends NonFunction> {
+  readonly get: () => A;
+  readonly set: (value: A) => void;
+  readonly update: setState<A>;
+  on(callback: (value: A) => void): void;
+  off(callback: (value: A) => void): void;
+}
+
+/** React Hook, similar to `React.useState()`. */
+export function useState<A extends NonFunction>(s: State<A>): [A, setState<A>] {
+  const [current, setCurrent] = React.useState<A>(s.get);
+  React.useEffect(() => {
+    s.on(setCurrent);
+    return () => s.off(setCurrent);
+  });
+  return [current, s.update];
+};
+
+/**
+   State with initial default value.
+ */
+export class StateDef<A extends NonFunction> implements State<A> {
+  protected value: A;
+  protected defaultValue: A;
+  protected event: symbol;
+
+  constructor(defaultValue: A) {
+    this.value = this.defaultValue = defaultValue;
+    this.event = Symbol('dome.state');
+    this.get = this.get.bind(this);
+    this.set = this.get.bind(this);
+    this.reset = this.reset.bind(this);
+    this.update = this.update.bind(this);
+  }
+
+  get(): A { return this.value; }
+
+  /** Notify callbacks on change, using _deep_ structural comparison. */
+  set(value: A) {
+    if (!isEqual(value, this.value)) {
+      this.value = value;
+      Dome.emit(this.event, value);
+    }
+  }
+
+  /** State updater. */
+  update(upd: updateAction<A>) {
+    if (upd === null)
+      this.reset();
+    else {
+      if (typeof upd === 'function')
+        this.set(upd(this.value));
+      else
+        this.set(upd);
+    }
+  }
+
+  /** Restore default value. */
+  reset() {
+    this.set(this.defaultValue);
+  }
+
+  /** Callback Emitter. */
+  on(callback: (value: A) => void) {
+    Dome.emitter.on(this.event, callback);
+  }
+
+  /** Callback Emitter. */
+  off(callback: (value: A) => void) {
+    Dome.emitter.off(this.event, callback);
+  }
+
+}
+
+/**
+   State with possibly undefined initial value.
+ */
+export class StateOpt<A extends NonFunction> extends StateDef<undefined | A> {
+  constructor(defaultValue?: A) {
+    super(defaultValue);
+  }
+}
+
+// --------------------------------------------------------------------------
+// --- Settings
+// --------------------------------------------------------------------------
+
+/**
+   Generic interface to Window and Global Settings.
+   To be used with [[useSettings]] with instances of its derived classes,
+   typically [[WindowSettings]] and [[GlobalSettings]]. You should never have
+   to implement a Settings class on your own.
+
+   All setting values are identified with
+   an untyped `dataKey: string`, that can be dynamically modified
+   for each component. Hence, two components might share both datakeys
+   and settings.
+
+   When several components share the same setting `dataKey` the behavior will be
+   different depending on the situation:
+   - for Window Settings, each component in each window retains its own
+   setting value, although the last modified value from _any_ of them will be
+   saved and used for any further initial value;
+   - for Global Settings, all components synchronize to the last modified value
+   from any component of any window.
+
+   Type safety is ensured by safe JSON encoders and decoders, however, they
+   might fail at runtime, causing settings value to be initialized to their
+   fallback and not to be saved nor synchronized.
+   This is not harmful but annoying.
+
+   To mitigate this effect, each instance of a Settings class has its
+   own, private, unique symbol that we call its « role ». A given `dataKey`
+   shall always be used with the same « role » otherwise it is discarded,
+   and an error message is logged when in DEVEL mode.
+ */
+abstract class Settings<A> {
+
+  private static keyRoles = new Map<string, symbol>();
+
+  private readonly role: symbol;
+  protected readonly decoder: JSON.Safe<A>;
+  protected readonly encoder: JSON.Encoder<A>;
+
+  /**
+     Encoders shall be protected against exception.
+     Use [[JSON.jTry]] and [[JSON.jCatch]] in case of uncertainty.
+     Decoders are automatically protected internally to the Settings class.
+     @param role Debugging name of instance roles (each instance has its unique
+     role, though)
+     @param decoder JSON decoder for the setting values
+     @param encoder JSON encoder for the setting values
+     @param fallback If provided, used to automatically protect your encoders
+     against exceptions.
+   */
+  constructor(
+    role: string,
+    decoder: JSON.Safe<A>,
+    encoder: JSON.Encoder<A>,
+    fallback?: A,
+  ) {
+    this.role = Symbol(role);
+    this.encoder = encoder;
+    this.decoder =
+      fallback !== undefined ? JSON.jCatch(decoder, fallback) : decoder;
+  }
+
+  /**
+     Returns identity if the data key is only
+     used with the same setting instance.
+     Otherwise, returns `undefined`.
+   */
+  validateKey(dataKey?: string): string | undefined {
+    if (dataKey === undefined) return undefined;
+    const rq = this.role;
+    const rk = Settings.keyRoles.get(dataKey);
+    if (rk === undefined) {
+      Settings.keyRoles.set(dataKey, rq);
+    } else {
+      if (rk !== rq) {
+        if (DEVEL) console.error(
+          `[Dome.settings] Key ${dataKey} used with incompatible roles`, rk, rq,
+        );
+        return undefined;
+      }
+    }
+    return dataKey;
+  }
+
+  /** @internal */
+  abstract loadData(key: string): JSON.json;
+
+  /** @internal */
+  abstract saveData(key: string, data: JSON.json): void;
+
+  /** @internal */
+  abstract event: string;
+
+  /** Returns the current setting value for the provided data key. You shall
+      only use validated keys otherwise you might fallback to default values. */
+  loadValue(dataKey?: string) {
+    return this.decoder(dataKey ? this.loadData(dataKey) : undefined)
+  }
+
+  /** Push the new setting value for the provided data key.
+      You shall only use validated keys otherwise further loads
+      might fail and fallback to defaults. */
+  saveValue(dataKey: string, value: A) {
+    try { this.saveData(dataKey, this.encoder(value)); }
+    catch (err) {
+      if (DEVEL) console.error(
+        '[Dome.settings] Error while encoding value',
+        dataKey, value, err,
+      );
+    }
+  }
+
+}
+
+/**
+   Generic React Hook to be used with any kind of [[Settings]].
+   You may share `dataKey` between components, or change it dynamically.
+   However, a given data key shall always be used for the same Setting instance.
+   See [[Settings]] documentation for details.
+   @param S The instance settings to be used.
+   @param dataKey Identifies which value in the settings to be used.
+ */
+export function useSettings<A>(
+  S: Settings<A>,
+  dataKey?: string,
+): [A, (update: A) => void] {
+
+  const theKey = React.useMemo(() => S.validateKey(dataKey), [S, dataKey]);
+  const [value, setValue] = React.useState<A>(() => S.loadValue(theKey));
+
+  React.useEffect(() => {
+    if (theKey) {
+      const callback = () => setValue(S.loadValue(theKey));
+      Dome.on(S.event, callback);
+      return () => Dome.off(S.event, callback);
+    }
+    return undefined;
+  });
+
+  const updateValue = React.useCallback((update: A) => {
+    if (!isEqual(value, update)) {
+      setValue(update);
+      if (theKey) S.saveValue(theKey, update);
+    }
+  }, [S, theKey]);
+
+  return [value, updateValue];
+
+}
+
+/** Window Settings for non-JSON data.
+    In most situations, you can use [[WindowSettings]] instead.
+    You can use a [[JSON.Loose]] decoder for optional values. */
+export class WindowSettingsData<A> extends Settings<A> {
+
+  constructor(
+    role: string,
+    decoder: JSON.Safe<A>,
+    encoder: JSON.Encoder<A>,
+    fallback?: A,
+  ) {
+    super(role, decoder, encoder, fallback);
+  }
+
+  event = 'dome.defaults';
+  loadData(key: string) { return Dome.getWindowSetting(key) as JSON.json; }
+  saveData(key: string, data: JSON.json) { Dome.setWindowSetting(key, data); }
+
+}
+
+/** Global Settings for non-JSON data.
+    In most situations, you can use [[WindowSettings]] instead.
+    You can use a [[JSON.Loose]] decoder for optional values. */
+export class GlobalSettingsData<A> extends Settings<A> {
+
+  constructor(
+    role: string,
+    decoder: JSON.Safe<A>,
+    encoder: JSON.Encoder<A>,
+    fallback?: A,
+  ) {
+    super(role, decoder, encoder, fallback);
+  }
+
+  event = 'dome.settings';
+  loadData(key: string) { return Dome.getGlobalSetting(key) as JSON.json; }
+  saveData(key: string, data: JSON.json) { Dome.setGlobalSetting(key, data); }
+
+}
+
+/** Window Settings.
+    For non-JSON data, use [[WindowSettingsdata]] instead.
+    You can use a [[JSON.Loose]] decoder for optional values. */
+export class WindowSettings<A extends JSON.json> extends WindowSettingsData<A> {
+
+  constructor(role: string, decoder: JSON.Safe<A>, fallback?: A) {
+    super(role, decoder, JSON.identity, fallback);
+  }
+
+}
+
+/** Global Settings.
+    For non-JSON data, use [[WindowSettingsdata]] instead.
+    You can use a [[JSON.Loose]] decoder for optional values. */
+export class GlobalSettings<A extends JSON.json> extends GlobalSettingsData<A> {
+
+  constructor(role: string, decoder: JSON.Safe<A>, fallback?: A) {
+    super(role, decoder, JSON.identity, fallback);
+  }
+
+}
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/renderer/dome.js b/ivette/src/dome/src/renderer/dome.js
index 55f1a81cadb1de48ba7d2d72b9ded74d41ba827b..322a76d8bc3242c7d2631da72db37257d2602649 100644
--- a/ivette/src/dome/src/renderer/dome.js
+++ b/ivette/src/dome/src/renderer/dome.js
@@ -148,10 +148,6 @@ ipcRenderer.on('dome.ipc.command', (_event,argv,wdir) => {
   emitter.emit('dome.command',argv,wdir);
 });
 
-// --------------------------------------------------------------------------
-// --- Main-Process Communication
-// --------------------------------------------------------------------------
-
 // --------------------------------------------------------------------------
 // --- Window Management
 // --------------------------------------------------------------------------
@@ -395,55 +391,69 @@ export function popupMenu( items, callback )
 // --- Settings
 // --------------------------------------------------------------------------
 
-var globals = {} ;
-var globalPatches = {} ;
+var globalSettings = new Map();
+var globalPatches = new Map();
 
-var settings = {} ;
-var settingsPatches = {} ;
+var windowSettings = new Map();
+var windowPatches = new Map();
+
+const initSetting =
+      (m, data) => _.forEach(data,(value,key) => m.set(key,value));
 
 // initial values => synchronized event
-function syncSettings() {
+const syncSettings = () => {
   const fullSettings = ipcRenderer.sendSync('dome.ipc.settings.sync');
-  globals = fullSettings.globals ;
-  settings = fullSettings.settings ;
-}
+  initSetting( globalSettings, fullSettings.globals );
+  initSetting( windowSettings, fullSettings.settings );
+};
 
 const readSetting = ( local, key, defaultValue ) => {
-  const value = _.get( local ? settings : globals , key );
+  const store = local ? windowSettings : globalSettings;
+  const value = store.get(key);
   return value === undefined ? defaultValue : value ;
 };
 
 const writeSetting = ( local, key, value ) => {
-  if (key) {
-    const theValue = value===undefined ? null : value ;
-    const store = local ? settings : globals ;
-    const patches = local ? settingsPatches : globalPatches ;
-    _.set( store, key, theValue );
-    _.set( patches,  key, theValue );
+  const store = local ? windowSettings : globalSettings;
+  const patches = local ? windowPatches : globalPatches;
+  if (value === undefined) {
+    store.delete(key);
+    patches.set(key,null);
+  } else {
+    store.set(key,value);
+    patches.set(key,value);
+  }
+  if (local) {
+    fireSaveSettings();
+  } else {
     emitter.emit('dome.settings');
-    if (local) {
-      if (DEVEL) fireSaveSettings();
-    } else {
-      fireSaveGlobals();
-    }
+    fireSaveGlobals();
+  }
+};
+
+const flushPatches = (m) => {
+  if (m.size > 0) {
+    const args = [];
+    m.forEach((value,key) => {
+      args.push({ key, value });
+    });
+    m.clear();
+    return args;
   }
+  return undefined;
 };
 
 const fireSaveSettings = _.debounce(
   () => {
-    if (!_.isEmpty(settingsPatches)) {
-      ipcRenderer.send( 'dome.ipc.settings.window', settingsPatches ) ;
-      settingsPatches = {} ;
-    }
+    const args = flushPatches(windowPatches);
+    args && ipcRenderer.send( 'dome.ipc.settings.window', args ) ;
   }, 100
 );
 
 const fireSaveGlobals = _.debounce(
   () => {
-    if (!_.isEmpty(globalPatches)) {
-      ipcRenderer.send( 'dome.ipc.settings.global', globalPatches ) ;
-      globalPatches = {} ;
-    }
+    const args = flushPatches(globalPatches);
+    args && ipcRenderer.send( 'dome.ipc.settings.global', args ) ;
   }, 100
 );
 
@@ -456,25 +466,32 @@ ipcRenderer.on('dome.ipc.closing', (_evt) => {
 });
 
 /** @event 'dome.settings'
-    @description Emitted when the settings have been updated. */
+    @description Emitted when the global settings have been updated. */
 
 /** @event 'dome.defaults'
-    @description Emitted when the settings have been reset to default. */
+    @description Emitted when the window settings have re-initialized. */
 
 ipcRenderer.on('dome.ipc.settings.defaults',(sender) => {
   fireSaveSettings.cancel();
   fireSaveGlobals.cancel();
-  settingsPatches = {};
-  globalPatches = {};
-  settings = {};
-  globals = {};
-  emitter.emit('dome.defaults');
+  windowPatches.clear();
+  globalPatches.clear();
+  windowSettings.clear();
+  globalSettings.clear();
   emitter.emit('dome.settings');
+  emitter.emit('dome.defaults');
 });
 
 ipcRenderer.on('dome.ipc.settings.update',(sender,patches) => {
-  // Don't cancel local updates
-  _.merge( globals , patches , globalPatches );
+  patches.forEach(({ key, value }) => {
+    // Don't cancel local updates
+    if (!globalPatches.has(key)) {
+      if (value === null)
+        globalSettings.delete(key);
+      else
+        globalSettings.set(key,value);
+    }
+  });
   emitter.emit('dome.settings');
 });
 
@@ -493,7 +510,7 @@ export function getWindowSetting( key, defaultValue ) {
 }
 
 /** @summary Set value into local window (persistent) settings.
-    @param {string} key to store the data
+    @param {string} [key] to store the data
     @param {any} value associated value or object
     @description
     This settings are local to the current window, but persistently
@@ -501,7 +518,7 @@ export function getWindowSetting( key, defaultValue ) {
     For global application settings, use `setGlobal()` instead.
 */
 export function setWindowSetting( key , value ) {
-  writeSetting( true, key, value );
+  key && writeSetting( true, key, value );
 }
 
 /**
@@ -524,7 +541,8 @@ export function getGlobalSetting( key, defaultValue ) {
     @description
     These settings are global to the current window, but persistently
     saved in the user's home directory. Updated values are broadcasted
-    in batch to all other windows, which in turn receive a `'dome.settings'`
+    in batch to all other windows,
+    which in turn receive a `'dome.settings'`
     event for synchronizing.<br/>
     For local window settings, use `set()` instead.
 */
@@ -684,25 +702,21 @@ export function useCommand() {
 
 function useSettings( local, settings, defaultValue )
 {
-  const [ value, setValue ] = React.useState(() => readSetting( local, settings, defaultValue ));
+  const [ value, setValue ] =
+        React.useState(() => readSetting( local, settings, defaultValue ));
   React.useEffect(() => {
-    if (settings) {
-      let callback = () => {
-        let v = readSetting( local, settings , defaultValue );
-        setValue(v);
-      };
-      emitter.on('dome.settings',callback);
-      return () => emitter.off( 'dome.settings', callback );
-    } else {
-      let callback = () => setValue(defaultValue);
-      emitter.on('dome.defaults',callback);
-      return () => emitter.off( 'dome.defaults', callback );
-    }
+    let callback = () => {
+      let v = readSetting( local, settings , defaultValue );
+      setValue(v);
+    };
+    const event = local ? 'dome.defaults' : 'dome.settings' ;
+    emitter.on(event,callback);
+    return () => emitter.off(event, callback);
   });
   const doUpdate = (upd) => {
     const theValue = typeof(upd)==='function' ? upd(value) : upd ;
     if (settings) writeSetting( local, settings, theValue );
-    else setValue(theValue);
+    if (local) setValue(theValue);
   };
   return [ value, doUpdate ];
 }
@@ -715,8 +729,7 @@ function useSettings( local, settings, defaultValue )
    @description
    Similar to `React.useState()` with persistent _window_ settings.
    When the settings key is undefined, it simply uses a local React state.
-   Also responds to `'dome.settings'` to update the state and `'dome.defaults'`
-   to restore the default value.
+   Also responds to `'dome.defaults'`.
 
    The `setValue` callback accepts either a value, or a function to be applied
    on current value.
@@ -750,8 +763,7 @@ export function useSwitch( settings, defaultValue=false )
    @description
    Similar to `React.useState()` with persistent _global_ settings.
    When the settings key is undefined, it simply uses a local React state.
-   Also responds to `'dome.settings'` to update the state and `'dome.defaults'`
-   to restore the default value.
+   Also responds to `'dome.settings'` to update the state.
 
    The `setValue` callback accepts either a value, or a function to be applied
    on current value.
diff --git a/ivette/src/dome/src/renderer/frame/sidebars.js b/ivette/src/dome/src/renderer/frame/sidebars.js
index 08ed6ae624eea139d9d54fac1198d05a1c90a685..6fcd8e42a50b7fca2f0dff2261ebd09b4daf413b 100644
--- a/ivette/src/dome/src/renderer/frame/sidebars.js
+++ b/ivette/src/dome/src/renderer/frame/sidebars.js
@@ -108,7 +108,7 @@ export function Section(props) {
 
   const context = React.useContext( SideBarContext );
   const [ state=true, setState ] = Dome.useState(
-    makeSettings(context,props),
+    makeSettings(context.settings,props),
     props.defaultUnfold
   );
   const { enabled=true, disabled=false, unfold, children } = props ;
diff --git a/ivette/src/dome/src/renderer/table/views.tsx b/ivette/src/dome/src/renderer/table/views.tsx
index 953cdbdf5901e39f3b9ae3eff6de5a7affa1f1b8..b485a7e8a5ccb7d062c7bd6e61eac70aeba48002 100644
--- a/ivette/src/dome/src/renderer/table/views.tsx
+++ b/ivette/src/dome/src/renderer/table/views.tsx
@@ -212,7 +212,7 @@ function makeDataGetter(
       if (rowData !== undefined) return getter(rowData, dataKey);
     } catch (err) {
       console.error(
-        '[Dome.table] custom getter error',
+        '[Dome.table] Custom getter error',
         'rowData:', rowData,
         'dataKey:', dataKey,
         err,
@@ -240,7 +240,7 @@ function makeDataRenderer(
       return contents;
     } catch (err) {
       console.error(
-        '[Dome.table] custom renderer error',
+        '[Dome.table] Custom renderer error',
         'dataKey:', props.dataKey,
         'cellData:', cellData,
         err,
diff --git a/ivette/src/dome/src/renderer/text/buffers.js b/ivette/src/dome/src/renderer/text/buffers.js
index 2546e8f5b7cc81d1a159c4a2d218c811fc028625..444970d2c104736ea9ac09970ea030638a836dfa 100644
--- a/ivette/src/dome/src/renderer/text/buffers.js
+++ b/ivette/src/dome/src/renderer/text/buffers.js
@@ -564,7 +564,7 @@ is blocked.
     } else if (typeof text === 'string') {
       this.append(text);
     } else if (text !== null) {
-      console.error('[Dome.buffers] unexpected text',text);
+      console.error('[Dome.buffers] Unexpected text', text);
     }
   }
 
diff --git a/ivette/src/dome/template/makefile.packages b/ivette/src/dome/template/makefile.packages
index 05b8cb03aece9be54813a53d3952d9e804943839..34a494d77320ff171a14873d6eb15b4456e9bbc0 100644
--- a/ivette/src/dome/template/makefile.packages
+++ b/ivette/src/dome/template/makefile.packages
@@ -25,6 +25,7 @@ DOME_APP_PACKAGES= \
 	lodash \
 	react-virtualized \
 	react-draggable \
+	react-fast-compare \
 	codemirror
 
 # --------------------------------------------------------------------------
diff --git a/ivette/src/frama-c/LabViews.tsx b/ivette/src/frama-c/LabViews.tsx
index d41a4bc67b37746d9f5fe3c1b9b0e1a0a9928a24..52bc3151cac0951f6f9642dee1e7ceb83b05dc6b 100644
--- a/ivette/src/frama-c/LabViews.tsx
+++ b/ivette/src/frama-c/LabViews.tsx
@@ -355,7 +355,6 @@ function CustomViews({ settings, shape, setShape, views: libViews }: any) {
   const [edited, setEdited]: any = React.useState();
   const triggerDefault = React.useRef();
   const { current, shapes = {} } = local;
-
   const theViews: any = {};
 
   _.forEach(libViews, (view) => {
diff --git a/ivette/src/renderer/Controller.tsx b/ivette/src/renderer/Controller.tsx
index 01d73e3cb1486b920d9fbd9e812189eac70cbe75..699fa98dc8a2018dc87afac4848253179e67aaad 100644
--- a/ivette/src/renderer/Controller.tsx
+++ b/ivette/src/renderer/Controller.tsx
@@ -169,17 +169,10 @@ const editor = new RichTextBuffer();
 const RenderConsole = () => {
   const scratch = React.useRef([] as string[]);
   const [cursor, setCursor] = React.useState(-1);
-  const [H0, setH0] = Dome.useState('Controller.history', []);
+  const [history, setHistory] = Dome.useState('Controller.history', []);
   const [isEmpty, setEmpty] = React.useState(true);
   const [noTrash, setNoTrash] = React.useState(true);
 
-  // Cope with merge settings that keeps previous array entries (BUG in DOME)
-  const history = Array.isArray(H0) ? H0.filter((h) => h !== '') : [];
-  const setHistory = (hs: string[]) => {
-    const n = hs.length;
-    setH0(n < 50 ? hs.concat(Array(50 - n).fill('')) : hs);
-  };
-
   Dome.useEmitter(editor, 'change', () => {
     const cmd = editor.getValue().trim();
     setEmpty(cmd === '');