Skip to content

Commit 0f59832

Browse files
committed
Fix issue #14: Display the Spaces windows on the appropriate, currently-active display.
1 parent 25a3b27 commit 0f59832

File tree

5 files changed

+184
-42
lines changed

5 files changed

+184
-42
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [1.1.6] - 2025-??-??
6+
7+
### Changes
8+
9+
- Fixed [issue #14](https://github.com/codedread/spaces/issues/14): Open Spaces windows on the currently-active display.
10+
- Increased unit test coverage from 8.11% to 8.72%.
11+
12+
513
## [1.1.5] - 2025-09-08
614

715
### Fixes

js/background/background.js

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
/* spaces
66
* Copyright (C) 2015 Dean Oemcke
7+
* Copyright (C) 2025 Jeff Schiller (Codedread)
78
*/
89

910
import { dbService } from './dbService.js';
@@ -540,18 +541,18 @@ async function showSpacesOpenWindow(windowId, editMode) {
540541

541542
// otherwise re-create it
542543
} else {
543-
// TODO(codedread): Handle multiple displays and errors.
544-
const displays = await chrome.system.display.getInfo();
545-
let screen = displays[0].bounds;
546-
const window = await chrome.windows.create(
547-
{
548-
type: 'popup',
549-
url,
550-
height: screen.height - 100,
551-
width: Math.min(screen.width, 1000),
552-
top: 0,
553-
left: 0,
554-
});
544+
// Display on the left-hand side of the appropriate display.
545+
const workArea = await getTargetDisplayWorkArea();
546+
const windowHeight = Math.round(workArea.height * 0.9);
547+
const windowWidth = Math.min(workArea.width - 100, 1000);
548+
const window = await chrome.windows.create({
549+
type: 'popup',
550+
url,
551+
height: windowHeight,
552+
width: windowWidth,
553+
top: workArea.top,
554+
left: workArea.left,
555+
});
555556
spacesOpenWindowId = window.id;
556557
await chrome.storage.local.set({spacesOpenWindowId: window.id});
557558
}
@@ -618,20 +619,19 @@ async function createOrShowSpacesPopupWindow(action, tabUrl) {
618619

619620
// otherwise create it
620621
} else {
621-
// TODO(codedread): Handle multiple displays and errors.
622-
const displays = await chrome.system.display.getInfo();
623-
let screen = displays[0].bounds;
624-
625-
const window = await chrome.windows.create(
626-
{
627-
type: 'popup',
628-
url: popupUrl,
629-
focused: true,
630-
height: 450,
631-
width: 310,
632-
top: screen.height - 450,
633-
left: screen.width - 310,
634-
});
622+
// Display in the lower-right corner of the appropriate display.
623+
const workArea = await getTargetDisplayWorkArea();
624+
const popupHeight = 450;
625+
const popupWidth = 310;
626+
const window = await chrome.windows.create({
627+
type: 'popup',
628+
url: popupUrl,
629+
focused: true,
630+
height: popupHeight,
631+
width: popupWidth,
632+
top: Math.round(workArea.top + workArea.height - popupHeight),
633+
left: Math.round(workArea.left + workArea.width - popupWidth),
634+
});
635635
spacesPopupWindowId = window.id;
636636
await chrome.storage.local.set({spacesPopupWindowId: window.id});
637637
}
@@ -863,18 +863,17 @@ async function handleLoadSession(sessionId, tabUrl) {
863863
return curTab.url;
864864
});
865865

866-
// TODO(codedread): Handle multiple displays and errors.
867-
const displays = await chrome.system.display.getInfo();
868-
let screen = displays[0].bounds;
869-
870-
const newWindow = await chrome.windows.create(
871-
{
872-
url: urls,
873-
height: screen.height - 100,
874-
width: screen.width - 100,
875-
top: 0,
876-
left: 0,
877-
});
866+
// Display new session over most of the appropriate display.
867+
const workArea = await getTargetDisplayWorkArea();
868+
const windowHeight = workArea.height - 100;
869+
const windowWidth = workArea.width - 100;
870+
const newWindow = await chrome.windows.create({
871+
url: urls,
872+
height: windowHeight,
873+
width: windowWidth,
874+
top: workArea.top,
875+
left: workArea.left,
876+
});
878877

879878
// force match this new window to the session
880879
await spacesService.matchSessionToWindow(session, newWindow);
@@ -1255,5 +1254,36 @@ function moveTabToWindow(tab, windowId) {
12551254
spacesService.queueWindowEvent(windowId);
12561255
}
12571256

