Compare commits

...

6 Commits

Author SHA1 Message Date
Gregory Schier
7b78fac24e Fix native titlebar. Get menu ready for native Mac menus 2025-12-05 15:14:13 -08:00
Gregory Schier
6534b3f622 Debug Window 2025-12-05 17:37:54 -08:00
Gregory Schier
daba21fbca Couple small fixes for Windows 2025-12-05 10:18:21 -08:00
Gregory Schier
3b99ea1cad Try fixing titlebar thing for Windows 2025-12-05 10:03:54 -08:00
Gregory Schier
937d7aa72a Fix Git invokation on Windows (#313) 2025-12-05 09:22:34 -08:00
Gregory Schier
5bf7278479 Add setting to use native window titlebar (#312) 2025-12-05 09:15:48 -08:00
18 changed files with 170 additions and 59 deletions

View File

@@ -7,7 +7,7 @@
"scripts": {
"build": "run-s build:*",
"build:1-build": "yaakcli build",
"build:2-cpywasm": "cp \"../../node_modules/@1password/sdk-core/nodejs/core_bg.wasm\" build/",
"build:2-cpywasm": "cpx \"../../node_modules/@1password/sdk-core/nodejs/core_bg.wasm\" build/",
"dev": "yaakcli dev"
},
"dependencies": {

View File

@@ -7,7 +7,7 @@ if (version.startsWith('wasm-pack ')) {
}
console.log('Installing wasm-pack via cargo...');
execSync('cargo install wasm-pack', { stdio: 'inherit' });
execSync('cargo install wasm-pack --locked', { stdio: 'inherit' });
function tryExecSync(cmd) {
try {

View File

@@ -31,6 +31,7 @@ use tokio::time;
use yaak_common::window::WorkspaceWindowTrait;
use yaak_grpc::manager::GrpcHandle;
use yaak_grpc::{Code, ServiceDefinition, serialize_message};
use yaak_mac_window::AppHandleMacWindowExt;
use yaak_models::models::{
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, Plugin, Workspace,
@@ -1321,7 +1322,13 @@ pub fn run() {
}))
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
// Don't restore StateFlags::DECORATIONS because we want to be able to toggle them on/off on a restart
// We could* make this work if we toggled them in the frontend before the window closes, but, this is nicer.
.plugin(
tauri_plugin_window_state::Builder::new()
.with_state_flags(StateFlags::all() - StateFlags::DECORATIONS)
.build(),
)
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
@@ -1391,6 +1398,10 @@ pub fn run() {
let grpc_handle = GrpcHandle::new(&app.app_handle());
app.manage(Mutex::new(grpc_handle));
// Specific settings
let settings = app.db().get_settings();
app.app_handle().set_native_titlebar(settings.use_native_titlebar);
monitor_plugin_events(&app.app_handle().clone());
Ok(())

View File

@@ -1,11 +1,10 @@
use std::time::SystemTime;
use crate::error::Result;
use crate::history::get_or_upsert_launch_info;
use chrono::{DateTime, Utc};
use log::{debug, info};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
use ts_rs::TS;
use yaak_common::api_client::yaak_api_client;
@@ -21,7 +20,7 @@ const KV_KEY: &str = "seen";
// Create updater struct
pub struct YaakNotifier {
last_check: SystemTime,
last_check: Option<Instant>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
@@ -47,9 +46,7 @@ pub struct YaakNotificationAction {
impl YaakNotifier {
pub fn new() -> Self {
Self {
last_check: SystemTime::UNIX_EPOCH,
}
Self { last_check: None }
}
pub async fn seen<R: Runtime>(&mut self, window: &WebviewWindow<R>, id: &str) -> Result<()> {
@@ -69,13 +66,13 @@ impl YaakNotifier {
pub async fn maybe_check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<()> {
let app_handle = window.app_handle();
let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
if ignore_check {
if let Some(i) = self.last_check
&& i.elapsed().as_secs() < MAX_UPDATE_CHECK_SECONDS
{
return Ok(());
}
self.last_check = SystemTime::now();
self.last_check = Some(Instant::now());
if !app_handle.db().get_settings().check_notifications {
info!("Notifications are disabled. Skipping check.");

View File

@@ -1,6 +1,6 @@
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use std::time::{Duration, Instant};
use crate::error::Result;
use log::{debug, error, info, warn};
@@ -24,7 +24,7 @@ const MAX_UPDATE_CHECK_HOURS_ALPHA: u64 = 1;
// Create updater struct
pub struct YaakUpdater {
last_update_check: SystemTime,
last_check: Option<Instant>,
}
pub enum UpdateMode {
@@ -62,9 +62,7 @@ pub enum UpdateTrigger {
impl YaakUpdater {
pub fn new() -> Self {
Self {
last_update_check: SystemTime::UNIX_EPOCH,
}
Self { last_check: None }
}
pub async fn check_now<R: Runtime>(
@@ -84,7 +82,7 @@ impl YaakUpdater {
let settings = window.db().get_settings();
let update_key = format!("{:x}", md5::compute(settings.id));
self.last_update_check = SystemTime::now();
self.last_check = Some(Instant::now());
info!("Checking for updates mode={} autodl={}", mode, auto_download);
@@ -176,9 +174,10 @@ impl YaakUpdater {
UpdateMode::Beta => MAX_UPDATE_CHECK_HOURS_BETA,
UpdateMode::Alpha => MAX_UPDATE_CHECK_HOURS_ALPHA,
} * (60 * 60);
let seconds_since_last_check = self.last_update_check.elapsed().unwrap().as_secs();
let ignore_check = seconds_since_last_check < update_period_seconds;
if ignore_check {
if let Some(i) = self.last_check
&& i.elapsed().as_secs() < update_period_seconds
{
return Ok(false);
}

View File

@@ -7,6 +7,7 @@ use tauri::{
};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
use yaak_models::query_manager::QueryManagerExt;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
@@ -94,7 +95,8 @@ pub(crate) fn create_window<R: Runtime>(
});
}
if config.hide_titlebar {
let settings = handle.db().get_settings();
if config.hide_titlebar && !settings.use_native_titlebar {
#[cfg(target_os = "macos")]
{
use tauri::TitleBarStyle;
@@ -102,7 +104,6 @@ pub(crate) fn create_window<R: Runtime>(
}
#[cfg(not(target_os = "macos"))]
{
// Doesn't seem to work from Rust, here, so we do it in main.tsx
win_builder = win_builder.decorations(false);
}
}

View File

@@ -30,6 +30,10 @@ pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>>
],
)?;
#[cfg(target_os = "macos")] {
window_menu.set_as_windows_menu_for_nsapp()?;
}
let help_menu = Submenu::with_id_and_items(
app_handle,
HELP_SUBMENU_ID,
@@ -44,6 +48,10 @@ pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>>
],
)?;
#[cfg(target_os = "macos")] {
help_menu.set_as_windows_menu_for_nsapp()?;
}
let menu = Menu::with_items(
app_handle,
&[

View File

@@ -1,16 +1,38 @@
use crate::error::Error::GitNotFound;
use crate::error::Result;
use std::path::Path;
use std::process::Command;
use std::process::{Command, Stdio};
use crate::error::Result;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use crate::error::Error::GitNotFound;
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
pub(crate) fn new_binary_command(dir: &Path) -> Result<Command> {
let status = Command::new("git").arg("--version").status();
// 1. Probe that `git` exists and is runnable
let mut probe = Command::new("git");
probe.arg("--version").stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
if let Err(_) = status {
#[cfg(target_os = "windows")]
{
probe.creation_flags(CREATE_NO_WINDOW);
}
let status = probe.status().map_err(|_| GitNotFound)?;
if !status.success() {
return Err(GitNotFound);
}
// 2. Build the reusable git command
let mut cmd = Command::new("git");
cmd.arg("-C").arg(dir);
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}
Ok(cmd)
}

View File

@@ -4,16 +4,32 @@ mod commands;
mod mac;
use crate::commands::{set_theme, set_title};
use tauri::{
Runtime, generate_handler,
plugin::{Builder, TauriPlugin},
};
use std::sync::atomic::AtomicBool;
use tauri::{generate_handler, plugin, plugin::TauriPlugin, Manager, Runtime};
pub trait AppHandleMacWindowExt {
/// Sets whether to use the native titlebar
fn set_native_titlebar(&self, enable: bool);
}
impl<R: Runtime> AppHandleMacWindowExt for tauri::AppHandle<R> {
fn set_native_titlebar(&self, enable: bool) {
self.state::<PluginState>().native_titlebar.store(enable, std::sync::atomic::Ordering::Relaxed);
}
}
pub(crate) struct PluginState {
native_titlebar: AtomicBool,
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
#[allow(unused)]
Builder::new("yaak-mac-window")
plugin::Builder::new("yaak-mac-window")
.setup(move |app, _| {
app.manage(PluginState { native_titlebar: AtomicBool::new(false) });
Ok(())
})
.invoke_handler(generate_handler![set_title, set_theme])
.on_window_ready(|window| {
.on_window_ready(move |window| {
#[cfg(target_os = "macos")]
{
mac::setup_traffic_light_positioner(&window);

View File

@@ -1,7 +1,8 @@
#![allow(deprecated)]
use crate::PluginState;
use csscolorparser::Color;
use objc::{msg_send, sel, sel_impl};
use tauri::{Emitter, Runtime, Window};
use tauri::{Emitter, Manager, Runtime, State, Window};
struct UnsafeWindowHandle(*mut std::ffi::c_void);
@@ -16,6 +17,8 @@ const MAIN_WINDOW_PREFIX: &str = "main_";
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
use cocoa::{appkit::NSWindow, base::nil, foundation::NSString};
let state: State<PluginState> = window.state();
let native_titlebar = state.native_titlebar.load(std::sync::atomic::Ordering::Relaxed);
unsafe {
let window_handle = UnsafeWindowHandle(window.ns_window().unwrap());
@@ -25,12 +28,16 @@ pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String)
let win_title = NSString::alloc(nil).init_str(&title);
let handle = window_handle;
NSWindow::setTitle_(handle.0 as cocoa::base::id, win_title);
position_traffic_lights(
UnsafeWindowHandle(window2.ns_window().expect("Failed to create window handle")),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
label,
);
if !native_titlebar {
position_traffic_lights(
UnsafeWindowHandle(
window2.ns_window().expect("Failed to create window handle"),
),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
label,
);
}
});
}
}
@@ -42,6 +49,8 @@ pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: Color) {
let brightness = (color.r as f64 + color.g as f64 + color.b as f64) / 3.0;
let label = window.label().to_string();
let state: State<PluginState> = window.state();
let native_titlebar = state.native_titlebar.load(std::sync::atomic::Ordering::Relaxed);
unsafe {
let window_handle = UnsafeWindowHandle(window.ns_window().unwrap());
@@ -56,12 +65,16 @@ pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: Color) {
};
NSWindow::setAppearance(handle.0 as cocoa::base::id, selected_appearance);
position_traffic_lights(
UnsafeWindowHandle(window2.ns_window().expect("Failed to create window handle")),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
label,
);
if !native_titlebar {
position_traffic_lights(
UnsafeWindowHandle(
window2.ns_window().expect("Failed to create window handle"),
),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
label,
);
}
});
}
}
@@ -119,6 +132,11 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: &Window<R>) {
use rand::distr::Alphanumeric;
use std::ffi::c_void;
let state: State<PluginState> = window.state();
if state.native_titlebar.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
position_traffic_lights(
UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")),
WINDOW_CONTROL_PAD_X,

View File

@@ -62,7 +62,7 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, };
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };

View File

@@ -0,0 +1,3 @@
-- Add a setting to force native window title bar / controls
ALTER TABLE settings
ADD COLUMN use_native_titlebar BOOLEAN DEFAULT FALSE NOT NULL;

View File

@@ -112,6 +112,8 @@ pub struct Settings {
pub editor_keymap: EditorKeymap,
pub editor_soft_wrap: bool,
pub hide_window_controls: bool,
// When true (primarily on Windows/Linux), use the native OS window title bar and controls
pub use_native_titlebar: bool,
pub interface_font: Option<String>,
pub interface_font_size: i32,
pub interface_scale: f32,
@@ -168,6 +170,7 @@ impl UpsertModelInfo for Settings {
(InterfaceFontSize, self.interface_font_size.into()),
(InterfaceScale, self.interface_scale.into()),
(HideWindowControls, self.hide_window_controls.into()),
(UseNativeTitlebar, self.use_native_titlebar.into()),
(OpenWorkspaceNewWindow, self.open_workspace_new_window.into()),
(ThemeDark, self.theme_dark.as_str().into()),
(ThemeLight, self.theme_light.as_str().into()),
@@ -193,6 +196,7 @@ impl UpsertModelInfo for Settings {
SettingsIden::InterfaceScale,
SettingsIden::InterfaceFont,
SettingsIden::HideWindowControls,
SettingsIden::UseNativeTitlebar,
SettingsIden::OpenWorkspaceNewWindow,
SettingsIden::Proxy,
SettingsIden::ThemeDark,
@@ -225,6 +229,7 @@ impl UpsertModelInfo for Settings {
interface_font_size: row.get("interface_font_size")?,
interface_scale: row.get("interface_scale")?,
interface_font: row.get("interface_font")?,
use_native_titlebar: row.get("use_native_titlebar")?,
open_workspace_new_window: row.get("open_workspace_new_window")?,
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
theme_dark: row.get("theme_dark")?,

View File

@@ -26,6 +26,7 @@ impl<'a> DbContext<'a> {
interface_scale: 1.0,
interface_font: None,
hide_window_controls: false,
use_native_titlebar: false,
open_workspace_new_window: None,
proxy: None,
theme_dark: "yaak-dark".to_string(),

View File

@@ -27,6 +27,7 @@ export function HeaderSize({
}: HeaderSizeProps) {
const settings = useAtomValue(settingsAtom);
const isFullscreen = useIsFullscreen();
const nativeTitlebar = settings.useNativeTitlebar;
const finalStyle = useMemo<CSSProperties>(() => {
const s = { ...style };
@@ -34,7 +35,9 @@ export function HeaderSize({
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
if (type() === 'macos') {
if (nativeTitlebar) {
// No style updates when using native titlebar
} else if (type() === 'macos') {
if (!isFullscreen) {
// Add large padding for window controls
s.paddingLeft = 72 / settings.interfaceScale;
@@ -51,6 +54,7 @@ export function HeaderSize({
settings.interfaceScale,
size,
style,
nativeTitlebar,
]);
return (
@@ -73,7 +77,7 @@ export function HeaderSize({
>
{children}
</div>
{!hideControls && <WindowControls onlyX={onlyXWindowControl} />}
{!hideControls && !nativeTitlebar && <WindowControls onlyX={onlyXWindowControl} />}
</div>
);
}

View File

@@ -4,11 +4,14 @@ import { useLicense } from '@yaakapp-internal/license';
import type { EditorKeymap, Settings } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp';
import { showConfirm } from '../../lib/confirm';
import { invokeCmd } from '../../lib/tauri';
import { CargoFeature } from '../CargoFeature';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { Icon } from '../core/Icon';
import { Link } from '../core/Link';
@@ -154,6 +157,8 @@ export function SettingsInterface() {
<LicenseSettings settings={settings} />
</CargoFeature>
<NativeTitlebarSetting settings={settings} />
{type() !== 'macos' && (
<Checkbox
checked={settings.hideWindowControls}
@@ -165,6 +170,33 @@ export function SettingsInterface() {
</VStack>
);
}
function NativeTitlebarSetting({ settings }: { settings: Settings }) {
const [nativeTitlebar, setNativeTitlebar] = useState(settings.useNativeTitlebar);
return (
<div className="flex gap-1 overflow-hidden h-2xs">
<Checkbox
checked={nativeTitlebar}
title="Native title bar"
help="Use the operating system's standard title bar and window controls"
onChange={setNativeTitlebar}
/>
{settings.useNativeTitlebar !== nativeTitlebar && (
<Button
color="primary"
size="2xs"
onClick={async () => {
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
await invokeCmd('cmd_restart');
}}
>
Apply and Restart
</Button>
)}
</div>
);
}
function LicenseSettings({ settings }: { settings: Settings }) {
const license = useLicense();
if (license.check.data?.type !== 'personal_use') {

View File

@@ -18,7 +18,7 @@ export function WindowControls({ className, onlyX }: Props) {
const [maximized, setMaximized] = useState<boolean>(false);
const settings = useAtomValue(settingsAtom);
// Never show controls on macOS or if hideWindowControls is true
if (type() === 'macos' || settings.hideWindowControls) {
if (type() === 'macos' || settings.hideWindowControls || settings.useNativeTitlebar) {
return null;
}

View File

@@ -1,6 +1,5 @@
import './main.css';
import { RouterProvider } from '@tanstack/react-router';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os';
import { changeModelStoreWorkspace, initModelStore } from '@yaakapp-internal/models';
import { StrictMode } from 'react';
@@ -10,12 +9,7 @@ import { initGlobalListeners } from './lib/initGlobalListeners';
import { jotaiStore } from './lib/jotai';
import { router } from './lib/router';
// Hide decorations here because it doesn't work in Rust for some reason (bug?)
const osType = type();
if (osType !== 'macos') {
await getCurrentWebviewWindow().setDecorations(false);
}
document.documentElement.setAttribute('data-platform', osType);
window.addEventListener('keydown', (e) => {