using System.Runtime.InteropServices;
using Microsoft.UI.Windowing;
using Windows.Graphics;
using WinRT.Interop;
namespace LlamaApp
{
///
/// A small centered settings window (separate from the tray flyout) styled
/// after the Windows 11 Settings app: a left NavigationView with three
/// pages — General (launch at startup, local models cache), Identity
/// (Hugging Face token) and Llama (server port). Saves to
/// on Save; discards on Cancel.
///
public sealed partial class SettingsWindow : Window
{
// Settings window size in DIPs (the units XAML layout uses), centered on
// the cursor's monitor. AppWindow sizes/positions are in PHYSICAL pixels,
// so the DIPs are scaled by that monitor's DPI before Resize/Move — the
// window shows the same amount of content on every screen, whatever the
// display scaling. Clamped to the work area so small screens still fit.
private const int WindowWidthDips = 960;
private const int WindowHeightDips = 560;
public SettingsWindow()
{
InitializeComponent();
Title = "Settings";
Configure();
SizeAndCenterOnScreen();
LoadCurrent();
// Extend Mica/content into the titlebar area and register our
// AppTitleBar element as the drag region. The system caption
// buttons (close) stay on the right; this is what drops the
// default white titlebar that clashes with the Mica backdrop.
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar);
}
private void Configure()
{
var presenter = (OverlappedPresenter)AppWindow.Presenter;
presenter.IsResizable = false;
presenter.IsMaximizable = false;
presenter.IsMinimizable = false;
// Don't appear in Alt-Tab / taskbar switcher — it's a child dialog
// of the tray app, not a standalone top-level window.
AppWindow.IsShownInSwitchers = false;
// Keep the border + titlebar (caption buttons) but let the Mica
// backdrop fill the titlebar area (ExtendsContentIntoTitleBar set
// in the ctor) — this is what drops the default white titlebar.
presenter.SetBorderAndTitleBar(hasBorder: true, hasTitleBar: true);
// Pin the corner radius to the standard 8px "round" style rather
// than relying on the system default.
WindowCorners.ApplyRound8(this);
}
///
/// Sizes the window to the fixed DIP design size — scaled by the cursor
/// monitor's DPI, clamped to its work area — then centers it there: the
/// natural spot for a dialog spawned from a tray-only app (no owning
/// window to center on).
///
private void SizeAndCenterOnScreen()
{
var cursor = GetCursorPos(out var pt) ? pt : new POINT();
var hmon = MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST);
var mi = new MONITORINFO { cbSize = Marshal.SizeOf() };
GetMonitorInfo(hmon, ref mi);
int workWidth = mi.rcWork.Right - mi.rcWork.Left;
int workHeight = mi.rcWork.Bottom - mi.rcWork.Top;
// DIP size → physical pixels at this monitor's DPI (default 96 =
// 100% scaling if the query fails), clamped to the work area with
// a small margin so very small screens still fit the window.
uint dpiX = 96, dpiY = 96;
GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, out dpiX, out dpiY);
int width = Math.Min((int)Math.Round(WindowWidthDips * dpiX / 96.0), (int)(workWidth * 0.92));
int height = Math.Min((int)Math.Round(WindowHeightDips * dpiY / 96.0), (int)(workHeight * 0.92));
AppWindow.Resize(new SizeInt32(width, height));
int x = mi.rcWork.Left + (workWidth - width) / 2;
int y = mi.rcWork.Top + (workHeight - height) / 2;
AppWindow.Move(new PointInt32(x, y));
}
private void LoadCurrent()
{
var s = Settings.Current;
TokenBox.Password = s.HuggingFaceToken ?? "";
CacheBox.Text = s.CacheDirectory ?? "";
PortBox.Value = s.ServerPort;
// The OS shortcut is the source of truth: a user may have toggled
// it via Task Manager > Startup outside this app, so read the real
// state rather than the persisted preference.
LaunchAtStartupBox.IsChecked = StartupHelper.IsRegistered();
LoadInstallInfo();
}
///
/// Populates the Installation Folder card (Llama page). The path is
/// informational, not a setting: for an external (PATH) installation
/// we show its directory but disable Empty — external installs are not
/// LlamaApp's to delete. For the app-managed install, Empty is offered
/// whenever the folder exists.
///
private void LoadInstallInfo()
{
var mgr = Llama.LlamaManager.Shared;
string path;
bool canEmpty;
if (mgr.CurrentOrigin == Llama.LlamaManager.Origin.External && mgr.BinaryPath is not null)
{
path = Path.GetDirectoryName(mgr.BinaryPath)!;
InstallDescriptionText.Text =
"Using an external llama installation found on PATH. It isn't managed by LlamaApp — emptying is only available for the app-managed install.";
canEmpty = false;
}
else
{
path = Llama.LlamaManager.ManagedInstallDir;
InstallDescriptionText.Text =
"Where LlamaApp installs the llama server binary. Emptying frees disk space — the binary is downloaded again on next launch.";
canEmpty = true;
}
InstallPathBox.Text = path;
var exists = Directory.Exists(path);
OpenInstallFolderButton.IsEnabled = exists;
EmptyInstallFolderButton.IsEnabled = canEmpty && exists;
}
private async void OpenInstallFolder_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
try
{
await Windows.System.Launcher.LaunchFolderPathAsync(InstallPathBox.Text);
}
catch (Exception ex)
{
Common.Log.Warn(ex, "open install folder failed");
}
}
private async void OpenLogFolder_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
try
{
await Windows.System.Launcher.LaunchFolderPathAsync(Common.Log.LogDirectory);
}
catch (Exception ex)
{
Common.Log.Warn(ex, "open log folder failed");
}
}
private async void EmptyInstallFolder_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var path = InstallPathBox.Text;
// Destructive: confirm first, with Cancel as the default button so
// Enter can't trigger it accidentally.
var confirm = new Microsoft.UI.Xaml.Controls.ContentDialog
{
XamlRoot = Content.XamlRoot,
Title = "Empty Llama folder?",
Content = $"This stops the llama server (if running) and deletes everything in:\n\n{path}\n\nThe llama binary is downloaded again the next time LlamaApp needs it.",
PrimaryButtonText = "Empty",
CloseButtonText = "Cancel",
DefaultButton = Microsoft.UI.Xaml.Controls.ContentDialogButton.Close,
};
if (await confirm.ShowAsync() != Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary)
return;
try
{
// Stop first: a running server holds a lock on llama.exe.
Llama.LlamaManager.Shared.StopServer();
// Empty = delete the contents, keep the folder itself.
foreach (var dir in Directory.EnumerateDirectories(path))
Directory.Delete(dir, recursive: true);
foreach (var file in Directory.EnumerateFiles(path))
File.Delete(file);
Llama.LlamaManager.Shared.NotifyManagedInstallRemoved();
Common.Log.Info($"emptied llama install folder: {path}");
await ShowMessageAsync("Folder emptied",
"The llama binary will be downloaded again the next time it's needed.");
}
catch (Exception ex)
{
// The raw exception is logged, not shown — a user-facing
// dialog gets an actionable message, not ex.Message.
Common.Log.Warn(ex, "empty install folder failed");
await ShowMessageAsync("Couldn't empty the folder",
"Some files may still be in use. Close any program using the folder and try again.");
}
LoadInstallInfo();
}
private async Task ShowMessageAsync(string title, string message)
{
var d = new Microsoft.UI.Xaml.Controls.ContentDialog
{
XamlRoot = Content.XamlRoot,
Title = title,
Content = message,
CloseButtonText = "OK",
};
await d.ShowAsync();
}
private async void Browse_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
// FolderPicker requires an owner HWND in unpackaged WinUI 3 apps.
var hwnd = WindowNative.GetWindowHandle(this);
var picker = new Windows.Storage.Pickers.FolderPicker();
InitializeWithWindow.Initialize(picker, hwnd);
picker.FileTypeFilter.Add("*");
try
{
var folder = await picker.PickSingleFolderAsync();
if (folder != null)
CacheBox.Text = folder.Path;
}
catch
{
// Picker failed (e.g. cancelled / unsupported state) — leave the
// current text untouched.
}
}
private void Save_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var s = Settings.Current;
s.HuggingFaceToken = TokenBox.Password;
s.CacheDirectory = CacheBox.Text.Trim();
// The NumberBox clamps to 1–65535 while editing; an empty box
// reads as NaN — fall back to the default port in that case. The
// value is applied the next time the app starts (the manager
// singleton's port is fixed at construction).
s.ServerPort = double.IsNaN(PortBox.Value)
? Llama.LlamaManager.DefaultServerPort
: (int)PortBox.Value;
// Apply the startup preference to the OS (create/delete the .lnk)
// and mirror it into settings.json as a hint for the checkbox on
// next open (LoadCurrent still re-reads the real OS state).
var wantStartup = LaunchAtStartupBox.IsChecked == true;
s.LaunchAtStartup = wantStartup;
try
{
if (wantStartup) StartupHelper.Register();
else StartupHelper.Unregister();
}
catch (Exception ex)
{
Common.Log.Warn(ex, "startup shortcut update failed");
}
s.Save();
Close();
}
private void Cancel_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
Close();
}
///
/// Switches the visible settings page from the selected nav item's
/// Tag. Null-guarded: the initial IsSelected in XAML can fire this
/// during InitializeComponent, before the page fields are connected.
///
private void NavView_SelectionChanged(
Microsoft.UI.Xaml.Controls.NavigationView sender,
Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
{
if (GeneralPage is null || IdentityPage is null || LlamaPage is null) return;
var tag = (args.SelectedItem as Microsoft.UI.Xaml.Controls.NavigationViewItem)?.Tag as string;
GeneralPage.Visibility = tag == "general"
? Microsoft.UI.Xaml.Visibility.Visible : Microsoft.UI.Xaml.Visibility.Collapsed;
IdentityPage.Visibility = tag == "identity"
? Microsoft.UI.Xaml.Visibility.Visible : Microsoft.UI.Xaml.Visibility.Collapsed;
LlamaPage.Visibility = tag == "llama"
? Microsoft.UI.Xaml.Visibility.Visible : Microsoft.UI.Xaml.Visibility.Collapsed;
}
// ---- Win32: center on the cursor's monitor ----
private const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int Left, Top, Right, Bottom; }
[StructLayout(LayoutKind.Sequential)]
private struct MONITORINFO
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int X, Y; }
[DllImport("user32.dll", ExactSpelling = true)]
private static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO mi);
[DllImport("shcore.dll", ExactSpelling = true)]
private static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY);
private const int MDT_EFFECTIVE_DPI = 0;
[DllImport("user32.dll", ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out POINT lpPoint);
}
}