1257+
/**
1258+
* Determines the most appropriate display to show a new window on.
1259+
* It prefers the display containing the currently focused Chrome window.
1260+
* If no window is focused, it falls back to the primary display.
1261+
* @returns {Promise<chrome.system.display.Bounds>} A promise that resolves to the work area bounds of the target display.
1262+
*/
1263+
async function getTargetDisplayWorkArea() {
1264+
const [displays, currentWindow] = await Promise.all([
1265+
chrome.system.display.getInfo(),
1266+
chrome.windows.getCurrent().catch(() => null) // Catch if no window is focused
1267+
]);
1268+
1269+
let targetDisplay = displays.find(d => d.isPrimary) || displays[0]; // Default to primary
1270+
1271+
// Find the display that contains the center of the current window
1272+
if (currentWindow) {
1273+
const windowCenterX = currentWindow.left + currentWindow.width / 2;
1274+
const windowCenterY = currentWindow.top + currentWindow.height / 2;
1275+
const activeDisplay = displays.find(display => {
1276+
const d = display.workArea;
1277+
return windowCenterX >= d.left && windowCenterX < (d.left + d.width) &&
1278+
windowCenterY >= d.top && windowCenterY < (d.top + d.height);
1279+
});
1280+
if (activeDisplay) {
1281+
targetDisplay = activeDisplay;
1282+
}
1283+
}
1284+
1285+
return targetDisplay.workArea;
1286+
}
1287+
12581288
// Exports for testing.
1259-
export { cleanParameter };
1289+
export { cleanParameter, getTargetDisplayWorkArea };

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "Spaces",
33
"description": "Intuitive tab management",
4-
"version": "1.1.5",
4+
"version": "1.1.6.0",
55
"permissions": [
66
"contextMenus",
77
"favicon",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { getTargetDisplayWorkArea } from '../js/background/background.js';
2+
import { setupChromeMocks, jest } from './helpers.js';
3+
4+
setupChromeMocks();
5+
6+
// Mock data for displays
7+
const mockDisplays = [
8+
{
9+
id: 'primary-display',
10+
isPrimary: true,
11+
workArea: { top: 24, left: 0, width: 1920, height: 1056 },
12+
bounds: { top: 0, left: 0, width: 1920, height: 1080 },
13+
},
14+
{
15+
id: 'secondary-display',
16+
isPrimary: false,
17+
workArea: { top: 0, left: 1920, width: 1280, height: 800 },
18+
bounds: { top: 0, left: 1920, width: 1280, height: 800 },
19+
},
20+
];
21+
22+
// Mock data for windows positioned on different displays
23+
const mockWindowOnPrimary = {
24+
id: 101,
25+
left: 100,
26+
top: 100,
27+
width: 800,
28+
height: 600,
29+
}; // Center: (500, 400) -> on primary
30+
31+
const mockWindowOnSecondary = {
32+
id: 102,
33+
left: 2000,
34+
top: 100,
35+
width: 800,
36+
height: 600,
37+
}; // Center: (2400, 400) -> on secondary
38+
39+
describe('getTargetDisplayWorkArea', () => {
40+
beforeEach(() => {
41+
// Clear all mocks before each test to ensure isolation
42+
jest.clearAllMocks();
43+
});
44+
45+
test('should return primary display work area when no window is focused', async () => {
46+
chrome.system.display.getInfo.mockResolvedValue(mockDisplays);
47+
chrome.windows.getCurrent.mockRejectedValue(new Error('No focused window'));
48+
49+
const workArea = await getTargetDisplayWorkArea();
50+
51+
expect(workArea).toEqual(mockDisplays[0].workArea);
52+
expect(chrome.system.display.getInfo).toHaveBeenCalledTimes(1);
53+
expect(chrome.windows.getCurrent).toHaveBeenCalledTimes(1);
54+
});
55+
56+
test('should return primary display work area when focused window is on primary display', async () => {
57+
chrome.system.display.getInfo.mockResolvedValue(mockDisplays);
58+
chrome.windows.getCurrent.mockResolvedValue(mockWindowOnPrimary);
59+
60+
const workArea = await getTargetDisplayWorkArea();
61+
62+
expect(workArea).toEqual(mockDisplays[0].workArea);
63+
});
64+
65+
test('should return secondary display work area when focused window is on secondary display', async () => {
66+
chrome.system.display.getInfo.mockResolvedValue(mockDisplays);
67+
chrome.windows.getCurrent.mockResolvedValue(mockWindowOnSecondary);
68+
69+
const workArea = await getTargetDisplayWorkArea();
70+
71+
expect(workArea).toEqual(mockDisplays[1].workArea);
72+
});
73+
74+
test('should return the only display work area in a single-monitor setup', async () => {
75+
const singleDisplay = [mockDisplays[0]];
76+
chrome.system.display.getInfo.mockResolvedValue(singleDisplay);
77+
chrome.windows.getCurrent.mockResolvedValue(mockWindowOnPrimary);
78+
79+
const workArea = await getTargetDisplayWorkArea();
80+
81+
expect(workArea).toEqual(singleDisplay[0].workArea);
82+
});
83+
84+
test('should fall back to primary display if focused window is not on any known display', async () => {
85+
const windowOffScreen = {
86+
id: 103,
87+
left: -5000,
88+
top: -5000,
89+
width: 800,
90+
height: 600,
91+
};
92+
chrome.system.display.getInfo.mockResolvedValue(mockDisplays);
93+
chrome.windows.getCurrent.mockResolvedValue(windowOffScreen);
94+
95+
const workArea = await getTargetDisplayWorkArea();
96+
97+
expect(workArea).toEqual(mockDisplays[0].workArea);
98+
});
99+
});

tests/helpers.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ export const setupChromeMocks = () => {
1515
runtime: {
1616
sendMessage: jest.fn(),
1717
},
18-
windows: {
19-
getCurrent: jest.fn(),
18+
system: {
19+
display: {
20+
getInfo: jest.fn(),
21+
},
2022
},
2123
tabs: {
2224
query: jest.fn(),
25+
},
26+
windows: {
27+
getCurrent: jest.fn(),
2328
}
2429
};
2530
};

0 commit comments

Comments
 (0)