Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ dist
.DS_Store
.vscode/*
!.vscode/extensions.json

docs/
24 changes: 24 additions & 0 deletions drag_debug.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3463,3 +3463,27 @@
[2026-07-15T12:00:36.093Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-15T12:00:36.094Z] dragIcon result: isEmpty=false, size={"width":96,"height":96}
[2026-07-15T12:00:36.094Z] calling sender.startDrag with item.file=C:\Users\yadav\Downloads\test_project.py, item.files=["C:\\Users\\yadav\\Downloads\\test_project.py"]
[2026-07-16T07:41:02.483Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:41:03.399Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:41:03.400Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:41:04.268Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:41:04.268Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:41:05.123Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:43:54.491Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:43:55.274Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:43:55.274Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:43:56.022Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:43:56.022Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:43:56.766Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:45:44.275Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:45:44.956Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:45:44.957Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:45:45.606Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:45:45.606Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:45:46.216Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:59:01.036Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:59:01.814Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:59:01.815Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:59:02.458Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
[2026-07-16T07:59:02.459Z] createFileStackDragIcon calling Resvg for count=1
[2026-07-16T07:59:03.131Z] createFileStackDragIcon resvg result: isEmpty=false, size={"width":96,"height":96}
86 changes: 86 additions & 0 deletions electron/main/geometry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
export type StickPosition = 'left' | 'right' | 'top'

export interface DisplayInfo {
id: number
workArea: { x: number; y: number; width: number; height: number }
}

export interface StickBoundsParams {
position: StickPosition
displays: DisplayInfo[]
displayId?: number
windowWidth: number
windowHeight: number
currentBounds?: { x: number; y: number }
}

export interface StickBoundsResult {
x: number
y: number
width: number
height: number
displayId: number
}

export function computeStickBounds(params: StickBoundsParams): StickBoundsResult {
const { position, displays, displayId, windowWidth, windowHeight, currentBounds } = params

let display: DisplayInfo | undefined

if (displayId !== undefined) {
display = displays.find(d => d.id === displayId)
}

if (!display && currentBounds) {
const nearest = findNearestDisplay(displays, currentBounds)
if (nearest) display = nearest
}

if (!display) {
display = displays[0]
}

const wa = display.workArea

let x: number
let y: number
let width = windowWidth
let height: number

switch (position) {
case 'left':
x = wa.x
y = wa.y
height = wa.height
break
case 'right':
x = wa.x + wa.width - windowWidth
y = wa.y
height = wa.height
break
case 'top':
x = wa.x + Math.floor((wa.width - windowWidth) / 2)
y = wa.y
height = Math.min(Math.max(windowHeight, 320), wa.height)
break
}

return { x, y, width, height, displayId: display.id }
}

function findNearestDisplay(displays: DisplayInfo[], point: { x: number; y: number }): DisplayInfo | undefined {
let nearest: DisplayInfo | undefined
let minDist = Infinity
for (const d of displays) {
const cx = d.workArea.x + d.workArea.width / 2
const cy = d.workArea.y + d.workArea.height / 2
const dx = cx - point.x
const dy = cy - point.y
const dist = dx * dx + dy * dy
if (dist < minDist) {
minDist = dist
nearest = d
}
}
return nearest
}
26 changes: 24 additions & 2 deletions electron/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
* renderer calls them through the typed preload bridge, so a signature mismatch
* is a compile-time error rather than a runtime one.
*/
import { app, ipcMain, clipboard, nativeImage } from 'electron'
import { app, ipcMain, clipboard, nativeImage, screen } from 'electron'
import { existsSync } from 'node:fs'
import { execFile } from 'node:child_process'
import { psHost } from './powershell'
import { type InvokeMap, type InvokeChannel, type SendMap, type SendChannel } from '../../shared/ipc'
import { getStore, loadSettings, saveSettings, pushState, addFiles, getWatcher } from './state'
import { getMainWindow } from './window'
import { setInteractive, setHeartbeatPaused, setHotZoneWidth } from './window'
import { setInteractive, setHeartbeatPaused, setHotZoneWidth, repositionWindow } from './window'
import { getOnboardingWindow } from './onboardingWindow'
import { startDragOut, resolveDragData } from './drag'
import { clipboardSignature } from '../clipboard/formats'
Expand Down Expand Up @@ -437,6 +437,9 @@ export function registerIpc(): void {
if (patch.hotZoneWidth !== undefined) {
setHotZoneWidth(patch.hotZoneWidth)
}
if (patch.stickPosition !== undefined || patch.stickDisplayId !== undefined) {
repositionWindow()
}
pushState.settings(next)
return next
})
Expand All @@ -451,6 +454,25 @@ export function registerIpc(): void {
win.minimize()
}
})

