diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ba08794
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+
+High level:
+* Tabs are hidden
+* Escape shows tabs page
+* Escape again shows groups page
+* Escape view hides url bar (true fullscreen)?
+
+Implementation
+* Content-script for * catches ESC, sends to background
+* Background catches ESC, switches views (or does nothing, or help page?)
+* Content page displays a grid of tabs, or of groups
+
+TODO
+* groups page shows windows as groups?
+
+Handling home page
+* Create new tab for it
+* OR use it like about:home, navigate away from it on url click
+* Hide from visible tabs in home page
+* ESC always goes back to it
+* Always hide it, in whatever group it is
+
+What happens when...
+
+* User closes a tab
+* User closes the window
+* User goes fullscreen (from Firefox)
+* User goes fullscreen (from web API)
+
+How to...
+
+* Open a new tab from home page? Have a URL entry bar there?
diff --git a/api.js b/api.js
new file mode 100644
index 0000000..e6a0997
--- /dev/null
+++ b/api.js
@@ -0,0 +1,5 @@
+// browser (Firefox) vs chrome (Chromium)
+const isChrome = typeof chrome != 'undefined';
+const api = isChrome ? chrome : browser;
+
+export default api;
diff --git a/background.js b/background.js
new file mode 100644
index 0000000..a9c5ef8
--- /dev/null
+++ b/background.js
@@ -0,0 +1,108 @@
+console.log('background:start');
+
+import api from './api.js';
+
+const homeURL = api.runtime.getURL('home.html');
+const homeTitle = 'Extension: (Escape) - ESCAPE';
+
+/*
+"normal"
+"popup"
+"panel"
+"detached_panel"
+*/
+//const windowType = 'normal';
+//const windowType = 'popup';
+//const windowType = 'panel';
+const windowType = 'detached_panel';
+
+let homeTabId = null;
+let port = null;
+
+// home page first load opens the connection
+api.runtime.onConnect.addListener(p => {
+ console.log('port connected');
+ port = p;
+});
+
+// Listen for ESC from web content
+function escapeHandler(message) {
+ if (message.ESC === 1) {
+ api.tabs.query({url: homeURL}).then(tabs => {
+ if (tabs.length) {
+ api.tabs.update(tabs[0].id, { active: true });
+ port.postMessage({show: 'tabs'});
+ }
+ // Otherwise open in a new tab
+ else {
+ api.tabs.create({url:homeURL}).then((tab) => {
+ // We don't want to sync this URL ever nor clutter the users history
+ api.history.deleteUrl({url:homeURL});
+ }).catch((e) => { throw e });
+ }
+ });
+ }
+}
+api.runtime.onMessage.addListener(escapeHandler);
+
+api.commands.onCommand.addListener((command) => {
+ console.log('onCommand', command);
+ if (command === 'open-escape') {
+ openEscapeWindow();
+ }
+});
+
+const escapeWindowIsOpen = async () => {
+ if (homeTabId != null) {
+ return true;
+ }
+ else {
+ const ew = await findEscapeWindow();
+ return ew || false;
+ }
+};
+
+const findEscapeWindow = async () => {
+ const ws = await api.windows.getAll({ populate: false, windowTypes: ['popup'] });
+ return ws.find(wi => wi.title == homeTitle);
+};
+
+const createEscapeWindow = async () => {
+ const opts = {
+ url: homeURL,
+ type: windowType
+ };
+
+ const onLoadingComplete = async (tabId) => {
+ homeTabId = tabId;
+ };
+
+ api.tabs.onUpdated.addListener((tabId, changeInfo, tabInfo) => {
+ if (changeInfo.status === 'complete') {
+ onLoadingComplete(tabInfo.id);
+ }
+ }, { properties: ["status"] });
+
+ await api.windows.create(opts);
+};
+
+const openEscapeWindow = async () => {
+ if (await escapeWindowIsOpen()) {
+ return;
+ }
+ createEscapeWindow();
+};
+
+await openEscapeWindow();
+
+/*
+setTimeout(async () => {
+ const isOpen = await escapeWindowIsOpen();
+ console.log('OPEN?', isOpen);
+
+ const w = await findEscapeWindow();
+ console.log('W?', w);
+}, 1000);
+*/
+
+console.log('background:end');
diff --git a/content-script.js b/content-script.js
new file mode 100644
index 0000000..3650d9f
--- /dev/null
+++ b/content-script.js
@@ -0,0 +1,12 @@
+document.onkeydown = function(evt) {
+ evt = evt || window.event;
+ var isEscape = false;
+ if ("key" in evt) {
+ isEscape = (evt.key == "Escape" || evt.key == "Esc");
+ } else {
+ isEscape = (evt.keyCode == 27);
+ }
+ if (isEscape) {
+ browser.runtime.sendMessage({"ESC": 1});
+ }
+};
diff --git a/home.css b/home.css
new file mode 100644
index 0000000..81f6bd6
--- /dev/null
+++ b/home.css
@@ -0,0 +1,93 @@
+.controls {
+}
+
+.controls.tabsview {
+ display: none;
+}
+
+html {
+ background: #f5f7f8;
+ font-family: 'Roboto', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ padding: 20px 0;
+}
+
+.cards {
+ width: 90%;
+ max-width: 1240px;
+ margin: 0 auto;
+
+ display: grid;
+
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ grid-gap: 20px;
+}
+
+@media only screen and (min-width: 500px) {
+ .cards {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+@media only screen and (min-width: 850px) {
+ .cards {
+ grid-template-columns: 1fr 1fr 1fr 1fr;
+ }
+}
+
+/* card */
+
+.card {
+ min-height: 100%;
+ background: white;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
+ display: flex;
+ flex-direction: column;
+ text-decoration: none;
+ color: #444;
+ position: relative;
+ top: 0;
+ transition: all .1s ease-in;
+}
+
+.card:hover {
+ top: -2px;
+ box-shadow: 0 4px 5px rgba(0,0,0,0.2);
+}
+
+.card article {
+ padding: 20px;
+ display: flex;
+
+ flex: 1;
+ justify-content: space-between;
+ flex-direction: column;
+
+}
+.card .thumb {
+ padding-bottom: 60%;
+ background-size: cover;
+ background-position: center center;
+}
+
+.card p {
+ flex: 1; /* make p grow to fill available space*/
+ line-height: 1.4;
+}
+
+/* typography */
+h1 {
+ font-size: 20px;
+ margin: 0;
+ color: #333;
+}
+
+.card span {
+ font-size: 12px;
+ font-weight: bold;
+ color: #999;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ margin: 2em 0 0 0;
+}
diff --git a/home.html b/home.html
new file mode 100644
index 0000000..5c93cbf
--- /dev/null
+++ b/home.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ ESCAPE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/home.js b/home.js
new file mode 100644
index 0000000..79f8fe6
--- /dev/null
+++ b/home.js
@@ -0,0 +1,314 @@
+/*
+
+ api.sessions.setTabValue(currentTabId, key, value);
+ await api.sessions.getTabValue(currentTabId, key);
+
+ init groups
+ * there's always a default group (no group property on the tab)
+ * (OR should default group be all tabs)
+ * otherwise get group property from the tab
+
+ managing storage
+ * scaling to lots of tabs/groups
+ * collect all on startup
+ * manage cache
+ * what can change?
+ * new tab in group
+ * tab changes groups
+ * tab closed (no action necessary)
+ * undo close tab to a group that no longer exists (use current group)
+
+ relevant tab events
+ * new
+ * set to current group
+ * activate
+ * close
+
+*/
+
+// browser (Firefox) vs chrome (Chromium)
+//const isChrome = typeof chrome != 'undefined';
+//const api = isChrome ? chrome : browser;
+import api from './api.js';
+
+const VIEW_GROUPS = 1;
+const VIEW_TABS = 2;
+
+// keys for extension-level data
+const EXT_DATA_CONFIG_KEY = 'config';
+
+// keys for per-tab data
+const TAB_DATA_GROUP_KEY = 'groupId';
+
+// default strings
+// TODO: move to i18n
+const DEFAULT_GROUP_TITLE = 'Default Group';
+const DEFAULT_NEW_GROUP_TITLE = 'New Group';
+
+// Handle ESC
+document.onkeydown = function(evt) {
+ evt = evt || window.event;
+ var isEscape = evt.key == 'Escape';
+ if (isEscape && currentView == VIEW_TABS) {
+ showGroups();
+ }
+};
+
+var currentView = null;
+
+var config = null;
+
+//await api.storage.local.remove(EXT_DATA_CONFIG_KEY);
+
+// Open port to background, so we can listen
+// for when we need to show tabs (user hits ESC on web page)
+var port = api.runtime.connect({name: 'background'});
+port.onMessage.addListener(function(msg) {
+ if (msg.show && msg.show == 'tabs') {
+ showTabs();
+ }
+});
+
+// Kick out the jams
+document.addEventListener('DOMContentLoaded', async () => {
+ // Initialize storage - this loads groups, everything else
+ await loadStorage();
+
+ // New group click handler
+ document.querySelector('.newgroup').addEventListener('click', async () => {
+ await newGroup();
+ });
+});
+
+const loadStorage = async () => {
+
+ const data = await api.storage.local.get(EXT_DATA_CONFIG_KEY);
+ console.log('storage', data);
+
+ // Not first run!
+ if (data.config) {
+ config = data.config;
+ }
+ else {
+ // First run!
+ console.log('first run');
+
+ config = {};
+
+ // Set up group storage
+ config.groups = {};
+
+ // Create default group
+ var id = await newGroup(DEFAULT_GROUP_TITLE);
+
+ // Storage default group id as last active group
+ config.lastGroupId = id;
+
+ //
+ await updateStorage();
+ }
+
+ // Pull data from each tab about which group they belong to
+ initializeGroupData().then(function() {
+ // Data loaded, start building UI
+ populateUI();
+ });
+}
+
+// Save changes to config to persistent storage
+// TODO: Temporary hack. Replace with proper evented solution.
+const updateStorage = async () => {
+ await api.storage.local.set({ config });
+ console.log('wrote', config);
+ const data = await api.storage.local.get(EXT_DATA_CONFIG_KEY);
+ console.log('data', data);
+}
+
+// On DOM
+function populateUI() {
+ // Populate groups with their tabs
+ initializeGroupData().then(function() {
+ showTabs();
+ });
+
+ // Listen for things that'll change our state
+ initTabEventListeners();
+}
+
+function initTabEventListeners() {
+ // add new tabs in this window to current group
+ api.tabs.onCreated.addListener(tab => {
+ addTabToGroup(tab.id, config.lastGroupId);
+ });
+
+ // remove detached tabs from their group
+ api.tabs.onDetached.addListener(tab => {
+ removeTabFromGroup(tab.id, config.lastGroupId);
+ });
+
+ // remove removed tabs from their group
+ api.tabs.onRemoved.addListener(tab => {
+ removeTabFromGroup(tab.id, config.lastGroupId);
+ });
+}
+
+function addTabToGroup(tabId, groupId) {
+ var index = config.groups[groupId].tabs.indexOf(tabId);
+ if (index == -1) {
+ config.groups[groupId].tabs.push(tabId);
+ api.sessions.setTabValue(tabId, TAB_DATA_GROUP_KEY, groupId);
+ }
+}
+
+function removeTabFromGroup(tabId, groupId) {
+ var index = config.groups[groupId].tabs.indexOf(tabId);
+ if (index > -1) {
+ config.groups[groupId].tabs.splice(index, 1);
+ api.sessions.remove(tab.id, TAB_DATA_GROUP_KEY);
+ }
+}
+
+function getGroupById(id) {
+ return config.groups[id];
+}
+
+function activateGroup(groupId) {
+ const group = getGroupById(groupId);
+ const lastGroup = getGroupById(config.lastGroupId);
+
+ /*
+
+ currentWindowTabs = api.tabs.query({currentWindow: true});
+ newGroupTabCount
+ currentWindowTabCount
+ const tabCountDifference = Math.abs(newGroupTabCount - currentWindowTabCount);
+ if (tabCountDifference > 0) {
+ // close last
+ }
+
+ */
+
+ if (group.tabs.length === 0) {
+ // New group?
+ api.tabs.create({}).then(tab => {
+ addTabToGroup(tab.id, groupId);
+ //api.tabshideshow.show(group.tabs);
+ //api.tabshideshow.hide(lastGroup.tabs);
+ setLastActiveGroupId(groupId);
+ });
+ }
+ else {
+ //api.tabshideshow.show(group.tabs);
+ //api.tabshideshow.hide(lastGroup.tabs);
+ setLastActiveGroupId(groupId);
+ }
+}
+
+function getLastActiveGroupId() {
+ return config.lastGroupId;
+}
+
+function setLastActiveGroupId(id) {
+ config.lastGroupId = id;
+}
+
+function initializeGroupData() {
+ return new Promise(function(resolve, reject) {
+ // Clear out old tab ids
+ for (let id in config.groups) {
+ config.groups[id].tabs = [];
+ }
+
+ api.tabs.query({currentWindow: true}).then(tabs => {
+ tabs.forEach(tab => {
+ api.sessions.getTabValue(tab.id, TAB_DATA_GROUP_KEY).then(groupId => {
+ if (groupId && config.groups[groupId]) {
+ config.groups[groupId].tabs.push(tab.id);
+ }
+ else {
+ // This should only happen on first run.
+ // Add all default tabs to default group.
+ var groupId = getLastActiveGroupId();
+ addTabToGroup(tab.id, groupId);
+ }
+ });
+ });
+ resolve();
+ });
+ });
+}
+
+const newGroup = async (title) => {
+ console.log('newGroup', title);
+ var id = window.crypto.getRandomValues(new Uint32Array(1))[0];
+ var group = {
+ id: id,
+ title: title || 'New Group',
+ tabs: []
+ };
+ config.groups[id] = group;
+ showGroups();
+ await updateStorage();
+ return id;
+}
+
+function showGroups() {
+ clearCards();
+ for (let id in config.groups) {
+ var group = config.groups[id];
+ var card = addCard();
+ card.querySelector('h1').innerText = group.title;
+ card.dataset.id = group.id;
+ card.addEventListener('click', e => {
+ var groupId = parseInt(card.dataset.id);
+ if (groupId != config.lastGroupId) {
+ activateGroup(groupId);
+ }
+ });
+ card.classList.add('group');
+ }
+ currentView = VIEW_GROUPS;
+ document.querySelector('.controls').classList.add('groupsview');
+ document.querySelector('.controls').classList.remove('tabsview');
+}
+
+function showTabs() {
+ var group = getGroupById(config.lastGroupId);
+ clearCards();
+ api.tabs.query({currentWindow: true}).then(function(tabs) {
+ tabs.forEach(tab => {
+ if (group.tabs.indexOf(tab.id) == -1) {
+ return;
+ }
+ var card = addCard();
+ card.querySelector('h1').innerText = tab.title;
+ card.dataset.id = tab.id;
+ card.addEventListener('click', e => {
+ var tabId = parseInt(card.dataset.id);
+ api.tabs.update(tabId, { active: true });
+ });
+ });
+ });
+ currentView = VIEW_TABS;
+ document.querySelector('.controls').classList.add('tabsview');
+ document.querySelector('.controls').classList.remove('groupsview');
+}
+
+function clearCards() {
+ var container = document.querySelector('.cards');
+ Array.prototype.slice.call(container.children).forEach(child => {
+ child.parentNode.removeChild(child);
+ });
+}
+
+function addCard() {
+ var container = document.querySelector('.cards');
+
+ var cardTpl = document.querySelector('.tpl-card')
+ var cardClone = document.importNode(cardTpl.content, true);
+ container.appendChild(cardClone);
+ var card = container.lastElementChild;
+ card.classList.add('card');
+
+ return card;
+}
diff --git a/icons/icon.png b/icons/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..b35102aa985feeb50878177c7cc0467c5eaaa86c
GIT binary patch
literal 3342
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
literal 0
HcmV?d00001
diff --git a/icons/icon@2x.png b/icons/icon@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5acdb195e5cc2e16ef7172052effbd65333b603
GIT binary patch
literal 8382
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