commit 85c914d65bb241a620f87909c941d0d93cbdca37 Author: tux Date: Sat Jun 28 19:21:26 2025 +0530 feat: initial commit 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