handle('displays:list', () => {
const all = screen.getAllDisplays()
const primary = screen.getPrimaryDisplay()
return all.map((d) => {
const rawName = (d as any).label || ''
const name = rawName || (d.id === primary.id ? 'Primary' : `Display #${d.id}`)
return {
id: d.id,
bounds: { x: d.bounds.x, y: d.bounds.y, width: d.bounds.width, height: d.bounds.height },
isPrimary: d.id === primary.id,
label: d.id === primary.id
? `${name} (Primary) ${d.bounds.width}×${d.bounds.height}`
: `${name} ${d.bounds.width}×${d.bounds.height}`,
name: d.id === primary.id ? `${name} (Primary)` : name,
resolution: `${d.bounds.width}×${d.bounds.height}`
}
})
})
}

/**
Expand Down
47 changes: 45 additions & 2 deletions electron/main/tray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
* state (checkmarks) is rebuilt every time the menu opens so it always reflects
* current settings.
*/
import { Menu, Tray, app, nativeImage, Notification } from 'electron'
import { Menu, Tray, app, nativeImage, Notification, screen } from 'electron'
import { existsSync } from 'node:fs'
import { PATHS } from '../store/paths'
import { loadSettings, saveSettings } from '../store/settings'
import { getMainWindow, setVisible } from './window'
import { getMainWindow, setVisible, repositionWindow } from './window'
import type { StickPosition } from '../../shared/types'
import { pushState } from './state'

let tray: Tray | null = null
Expand Down Expand Up @@ -56,6 +57,39 @@ export function createTray(): Tray {
} catch { /* ignore */ }
}

function buildDisplaySubmenu(currentId: number | undefined): Electron.MenuItemConstructorOptions[] {
const all = screen.getAllDisplays()
const primary = screen.getPrimaryDisplay()
return all.map((d) => {
const name = (d as any).label || (d.id === primary.id ? 'Primary' : `Display #${d.id}`)
return {
label: d.id === primary.id
? `${name} (Primary) ${d.bounds.width}×${d.bounds.height}`
: `${name} ${d.bounds.width}×${d.bounds.height}`,
type: 'radio' as const,
checked: currentId === d.id,
click: () => {
const next = saveSettings({ stickDisplayId: d.id })
pushState.settings(next)
repositionWindow()
}
}
})
}

function buildStickSubmenu(current: StickPosition): Electron.MenuItemConstructorOptions[] {
return (['left', 'right', 'top'] as StickPosition[]).map((pos) => ({
label: pos.charAt(0).toUpperCase() + pos.slice(1),
type: 'radio' as const,
checked: current === pos,
click: () => {
const next = saveSettings({ stickPosition: pos })
pushState.settings(next)
repositionWindow()
}
}))
}

const rebuild = () => {
const settings = loadSettings()
const menu = Menu.buildFromTemplate([
Expand Down Expand Up @@ -89,6 +123,15 @@ export function createTray(): Tray {
}
},
{ type: 'separator' },
{
label: 'Stick to',
submenu: buildStickSubmenu(settings.stickPosition)
},
{
label: 'Display',
submenu: buildDisplaySubmenu(settings.stickDisplayId)
},
{ type: 'separator' },
{
label: 'Quit Edge-Drop',
click: () => {
Expand Down
Loading