From 85c914d65bb241a620f87909c941d0d93cbdca37 Mon Sep 17 00:00:00 2001 From: tux Date: Sat, 28 Jun 2025 19:21:26 +0530 Subject: [PATCH] feat: initial commit --- .gitignore | 214 +++++++++++++++++++++++ App.axaml | 11 ++ App.axaml.cs | 23 +++ Program.cs | 21 +++ README.md | 1 + app.manifest | 18 ++ highminded.csproj | 37 ++++ highminded.sln | 16 ++ ui/controls/ChatUserControl.axaml | 14 ++ ui/controls/ChatUserControl.axaml.cs | 67 +++++++ ui/controls/SettingsUserControl.axaml | 30 ++++ ui/controls/SettingsUserControl.axaml.cs | 30 ++++ ui/windows/MainWindow.axaml | 53 ++++++ ui/windows/MainWindow.axaml.cs | 167 ++++++++++++++++++ utils/InMemoryDB.cs | 15 ++ utils/SetttingsManager.cs | 51 ++++++ 16 files changed, 768 insertions(+) create mode 100644 .gitignore create mode 100644 App.axaml create mode 100644 App.axaml.cs create mode 100644 Program.cs create mode 100644 README.md create mode 100644 app.manifest create mode 100644 highminded.csproj create mode 100644 highminded.sln create mode 100644 ui/controls/ChatUserControl.axaml create mode 100644 ui/controls/ChatUserControl.axaml.cs create mode 100644 ui/controls/SettingsUserControl.axaml create mode 100644 ui/controls/SettingsUserControl.axaml.cs create mode 100644 ui/windows/MainWindow.axaml create mode 100644 ui/windows/MainWindow.axaml.cs create mode 100644 utils/InMemoryDB.cs create mode 100644 utils/SetttingsManager.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2683417 --- /dev/null +++ b/.gitignore @@ -0,0 +1,214 @@ +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates +.vs/ + +# Build results + +*.sln.ide/ +[Dd]ebug/ +[Rr]elease/ +x64/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# NCrunch +_NCrunch_*/ +*.ncrunchsolution.user +nCrunchTemp_* + +# CodeRush +.cr/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings +Events_Avalonia.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############# +## Windows detritus +############# + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + +################# +## Monodevelop +################# +*.userprefs +*.nugetreferenceswitcher + + +################# +## Rider +################# +.idea + +################# +## VS Code +################# +.vscode/ + +################# +## Cake +################# +tools/* +!tools/packages.config +.nuget +artifacts/ +nuget +Avalonia.XBuild.sln +project.lock.json +.idea/* + + +################## +## BenchmarkDotNet +################## +BenchmarkDotNet.Artifacts/ + +dirs.sln + + +################## +# Xcode +################## +Index/ +Logs/ +ModuleCache.noindex/ +Build/Intermediates.noindex/ +build-intermediate +obj-Direct2D1/ +obj-Skia/ + +################## +# Vim +################## +.vim +coc-settings.json +.ccls-cache +.ccls +*.map +src/Web/Avalonia.Web.Blazor/wwwroot/*.js +src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js \ No newline at end of file diff --git a/App.axaml b/App.axaml new file mode 100644 index 0000000..dc1cd93 --- /dev/null +++ b/App.axaml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/App.axaml.cs b/App.axaml.cs new file mode 100644 index 0000000..3e945d8 --- /dev/null +++ b/App.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace highminded; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new highminded.ui.windows.MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..f7aef14 --- /dev/null +++ b/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace highminded; + +class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a941c4a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# highminded \ No newline at end of file diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..a8d8d01 --- /dev/null +++ b/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/highminded.csproj b/highminded.csproj new file mode 100644 index 0000000..8932f22 --- /dev/null +++ b/highminded.csproj @@ -0,0 +1,37 @@ + + + WinExe + net9.0 + enable + true + app.manifest + true + + + + + + + + + + + None + All + + + + + + + + + + + + + MainWindow.axaml + Code + + + diff --git a/highminded.sln b/highminded.sln new file mode 100644 index 0000000..e1cac7a --- /dev/null +++ b/highminded.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "highminded", "highminded.csproj", "{0A358F60-E66F-41F6-A479-F827B9CD1313}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0A358F60-E66F-41F6-A479-F827B9CD1313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A358F60-E66F-41F6-A479-F827B9CD1313}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A358F60-E66F-41F6-A479-F827B9CD1313}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A358F60-E66F-41F6-A479-F827B9CD1313}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ui/controls/ChatUserControl.axaml b/ui/controls/ChatUserControl.axaml new file mode 100644 index 0000000..f1715c5 --- /dev/null +++ b/ui/controls/ChatUserControl.axaml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/ui/controls/ChatUserControl.axaml.cs b/ui/controls/ChatUserControl.axaml.cs new file mode 100644 index 0000000..7bd413f --- /dev/null +++ b/ui/controls/ChatUserControl.axaml.cs @@ -0,0 +1,67 @@ +using Avalonia.Controls; +using System; +using System.ClientModel; +using System.Text; +using Avalonia.Input; +using highminded.utils; +using OpenAI.Chat; +using OpenAI; +using Markdig; +using Markdown.ColorCode; + +namespace highminded.ui.controls; + +public partial class ChatUserControl : UserControl +{ + + // OpenAI + private readonly OpenAI.Chat.ChatClient _client = null!; + private readonly MarkdownPipeline _pipeline = null!; + + public ChatUserControl() + { + InitializeComponent(); + + _client = new ChatClient( + model: InMemoryDb.Obj.settingsManager.Settings.Model, + credential: new ApiKeyCredential(InMemoryDb.Obj.settingsManager.Settings.ApiKey), + options: new OpenAIClientOptions + { + Endpoint = new Uri(InMemoryDb.Obj.settingsManager.Settings.ApiURL) + }); + + _pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseColorCode().Build(); + } + + private async void PromptBox_OnKeyDown(object? sender, KeyEventArgs e) + { + try + { + if (e.Key != Key.Enter) return; + + var prompt = PromptBox.Text; + if (prompt is null) return; + PromptBox.Clear(); + + AsyncCollectionResult completionUpdates = + _client.CompleteChatStreamingAsync(prompt); + + var responseBuilder = new StringBuilder(); + + await foreach (var completionUpdate in completionUpdates) + { + if (completionUpdate.ContentUpdate.Count <= 0) continue; + + var token = completionUpdate.ContentUpdate[0].Text; + responseBuilder.Append(token); + + var html = Markdig.Markdown.ToHtml(responseBuilder.ToString(), _pipeline); + ResultBlock.Text = html; + } + } + catch (Exception err) + { + ResultBlock.Text = err.Message; + } + } +} \ No newline at end of file diff --git a/ui/controls/SettingsUserControl.axaml b/ui/controls/SettingsUserControl.axaml new file mode 100644 index 0000000..6f60ca2 --- /dev/null +++ b/ui/controls/SettingsUserControl.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/windows/MainWindow.axaml.cs b/ui/windows/MainWindow.axaml.cs new file mode 100644 index 0000000..f699e18 --- /dev/null +++ b/ui/windows/MainWindow.axaml.cs @@ -0,0 +1,167 @@ +using Avalonia.Controls; +using System; +using System.Runtime.InteropServices; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; +using highminded.ui.controls; +using SharpHook; +using SharpHook.Data; + +namespace highminded.ui.windows; + +public partial class MainWindow : Window +{ + // MacOS + private const string ObjCRuntime = "/usr/lib/libobjc.dylib"; + + [DllImport(ObjCRuntime, EntryPoint = "objc_msgSend")] + private static extern void ObjcMsgSendInt(nint receiver, nint selector, int value); + + [DllImport(ObjCRuntime, EntryPoint = "sel_registerName")] + private static extern nint RegisterName(string name); + + // Windows + [DllImport("user32.dll")] + private static extern bool SetWindowDisplayAffinity(IntPtr hWnd, uint dwAffinity); + + // Controls + private readonly ChatUserControl _chatUserControl = new ChatUserControl(); + private readonly SettingsUserControl _settingsUserControl = new SettingsUserControl(); + + // Hotkey + private readonly TaskPoolGlobalHook _hook = new TaskPoolGlobalHook(); + + public MainWindow() + { + InitializeComponent(); + UControl.Content = _chatUserControl; + ChatBtnActive(); + HideOverlay(); + // Global Hotkey + _hook.KeyPressed += OnKeyPressed; + _hook.RunAsync(); + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + BeginMoveDrag(e); + } + + private void ChatBtnClick(object? sender, RoutedEventArgs e) + { + UControl.Content = _chatUserControl; + ChatBtnActive(); + } + + private void SettingBtnClick(object? sender, RoutedEventArgs e) + { + UControl.Content = _settingsUserControl; + SettingsBtnActive(); + } + + private void ChatBtnActive() + { + ChatBtn.Background = new SolidColorBrush(Color.FromArgb(25, 255, 255, 255)); + SettingsBtn.Background = new SolidColorBrush(Colors.Transparent); + } + + private void SettingsBtnActive() + { + ChatBtn.Background = new SolidColorBrush(Colors.Transparent); + SettingsBtn.Background = new SolidColorBrush(Color.FromArgb(25, 255, 255, 255)); + } + + private void OnKeyPressed(object? sender, KeyboardHookEventArgs e) + { + bool hasCtrl = (e.RawEvent.Mask & EventMask.Ctrl) != EventMask.None; + bool hasAlt = (e.RawEvent.Mask & EventMask.Alt) != EventMask.None; + bool hasShift = (e.RawEvent.Mask & EventMask.Shift) != EventMask.None; + bool hasH = e.Data.KeyCode == KeyCode.VcH; + bool hasBackslash = e.Data.KeyCode == KeyCode.VcBackslash; + + if (hasCtrl && hasShift && hasAlt && hasH) + { + ShowOverlay(); + } + + if (hasShift && hasBackslash) + { + Dispatcher.UIThread.Post(() => + { + if (WindowState == WindowState.Minimized || !IsVisible) + { + Show(); + WindowState = WindowState.Normal; + Activate(); + Topmost = true; + } + else + { + Hide(); + } + }); + } + } + + private void ShowOverlay() + { + var handle = TryGetPlatformHandle(); + + if (handle is null || handle.Handle == IntPtr.Zero) + { + Console.WriteLine("Invalid window handle."); + return; + } + + var hwnd = handle.Handle; + + switch (handle.HandleDescriptor) + { + case "HWND": + { + const uint include = 0x00000000; + SetWindowDisplayAffinity(hwnd, include); + break; + } + case "NSWindow": + { + const int include = 2; + var sharingTypeSelector = RegisterName("setSharingType:"); + ObjcMsgSendInt(hwnd, sharingTypeSelector, include); + break; + } + } + } + + private void HideOverlay() + { + var handle = TryGetPlatformHandle(); + + if (handle is null || handle.Handle == IntPtr.Zero) + { + Console.WriteLine("Invalid window handle."); + return; + } + + var hwnd = handle.Handle; + + switch (handle.HandleDescriptor) + { + case "HWND": + { + const uint exclude = 0x00000011; + SetWindowDisplayAffinity(hwnd, exclude); + break; + } + case "NSWindow": + { + const int exclude = 0; + var sharingTypeSelector = RegisterName("setSharingType:"); + ObjcMsgSendInt(hwnd, sharingTypeSelector, exclude); + break; + } + } + } +} \ No newline at end of file diff --git a/utils/InMemoryDB.cs b/utils/InMemoryDB.cs new file mode 100644 index 0000000..cb9a374 --- /dev/null +++ b/utils/InMemoryDB.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; +using Path = Avalonia.Controls.Shapes.Path; + +namespace highminded.utils; + +public class InMemoryDb +{ + // Initialize Singleton Class + InMemoryDb() { } + + public static readonly InMemoryDb Obj = new InMemoryDb(); + + public SettingsManager settingsManager = new SettingsManager(); +} \ No newline at end of file diff --git a/utils/SetttingsManager.cs b/utils/SetttingsManager.cs new file mode 100644 index 0000000..455e236 --- /dev/null +++ b/utils/SetttingsManager.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace highminded.utils; + +public class AppSettings +{ + public string Model { get; set; } + public string ApiURL { get; set; } + public string ApiKey { get; set; } + +} + +public class SettingsManager where T : class, new() +{ + private readonly string _settingsPath; + public T Settings { get; private set; } + + public SettingsManager(string appName = "highminded") + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appFolder = Path.Combine(appData, appName); + Directory.CreateDirectory(appFolder); + _settingsPath = Path.Combine(appFolder, "settings.json"); + Settings = Load(); + } + + private T Load() + { + if (!File.Exists(_settingsPath)) + return new T(); + + try + { + string json = File.ReadAllText(_settingsPath); + return JsonSerializer.Deserialize(json) ?? new T(); + } + catch + { + return new T(); // Fallback to default settings if error occurs + } + } + + public void Save() + { + var options = new JsonSerializerOptions { WriteIndented = true }; + string json = JsonSerializer.Serialize(Settings, options); + File.WriteAllText(_settingsPath, json); + } +} \ No newline at end of file