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
16 changes: 16 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ tauri-plugin-store = "2"
tauri-plugin-dialog = "2"
tauri-plugin-deep-link = "2"
tauri-plugin-os = "2"
tauri-plugin-window-state = "2"
url = "2"
base64 = "0.22"
chrono = "0.4"
Expand Down
9 changes: 9 additions & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default",
"core:window:allow-show",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"core:window:allow-is-maximized",
"core:window:allow-is-fullscreen",
"core:window:allow-start-dragging",
"window-state:default",
"opener:default",
"store:default",
"dialog:default",
Expand Down
13 changes: 13 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub fn run() {
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.manage(app_state.clone())
.manage(store.clone())
.setup(move |app| {
Expand Down Expand Up @@ -160,6 +161,18 @@ pub fn run() {
}
}

// On non-macOS, remove native window decorations BEFORE showing
// the window so there is no flash of the native title bar
#[cfg(not(target_os = "macos"))]
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_decorations(false);
}

// Show the window after full initialization
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
}

Ok(())
})
.invoke_handler(tauri::generate_handler![
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"x": 16,
"y": 18
},
"hiddenTitle": true
"hiddenTitle": true,
"visible": false
}
],
"security": {
Expand Down
89 changes: 89 additions & 0 deletions src/__tests__/useWindowControls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* @jest-environment node
*/
import { shouldReserveMacTrafficLightInset } from '@/composables/useWindowControls'

describe('shouldReserveMacTrafficLightInset', () => {
it('returns true on macOS when not fullscreen', () => {
expect(shouldReserveMacTrafficLightInset(true, false, true)).toBe(true)
})

it('returns false on macOS when fullscreen', () => {
expect(shouldReserveMacTrafficLightInset(true, true, true)).toBe(false)
})

it('returns false on non-macOS even when not fullscreen', () => {
expect(shouldReserveMacTrafficLightInset(false, false, true)).toBe(false)
})

it('returns false on non-macOS fullscreen', () => {
expect(shouldReserveMacTrafficLightInset(false, true, true)).toBe(false)
})

it('returns false in browser (non-desktop) context', () => {
expect(shouldReserveMacTrafficLightInset(true, false, false)).toBe(false)
})
})

describe('useWindowControls', () => {
const mockMinimize = jest.fn()
const mockToggleMaximize = jest.fn()
const mockClose = jest.fn()
const mockIsMaximized = jest.fn()
const mockIsFullscreen = jest.fn()
const mockOnResized = jest.fn()

beforeEach(() => {
jest.resetModules()
jest.mock('@tauri-apps/api/window', () => ({
getCurrentWindow: () => ({
minimize: mockMinimize,
toggleMaximize: mockToggleMaximize,
close: mockClose,
isMaximized: mockIsMaximized,
isFullscreen: mockIsFullscreen,
onResized: mockOnResized,
}),
}))
})

afterEach(() => {
jest.clearAllMocks()
})

it('executes minimize action', async () => {
const { useWindowControls } = await import('@/composables/useWindowControls')
const controls = useWindowControls()
await controls.minimize()
expect(mockMinimize).toHaveBeenCalled()
})

it('executes toggleMaximize action', async () => {
const { useWindowControls } = await import('@/composables/useWindowControls')
const controls = useWindowControls()
await controls.toggleMaximize()
expect(mockToggleMaximize).toHaveBeenCalled()
})

it('executes close action', async () => {
const { useWindowControls } = await import('@/composables/useWindowControls')
const controls = useWindowControls()
await controls.close()
expect(mockClose).toHaveBeenCalled()
})

it('tracks isMaximized and isFullscreen state', async () => {
mockIsMaximized.mockResolvedValue(true)
mockIsFullscreen.mockResolvedValue(false)
mockOnResized.mockResolvedValue(jest.fn())

const { useWindowControls } = await import('@/composables/useWindowControls')
const controls = useWindowControls()

// The composable refreshes state on mount via onMounted.
// Since we're in a test environment outside Vue, onMounted doesn't fire.
// We verify that the default state is sensible.
expect(controls.isMaximized.value).toBe(false)
expect(controls.isFullscreen.value).toBe(false)
})
})
20 changes: 19 additions & 1 deletion src/components/layout/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { usePlatform } from '@/composables/usePlatform'
import { shouldReserveMacTrafficLightInset, useWindowControls } from '@/composables/useWindowControls'
import { useTransferStore } from '@/store/transferStore'
import WindowControls from './WindowControls.vue'

defineProps<{
hideAiButton?: boolean
Expand All @@ -15,10 +19,24 @@ const emit = defineEmits<{

const transferStore = useTransferStore()
const { taskCount } = storeToRefs(transferStore)

const windowControls = useWindowControls()
const { isMac, platformReady } = usePlatform()

const trafficLightInset = computed(() => {
if (!platformReady.value)
return '78px'
return shouldReserveMacTrafficLightInset(isMac.value, windowControls.isFullscreen.value, true) ? '78px' : '0px'
})
</script>

<template>
<div class="pl-[90px] pr-2 border-b bg-muted/30 flex shrink-0 gap-1 h-10 items-center overflow-hidden" data-tauri-drag-region>
<div
class="pr-2 border-b bg-muted/30 flex shrink-0 gap-1 h-10 items-center overflow-hidden"
:style="{ paddingLeft: trafficLightInset }"
data-tauri-drag-region
>
<WindowControls />
<span class="text-xs text-muted-foreground font-semibold select-none">SqlKit</span>
<div class="flex-1" data-tauri-drag-region />

Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const { t } = useI18n()
<template>
<aside class="border-r bg-background flex flex-shrink-0 flex-col h-full w-16 z-50">
<!-- Logo -->
<div class="border-b flex h-14 items-center justify-center">
<div class="border-b flex h-14 items-center justify-center" data-tauri-drag-region>
<div class="text-lg text-white font-bold rounded-md bg-primary flex h-8 w-8 items-center justify-center">
S
</div>
Expand Down
41 changes: 41 additions & 0 deletions src/components/layout/WindowControls.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { usePlatform } from '@/composables/usePlatform'
import { useWindowControls } from '@/composables/useWindowControls'

const { isMaximized, minimize, toggleMaximize, close } = useWindowControls()
const { isMac, platformReady } = usePlatform()
</script>

<template>
<div v-if="platformReady && !isMac" class="mr-2 flex items-stretch">
<button
class="inline-flex h-10 w-11 transition-colors items-center justify-center hover:bg-foreground/10"
@click="minimize"
>
<svg width="12" height="12" viewBox="0 0 12 12">
<rect x="2" y="5.5" width="8" height="1" fill="currentColor" />
</svg>
</button>
<button
class="inline-flex h-10 w-11 transition-colors items-center justify-center hover:bg-foreground/10"
@click="toggleMaximize"
>
<svg v-if="isMaximized" width="12" height="12" viewBox="0 0 12 12">
<rect x="1.5" y="3.5" width="7" height="7" rx="0.5" fill="none" stroke="currentColor" stroke-width="1" />
<rect x="3.5" y="1.5" width="7" height="7" rx="0.5" fill="none" stroke="currentColor" stroke-width="1" />
</svg>
<svg v-else width="12" height="12" viewBox="0 0 12 12">
<rect x="1.5" y="1.5" width="9" height="9" rx="1" fill="none" stroke="currentColor" stroke-width="1" />
</svg>
</button>
<button
class="inline-flex h-10 w-11 transition-colors items-center justify-center hover:text-white hover:bg-red-500"
@click="close"
>
<svg width="12" height="12" viewBox="0 0 12 12">
<line x1="2" y1="2" x2="10" y2="10" stroke="currentColor" stroke-width="1.2" />
<line x1="10" y1="2" x2="2" y2="10" stroke="currentColor" stroke-width="1.2" />
</svg>
</button>
</div>
</template>
1 change: 1 addition & 0 deletions src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { useDataGridSelection } from './useDataGridSelection'
export { useDataGridSort } from './useDataGridSort'
export { useDataStudioChatAgent } from './useDataStudioChatAgent'
export { formatSql, resolveDialect, useSqlFormatter } from './useSqlFormatter'
export { shouldReserveMacTrafficLightInset, useWindowControls } from './useWindowControls'
5 changes: 5 additions & 0 deletions src/composables/usePlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { platform } from '@tauri-apps/plugin-os'
import { computed, ref } from 'vue'

const platformCache = ref<string | null>(null)
const platformReady = ref(false)

async function getPlatform(): Promise<string> {
if (platformCache.value) {
Expand All @@ -10,9 +11,12 @@ async function getPlatform(): Promise<string> {
try {
const p = await platform()
platformCache.value = p
platformReady.value = true
return p
}
catch {
platformCache.value = 'unknown'
platformReady.value = true
return 'unknown'
}
}
Expand Down Expand Up @@ -42,6 +46,7 @@ function usePlatform() {
modifierKey,
altKey,
platform: platformCache,
platformReady,
}
}

Expand Down
55 changes: 55 additions & 0 deletions src/composables/useWindowControls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getCurrentWindow } from '@tauri-apps/api/window'
import { onMounted, onUnmounted, ref } from 'vue'

export function shouldReserveMacTrafficLightInset(
isMac: boolean,
isFullscreen: boolean,
isDesktop = true,
): boolean {
return isDesktop && isMac && !isFullscreen
}

export function useWindowControls() {
const isMaximized = ref(false)
const isFullscreen = ref(false)

let unlistenResize: (() => void) | null = null

async function refreshState() {
try {
const window = getCurrentWindow()
const [maximized, fullscreen] = await Promise.all([
window.isMaximized(),
window.isFullscreen(),
])
isMaximized.value = maximized
isFullscreen.value = fullscreen
}
catch {
// Not in Tauri environment — leave defaults
}
}

const minimize = () => getCurrentWindow().minimize()
const toggleMaximize = () => getCurrentWindow().toggleMaximize()
const close = () => getCurrentWindow().close()

onMounted(async () => {
await refreshState()
try {
const unlisten = await getCurrentWindow().onResized(() => {
refreshState()
})
unlistenResize = unlisten
}
catch {
// Not in Tauri environment
}
})

onUnmounted(() => {
unlistenResize?.()
})

return { isMaximized, isFullscreen, minimize, toggleMaximize, close }
}
Loading