diff --git a/.gitignore b/.gitignore index 9e188d3..fe87991 100644 --- a/.gitignore +++ b/.gitignore @@ -426,4 +426,6 @@ appsettings.*.json !appsettings.example.json CLAUDE.md *.db -*.db-* \ No newline at end of file +*.db-* +src/EchoHub.Server/uploads/* +.claude/* diff --git a/src/EchoHub.Client/EchoHub.Client.csproj b/src/EchoHub.Client/EchoHub.Client.csproj index 7a9754d..01a3034 100644 --- a/src/EchoHub.Client/EchoHub.Client.csproj +++ b/src/EchoHub.Client/EchoHub.Client.csproj @@ -6,11 +6,19 @@ + + + + + PreserveNewest + + + Exe net10.0 diff --git a/src/EchoHub.Client/Program.cs b/src/EchoHub.Client/Program.cs index 3d2cd1f..283dce6 100644 --- a/src/EchoHub.Client/Program.cs +++ b/src/EchoHub.Client/Program.cs @@ -6,6 +6,7 @@ using EchoHub.Client.UI; using EchoHub.Core.Constants; using EchoHub.Core.DTOs; using EchoHub.Core.Models; +using Microsoft.Extensions.Configuration; using Serilog; using Terminal.Gui.App; using Terminal.Gui.Views; @@ -26,16 +27,14 @@ public static class Program public static void Main() { - // Configure Serilog file logger - var logDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".echohub", "logs"); + // Configure Serilog from appsettings.json + var configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + .Build(); + Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.File( - Path.Combine(logDir, "echohub-.log"), - rollingInterval: RollingInterval.Day, - retainedFileCountLimit: 7, - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .ReadFrom.Configuration(configuration) .CreateLogger(); Log.Information("EchoHub client starting"); diff --git a/src/EchoHub.Client/UI/MainWindow.cs b/src/EchoHub.Client/UI/MainWindow.cs index 90b900f..26f0d29 100644 --- a/src/EchoHub.Client/UI/MainWindow.cs +++ b/src/EchoHub.Client/UI/MainWindow.cs @@ -3,18 +3,17 @@ using EchoHub.Client.Themes; using EchoHub.Core.DTOs; using EchoHub.Core.Models; using Terminal.Gui.App; -using Terminal.Gui.Views; -using Terminal.Gui.ViewBase; -using Terminal.Gui.Drawing; using Terminal.Gui.Configuration; using Terminal.Gui.Input; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; namespace EchoHub.Client.UI; /// /// Main Terminal.Gui window for the EchoHub chat client. /// -public sealed class MainWindow : Window +public sealed class MainWindow : Runnable { private readonly IApplication _app; private readonly ListView _channelList; @@ -24,6 +23,11 @@ public sealed class MainWindow : Window private readonly Label _statusLabel; private MenuBar _menuBar; + // Cached Key constants — compare via .KeyCode to avoid Key.Equals (which also checks Handled) + private static readonly Key EnterKey = Key.Enter; + private static readonly Key AltEnterKey = Key.Enter.WithAlt; + private static readonly Key CtrlCKey = Key.C.WithCtrl; + private readonly List _channelNames = []; private readonly Dictionary> _channelMessages = []; private string _currentChannel = string.Empty; @@ -72,8 +76,7 @@ public sealed class MainWindow : Window public MainWindow(IApplication app) { _app = app; - Title = "EchoHub"; - BorderStyle = LineStyle.None; + Arrangement = ViewArrangement.Fixed; // Menu bar at the top _menuBar = BuildMenuBar(); @@ -122,10 +125,10 @@ public sealed class MainWindow : Window _chatFrame.Add(_messageList); Add(_chatFrame); - // Bottom input area (multiline) + // Bottom input area var inputFrame = new FrameView { - Title = "Message (Enter=send, Shift+Enter=newline)", + Title = "Message (Enter=send, Alt+Enter=newline)", X = 25, Y = Pos.Bottom(_chatFrame), Width = Dim.Fill(), @@ -138,7 +141,6 @@ public sealed class MainWindow : Window Y = 0, Width = Dim.Fill(), Height = Dim.Fill(), - Text = "", WordWrap = true }; _inputField.KeyDown += OnInputKeyDown; @@ -159,6 +161,9 @@ public sealed class MainWindow : Window // Apply our custom color schemes to all views ApplyColorSchemes(); + + // Window-level key handling for Ctrl+S (send) and Ctrl+C (quit) + KeyDown += OnWindowKeyDown; } /// @@ -203,34 +208,43 @@ public sealed class MainWindow : Window } // Combine user items with theme items, separated by a line - var userMenuChildren = new MenuItem[] + var allUserItems = new List { new MenuItem("_My Profile", "Open your profile panel", () => OnProfileRequested?.Invoke(), Key.Empty), new MenuItem("Set _Status...", "Set your status", () => OnStatusRequested?.Invoke(), Key.Empty), + new Line() }; + allUserItems.AddRange(themeItems); - // Merge: user items + separator + theme items - var allUserItems = new MenuItem[userMenuChildren.Length + 1 + themeItems.Count]; - userMenuChildren.CopyTo(allUserItems, 0); - allUserItems[userMenuChildren.Length] = null!; // null separator - for (int i = 0; i < themeItems.Count; i++) - allUserItems[userMenuChildren.Length + 1 + i] = themeItems[i]; - - return new MenuBar( + var menuBar = new MenuBar( [ new MenuBarItem("_File", [ new MenuItem("_Quit", "Quit EchoHub", () => _app.RequestStop(), Key.Empty) ]), - new MenuBarItem("_Server", - [ + new MenuBarItem("_Server", new View[] + { new MenuItem("_Connect...", "Connect to a server", () => OnConnectRequested?.Invoke(), Key.Empty), new MenuItem("_Disconnect", "Disconnect from server", () => OnDisconnectRequested?.Invoke(), Key.Empty), - null!, + new Line(), new MenuItem("_Saved Servers...", "View saved servers", () => OnSavedServersRequested?.Invoke(), Key.Empty) - ]), + }), new MenuBarItem("_User", allUserItems) ]); + menuBar.X = 0; + menuBar.Y = 0; + menuBar.Width = Dim.Fill(); + + // Workaround: Make CommandView (title text area) mouse-transparent on each MenuBarItem. + // Without this, clicks on the text hit the CommandView sub-view whose Source propagates + // as a plain View — MenuBar.OnAccepting checks "sourceView is MenuBarItem" which fails. + // Making CommandView transparent lets clicks pass through to the MenuBarItem itself. + foreach (var mbi in menuBar.SubViews.OfType()) + { + mbi.CommandView.ViewportSettings |= ViewportSettingsFlags.TransparentMouse; + } + + return menuBar; } /// @@ -261,13 +275,12 @@ public sealed class MainWindow : Window private void OnInputKeyDown(object? sender, Key e) { - if (e == Key.Enter.WithShift) + if (e.KeyCode == AltEnterKey.KeyCode) { - // Shift+Enter: let TextView handle it (inserts newline) - return; + _inputField.InsertText("\n"); + e.Handled = true; } - - if (e == Key.Enter) + else if (e.KeyCode == EnterKey.KeyCode) { var text = _inputField.Text?.Trim() ?? string.Empty; if (!string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(_currentChannel)) @@ -277,6 +290,21 @@ public sealed class MainWindow : Window } e.Handled = true; } + else if (e.KeyCode == CtrlCKey.KeyCode) + { + _app.RequestStop(); + e.Handled = true; + } + } + + private void OnWindowKeyDown(object? sender, Key e) + { + // Ctrl+C quits from anywhere + if (e.KeyCode == CtrlCKey.KeyCode) + { + _app.RequestStop(); + e.Handled = true; + } } /// diff --git a/src/EchoHub.Client/appsettings.example.json b/src/EchoHub.Client/appsettings.example.json new file mode 100644 index 0000000..34fea85 --- /dev/null +++ b/src/EchoHub.Client/appsettings.example.json @@ -0,0 +1,22 @@ +{ + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "logs/echohub-.log", + "rollingInterval": "Day", + "retainedFileCountLimit": 7, + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}" + } + } + ] + } +}