diff --git a/README.md b/README.md index 0998955..f5bff21 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,62 @@ -# Outlook Relook +# Outcut -A Chrome extension that reskins Outlook Web App (outlook.office.com) with minimalist themes and granular control over visual clutter. +Keyboard shortcuts for Outlook Web App — Gmail-style multi-select, delete, archive, and more. ## Features -- **Switchable themes:** Swiss (Helvetica minimalism) and Material (clean, subtle elevation) -- **Light & Dark modes** with system preference detection -- **~40 granular toggles** for density, element hiding, readability, and behavior -- **MutationObserver** suppresses dynamically injected clutter (Copilot, banners, suggested replies) -- **Behavior patches:** auto-collapse ribbon, suppress contact hover cards, auto-advance after delete, toast management -- **Gmail-style keyboard navigation:** j/k to move, x/Space to multi-select, # delete, e archive, Shift+i/u read/unread, v move -- **Quick actions:** "Mark all as read" button, Ctrl+Shift+K folder jump -- **Settings sync** across Chrome instances via chrome.storage.sync -- **Export/Import/Reset** settings +- **Gmail or Outlook shortcut presets** — choose your preferred key bindings +- **Multi-select** — select multiple messages and act on them at once +- **Batch actions** — delete, archive, flag, pin, move, read/unread on selected messages +- **Works everywhere** — outlook.office.com, outlook.cloud.microsoft, and Outlook PWA -## Install (Development) +### Gmail Preset (default) +| Key | Action | +|-----|--------| +| j / Down | Next message | +| k / Up | Previous message | +| x / Space | Toggle select | +| Shift+Down/Up | Extend selection | +| # | Delete | +| e | Archive | +| Shift+i / Shift+u | Toggle read/unread | +| v | Move to folder | +| f | Flag/unflag | +| p | Pin/unpin | +| Escape | Deselect all | +| Enter / o | Open message | +### Outlook Preset +| Key | Action | +|-----|--------| +| Down / j | Next message | +| Up / k | Previous message | +| Space | Toggle select | +| Shift+Down/Up | Extend selection | +| Delete / Backspace | Delete | +| e | Archive | +| q / Shift+i | Toggle read/unread | +| v | Move to folder | +| f / Insert | Flag/unflag | +| p | Pin/unpin | +| Escape | Deselect all | +| Enter | Open message | + +## Install + +### From Chrome Web Store +Coming soon. + +### Development 1. Clone this repo 2. Open `chrome://extensions` in Chrome -3. Enable "Developer mode" (top-right toggle) +3. Enable "Developer mode" 4. Click "Load unpacked" and select this directory -5. Open [outlook.office.com](https://outlook.office.com) — the extension activates automatically +5. Open Outlook Web App — the extension activates automatically ## Usage -Click the extension icon to open the settings panel. Changes apply immediately — no page reload needed. +Click the extension icon to choose your shortcut preset (Gmail or Outlook) and toggle keyboard navigation on/off. -### Keyboard Shortcuts +## Privacy -**Message list (Gmail-style):** -- `j` / `Down Arrow` — Next message -- `k` / `Up Arrow` — Previous message -- `x` / `Space` — Toggle select -- `Shift+Down` / `Shift+Up` — Extend selection -- `#` — Delete selected -- `e` — Archive selected -- `Shift+i` — Mark read -- `Shift+u` — Mark unread -- `v` — Move to folder -- `Escape` — Deselect all -- `Enter` / `o` — Open message - -**Global:** -- `Ctrl+Shift+K` (or `Cmd+Shift+K` on Mac) — Quick folder jump - -## Development - -No build step. Edit files, reload the extension at `chrome://extensions`, refresh the OWA tab. - -- `themes/base.css` — density/spacing/hiding rules (always loaded) -- `themes/swiss.css` / `themes/material.css` — theme-specific styles -- `content/` — content scripts (observer, behavior, injector) -- `popup/` — settings panel -- `selectors-test.html` — offline selector verification - -### Selector Strategy - -OWA uses obfuscated class names. Selectors prioritize `aria-label`, `data-*`, and `role` attributes over class names. See `content/selectors.js` for the registry. - -When selectors break after an OWA update, check the console for `[Outlook Relook]` warnings, inspect the live DOM, and update `selectors.js`. - -## Adding a New Theme - -1. Create `themes/yourtheme.css` — define the same CSS custom properties as `swiss.css` -2. Add an option to the theme dropdown in `popup/popup.html` -3. That's it — the content script handles injection automatically - -## Adding a New Toggle - -1. Add the key and default to `DEFAULTS` in both `content/settings-defaults.js` and `popup/popup.js` -2. Add the toggle HTML to the appropriate section in `popup/popup.html` -3. If CSS-based: add a `data-or-*` attribute mapping in `content/content.js` and the CSS rule in `themes/base.css` -4. If observer-based: add a selector entry to `content/selectors.js` and a mapping in `content/observer.js` -5. If behavior-based: add a setup function in `content/behavior.js` +Outcut stores only your preferences locally. No data is collected or transmitted. See [PRIVACY.md](PRIVACY.md). diff --git a/content/behavior.js b/content/behavior.js index ea55e27..e984a58 100644 --- a/content/behavior.js +++ b/content/behavior.js @@ -20,7 +20,7 @@ window.OutlookRelook.Behavior = (function () { for (var i = 0; i < elements.length; i++) { if (elements[i].getAttribute('aria-expanded') === 'true') { elements[i].click(); - console.log('[Outlook Relook] Auto-collapsed ribbon'); + console.log('[Outcut] Auto-collapsed ribbon'); } } }, 2000); @@ -217,7 +217,7 @@ window.OutlookRelook.Behavior = (function () { for (var w = 0; w < composeWindows.length; w++) { composeWindows[w].style.minHeight = '60vh'; composeWindows[w].style.height = '60vh'; - console.log('[Outlook Relook] Auto-resized compose window'); + console.log('[Outcut] Auto-resized compose window'); } } } @@ -242,7 +242,7 @@ window.OutlookRelook.Behavior = (function () { window.Notification = function (title, options) { var now = Date.now(); if (now - lastNotificationTime < MIN_INTERVAL) { - console.log('[Outlook Relook] Throttled notification: "' + title + '"'); + console.log('[Outcut] Throttled notification: "' + title + '"'); return {}; } lastNotificationTime = now; @@ -271,7 +271,7 @@ window.OutlookRelook.Behavior = (function () { setupAutoResizeCompose(); setupThrottleNotifications(); - console.log('[Outlook Relook] Behavior patches applied'); + console.log('[Outcut] Behavior patches applied'); } function updateSettings(settings) { diff --git a/content/content.js b/content/content.js index b307306..7ec6136 100644 --- a/content/content.js +++ b/content/content.js @@ -62,7 +62,7 @@ if (settings.accentColor) { document.documentElement.style.setProperty('--or-accent-override', settings.accentColor); } - console.log('[Outlook Relook] Settings applied to DOM'); + console.log('[Outcut] Settings applied to DOM'); } function clearDesignFromDOM() { @@ -80,7 +80,7 @@ var themeLink = document.getElementById('outlook-relook-theme'); if (themeLink) themeLink.remove(); - console.log('[Outlook Relook] Design tweaks cleared from DOM'); + console.log('[Outcut] Design tweaks cleared from DOM'); } function injectThemeCSS(theme) { @@ -91,7 +91,7 @@ link.rel = 'stylesheet'; link.href = chrome.runtime.getURL('themes/' + theme + '.css'); document.head.appendChild(link); - console.log('[Outlook Relook] Theme loaded: ' + theme); + console.log('[Outcut] Theme loaded: ' + theme); } function startDesignTweaks(settings) { @@ -101,7 +101,7 @@ OR.Behavior.start(settings); OR.Injector.start(settings); designTweaksActive = true; - console.log('[Outlook Relook] Design tweaks enabled'); + console.log('[Outcut] Design tweaks enabled'); } function stopDesignTweaks() { @@ -110,12 +110,12 @@ OR.Injector.stop(); clearDesignFromDOM(); designTweaksActive = false; - console.log('[Outlook Relook] Design tweaks disabled'); + console.log('[Outcut] Design tweaks disabled'); } async function init() { const settings = await OR.loadSettings(); - console.log('[Outlook Relook] Loaded settings:', settings); + console.log('[Outcut] Loaded settings:', settings); // Keyboard navigation — always starts (gated by its own toggle internally) OR.Keyboard.start(settings); diff --git a/content/injector.js b/content/injector.js index 9d8bc86..8f76938 100644 --- a/content/injector.js +++ b/content/injector.js @@ -60,13 +60,13 @@ window.OutlookRelook.Injector = (function () { for (var j = 0; j < menuItems.length; j++) { if (/mark all as read/i.test(menuItems[j].textContent)) { menuItems[j].click(); - console.log('[Outlook Relook] Marked all as read'); + console.log('[Outcut] Marked all as read'); return; } } // Close context menu if option not found document.body.click(); - console.warn('[Outlook Relook] "Mark all as read" menu item not found'); + console.warn('[Outcut] "Mark all as read" menu item not found'); }, 300); } }; @@ -244,7 +244,7 @@ window.OutlookRelook.Injector = (function () { currentSettings = settings; setupMarkAllRead(); setupFolderJump(); - console.log('[Outlook Relook] Injector started'); + console.log('[Outcut] Injector started'); } function updateSettings(settings) { diff --git a/content/keyboard.js b/content/keyboard.js index c577290..e287ed6 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -1,4 +1,4 @@ -// Outlook Relook — Gmail-Style Keyboard Navigation & Multi-Select +// Outcut — Keyboard Navigation & Multi-Select // Adds keyboard focus cursor and multi-select to OWA's message list. // Gated by the keyboardMultiSelect setting. // @@ -7,6 +7,53 @@ window.OutlookRelook = window.OutlookRelook || {}; +// Keyboard shortcut presets +var KEY_PRESETS = { + gmail: { + nextMessage: [{key: 'j'}, {key: 'ArrowDown'}], + prevMessage: [{key: 'k'}, {key: 'ArrowUp'}], + selectExtendDown: [{key: 'j', shift: true}, {key: 'ArrowDown', shift: true}], + selectExtendUp: [{key: 'k', shift: true}, {key: 'ArrowUp', shift: true}], + toggleSelect: [{key: 'x'}, {key: ' '}], + delete: [{key: '#'}], + archive: [{key: 'e'}], + readUnread: [{key: 'I', shift: true}, {key: 'U', shift: true}], + move: [{key: 'v'}], + flag: [{key: 'f'}], + pin: [{key: 'p'}], + deselect: [{key: 'Escape'}], + open: [{key: 'Enter'}, {key: 'o'}], + label: 'Gmail-style: j/k nav, x select, # del, e archive' + }, + outlook: { + nextMessage: [{key: 'ArrowDown'}, {key: 'j'}], + prevMessage: [{key: 'ArrowUp'}, {key: 'k'}], + selectExtendDown: [{key: 'ArrowDown', shift: true}], + selectExtendUp: [{key: 'ArrowUp', shift: true}], + toggleSelect: [{key: ' '}], + delete: [{key: 'Delete'}, {key: 'Backspace'}], + archive: [{key: 'e'}], + readUnread: [{key: 'q'}, {key: 'I', shift: true}], + move: [{key: 'v'}], + flag: [{key: 'Insert'}, {key: 'f'}], + pin: [{key: 'p'}], + deselect: [{key: 'Escape'}], + open: [{key: 'Enter'}], + label: 'Outlook-style: arrows nav, Space select, Del delete' + } +}; + +function matchesAction(e, actionBindings) { + if (!actionBindings) return false; + for (var i = 0; i < actionBindings.length; i++) { + var b = actionBindings[i]; + if (e.key === b.key && !!e.shiftKey === !!b.shift && !!e.ctrlKey === !!b.ctrl) { + return true; + } + } + return false; +} + window.OutlookRelook.Keyboard = (function () { 'use strict'; @@ -246,7 +293,7 @@ window.OutlookRelook.Keyboard = (function () { performAction(targets, function (target) { var btn = findButton(target, 'Delete'); if (btn) { btn.click(); return true; } - console.warn('[Outlook Relook] Delete button not found'); + console.warn('[Outcut] Delete button not found'); return false; }); } @@ -255,7 +302,7 @@ window.OutlookRelook.Keyboard = (function () { performAction(targets, function (target) { var btn = findButton(target, 'Archive'); if (btn) { btn.click(); return true; } - console.warn('[Outlook Relook] Archive button not found'); + console.warn('[Outcut] Archive button not found'); return false; }); } @@ -288,7 +335,7 @@ window.OutlookRelook.Keyboard = (function () { var btn = findButton(target, 'Flag this message') || findButton(target, 'Flag / Unflag'); if (btn) { btn.click(); return true; } - console.warn('[Outlook Relook] Flag button not found'); + console.warn('[Outcut] Flag button not found'); return false; }); } @@ -297,7 +344,7 @@ window.OutlookRelook.Keyboard = (function () { performAction(targets, function (target) { var btn = findButton(target, 'Pin / Unpin'); if (btn) { btn.click(); return true; } - console.warn('[Outlook Relook] Pin button not found'); + console.warn('[Outcut] Pin button not found'); return false; }); } @@ -332,8 +379,8 @@ window.OutlookRelook.Keyboard = (function () { var items = getMessageItems(); if (items.length === 0) return; - var key = e.key; - var shift = e.shiftKey; + var presetName = currentSettings.keyboardPreset || 'gmail'; + var preset = KEY_PRESETS[presetName] || KEY_PRESETS.gmail; // Initialize focus if not set var currentIdx = getFocusedIndex(items); @@ -356,101 +403,54 @@ window.OutlookRelook.Keyboard = (function () { var handled = true; var targets; - switch (key) { - case 'j': - case 'ArrowDown': - if (shift) { - toggleSelect(items, currentIdx); - if (currentIdx < items.length - 1) { - setFocus(items, currentIdx + 1); - toggleSelect(items, currentIdx + 1); - } - } else { - if (currentIdx < items.length - 1) { - setFocus(items, currentIdx + 1); - } - } - break; - - case 'k': - case 'ArrowUp': - if (shift) { - toggleSelect(items, currentIdx); - if (currentIdx > 0) { - setFocus(items, currentIdx - 1); - toggleSelect(items, currentIdx - 1); - } - } else { - if (currentIdx > 0) { - setFocus(items, currentIdx - 1); - } - } - break; - - case 'x': - case ' ': - e.preventDefault(); - toggleSelect(items, currentIdx); - break; - - case '#': - targets = getActionTargets(items); - actionDelete(targets); - break; - - case 'e': - targets = getActionTargets(items); - actionArchive(targets); - break; - - case 'I': - case 'U': - // Shift+i or Shift+u — OWA uses a single "Read / Unread" toggle - if (shift) { - targets = getActionTargets(items); - actionMarkReadUnread(targets); - } else { - handled = false; - } - break; - - case 'v': - targets = getActionTargets(items); - actionMove(targets); - break; - - case 'f': - // Flag/unflag - targets = getActionTargets(items); - actionFlag(targets); - break; - - case 'p': - // Pin/unpin - targets = getActionTargets(items); - actionPin(targets); - break; - - case 'Escape': - clearSelection(); - break; - - case 'Enter': - case 'o': - if (currentIdx >= 0 && currentIdx < items.length) { - items[currentIdx].click(); - } - break; - - default: - handled = false; + if (matchesAction(e, preset.selectExtendDown)) { + toggleSelect(items, currentIdx); + if (currentIdx < items.length - 1) { + setFocus(items, currentIdx + 1); + toggleSelect(items, currentIdx + 1); + } + } else if (matchesAction(e, preset.selectExtendUp)) { + toggleSelect(items, currentIdx); + if (currentIdx > 0) { + setFocus(items, currentIdx - 1); + toggleSelect(items, currentIdx - 1); + } + } else if (matchesAction(e, preset.nextMessage)) { + if (currentIdx < items.length - 1) setFocus(items, currentIdx + 1); + } else if (matchesAction(e, preset.prevMessage)) { + if (currentIdx > 0) setFocus(items, currentIdx - 1); + } else if (matchesAction(e, preset.toggleSelect)) { + e.preventDefault(); + toggleSelect(items, currentIdx); + } else if (matchesAction(e, preset.delete)) { + targets = getActionTargets(items); + actionDelete(targets); + } else if (matchesAction(e, preset.archive)) { + targets = getActionTargets(items); + actionArchive(targets); + } else if (matchesAction(e, preset.readUnread)) { + targets = getActionTargets(items); + actionMarkReadUnread(targets); + } else if (matchesAction(e, preset.move)) { + targets = getActionTargets(items); + actionMove(targets); + } else if (matchesAction(e, preset.flag)) { + targets = getActionTargets(items); + actionFlag(targets); + } else if (matchesAction(e, preset.pin)) { + targets = getActionTargets(items); + actionPin(targets); + } else if (matchesAction(e, preset.deselect)) { + clearSelection(); + } else if (matchesAction(e, preset.open)) { + if (currentIdx >= 0 && currentIdx < items.length) items[currentIdx].click(); + } else { + handled = false; } if (handled) { e.stopPropagation(); - if (key !== 'Enter' && key !== 'o') { - e.preventDefault(); - } + if (e.key !== 'Enter') e.preventDefault(); } } @@ -497,7 +497,7 @@ window.OutlookRelook.Keyboard = (function () { document.addEventListener('keydown', handleKeydown, true); setupListObserver(); - console.log('[Outlook Relook] Keyboard navigation started'); + console.log('[Outcut] Keyboard navigation started'); cleanupFns.push(function () { document.removeEventListener('keydown', handleKeydown, true); }); @@ -506,9 +506,11 @@ window.OutlookRelook.Keyboard = (function () { function updateSettings(settings) { var wasEnabled = currentSettings.keyboardMultiSelect; var isEnabled = settings.keyboardMultiSelect; + var oldPreset = currentSettings.keyboardPreset; + var newPreset = settings.keyboardPreset; currentSettings = settings; - if (wasEnabled !== isEnabled) { + if (wasEnabled !== isEnabled || oldPreset !== newPreset) { stop(); start(settings); } diff --git a/content/observer.js b/content/observer.js index 608c78d..43282a0 100644 --- a/content/observer.js +++ b/content/observer.js @@ -52,7 +52,7 @@ window.OutlookRelook.Observer = (function () { for (const el of elements) { if (el.style.display !== 'none') { el.style.display = 'none'; - console.log('[Outlook Relook] Suppressed: ' + name, el); + console.log('[Outcut] Suppressed: ' + name, el); } } } @@ -72,7 +72,7 @@ window.OutlookRelook.Observer = (function () { var text = el.textContent || ''; if (pattern.test(text) && text.length < 200) { el.style.display = 'none'; - console.log('[Outlook Relook] Suppressed by text: "' + text.trim().substring(0, 50) + '"', el); + console.log('[Outcut] Suppressed by text: "' + text.trim().substring(0, 50) + '"', el); } } } @@ -105,7 +105,7 @@ window.OutlookRelook.Observer = (function () { subtree: true, }); - console.log('[Outlook Relook] Observer started'); + console.log('[Outcut] Observer started'); } function updateSettings(settings) { @@ -118,7 +118,7 @@ window.OutlookRelook.Observer = (function () { if (observer) { observer.disconnect(); observer = null; - console.log('[Outlook Relook] Observer stopped'); + console.log('[Outcut] Observer stopped'); } } diff --git a/content/settings-defaults.js b/content/settings-defaults.js index 7393a1b..6ae20db 100644 --- a/content/settings-defaults.js +++ b/content/settings-defaults.js @@ -6,6 +6,7 @@ window.OutlookRelook = window.OutlookRelook || {}; window.OutlookRelook.DEFAULTS = { // Keyboard Navigation (primary feature, always visible) keyboardMultiSelect: true, + keyboardPreset: 'gmail', // Design Tweaks (experimental, hidden by default) enableDesignTweaks: false, diff --git a/manifest.json b/manifest.json index 2034aeb..5ae3844 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,10 @@ { "manifest_version": 3, - "name": "Outlook Relook", - "version": "0.1.0", - "description": "Minimalist reskin for Outlook Web App — clean themes, less clutter, more email.", - "permissions": ["storage", "activeTab"], + "name": "Outcut", + "version": "1.0.0", + "description": "Keyboard shortcuts for Outlook — Gmail-style multi-select, delete, archive, and more.", + "homepage_url": "https://github.com/joelbrockcoluminate/outcut", + "permissions": ["storage"], "content_scripts": [ { "matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*", "https://outlook.cloud.microsoft/*"], diff --git a/popup/popup.html b/popup/popup.html index 1e33c96..e987fde 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -7,7 +7,7 @@