diff --git a/Main.cs b/Main.cs index c78e35b..bbc044e 100644 --- a/Main.cs +++ b/Main.cs @@ -2,6 +2,15 @@ namespace PainoBar_Helper { public partial class Main : Form { + // Static event for Pianobar output - accessible to all forms + public static event Action? OnPianobarOutputReceived; + + // Static terminal history buffer for StationForm replay + public static List TerminalHistory = new List(); + + // Static reference to the main instance for accessing PianobarManager + private static Main? _instance; + private readonly ProfileManager _profileManager; private readonly PianobarManager _pianobarManager; private OutputManager? _outputManager; @@ -22,6 +31,8 @@ namespace PainoBar_Helper { InitializeComponent(); + _instance = this; + _profileManager = new ProfileManager(); _pianobarManager = new PianobarManager(); @@ -52,6 +63,18 @@ namespace PainoBar_Helper } } + /// + /// Sends a command to Pianobar's standard input stream. + /// + /// The command text to send (newline will be added automatically) + public static void SendStdin(string text) + { + if (_instance?._pianobarManager != null) + { + _instance._pianobarManager.SendCommand(text); + } + } + #region Initialization private void InitializeMenus() @@ -83,6 +106,7 @@ namespace PainoBar_Helper _pianobarManager.SongChanged += PianobarManager_SongChanged; _pianobarManager.StationChanged += PianobarManager_StationChanged; _pianobarManager.EventOccurred += PianobarManager_EventOccurred; + _pianobarManager.OutputReceived += PianobarManager_OutputReceived; // Profile events _profileManager.ProfilesChanged += ProfileManager_ProfilesChanged; @@ -240,7 +264,7 @@ namespace PainoBar_Helper return; } - var stationForm = new StationForm(_pianobarManager); + var stationForm = new StationForm(); stationForm.ShowDialog(this); } @@ -344,6 +368,7 @@ namespace PainoBar_Helper { _outputManager?.Stop(); _outputManager = null; + TerminalHistory.Clear(); // Clear history on disconnect UpdateConnectionStatus(ConnectionStatus.Disconnected); UpdatePlayPauseButton(); }); @@ -375,6 +400,16 @@ namespace PainoBar_Helper }); } + private void PianobarManager_OutputReceived(object? sender, string e) + { + // Add to history and forward to static event for StationForm and other subscribers + if (!string.IsNullOrEmpty(e)) + { + TerminalHistory.Add(e); + OnPianobarOutputReceived?.Invoke(e); + } + } + private void OutputManager_ActiveStateChanged(object? sender, EventArgs e) { Invoke(() => diff --git a/PianobarManager.cs b/PianobarManager.cs index 58c9423..d13cde2 100644 --- a/PianobarManager.cs +++ b/PianobarManager.cs @@ -313,9 +313,15 @@ namespace PainoBar_Helper lock (_inputLock) { - // Use LF explicitly to avoid CRLF handling issues with redirected stdin. - _inputWriter.Write(command); - _inputWriter.Write('\n'); + // Convert to ASCII bytes with Unix LF to avoid StreamWriter text encoding issues + // that can cause Pianobar's C-runtime to see extra characters + byte[] bytes = System.Text.Encoding.ASCII.GetBytes(command + "\n"); + + // Write raw bytes directly to base stream, bypassing StreamWriter + _inputWriter.BaseStream.Write(bytes, 0, bytes.Length); + + // Flush both base stream and StreamWriter buffers immediately + _inputWriter.BaseStream.Flush(); _inputWriter.Flush(); } diff --git a/ProfileManager.cs b/ProfileManager.cs index 0c44ccc..296c029 100644 --- a/ProfileManager.cs +++ b/ProfileManager.cs @@ -177,6 +177,11 @@ namespace PainoBar_Helper sb.AppendLine($"event_command = \"{eventCmdPath}\""); sb.AppendLine(); + // Force unbuffered stdout for immediate C# pipe output + sb.AppendLine("# Unbuffer output for real-time streaming"); + sb.AppendLine("unbuffer_output = 1"); + sb.AppendLine(); + // Additional settings for better integration sb.AppendLine("# Output formatting"); sb.AppendLine("format_nowplaying_song = %t|%a|%l|%r"); diff --git a/StationForm.cs b/StationForm.cs index fcdc728..9f40904 100644 --- a/StationForm.cs +++ b/StationForm.cs @@ -5,33 +5,38 @@ namespace PainoBar_Helper { public partial class StationForm : Form { - private readonly PianobarManager _pianobarManager; - private readonly StringBuilder _outputBuffer = new StringBuilder(); - - public StationForm(PianobarManager pianobarManager) + public StationForm() { InitializeComponent(); - _pianobarManager = pianobarManager; - - // Subscribe to output BEFORE controls are initialized - _pianobarManager.OutputReceived += PianobarManager_TerminalOutputReceived; - - // Handle form load to display buffered output + // Handle form load and closing for event subscription this.Load += StationForm_Load; + this.FormClosing += StationForm_FormClosing; InitializeControls(); } private void StationForm_Load(object? sender, EventArgs e) { - // Display any buffered output when form loads - if (terminal_box != null && _outputBuffer.Length > 0) + // First, populate terminal with existing history + if (terminal_box != null && Main.TerminalHistory.Count > 0) { - terminal_box.Text = _outputBuffer.ToString(); + foreach (string line in Main.TerminalHistory) + { + terminal_box.AppendText(line + "\n"); + } terminal_box.SelectionStart = terminal_box.Text.Length; terminal_box.ScrollToCaret(); } + + // Second, subscribe to live output events + Main.OnPianobarOutputReceived += AppendToTerminal; + } + + private void StationForm_FormClosing(object? sender, FormClosingEventArgs e) + { + // Unsubscribe from the static event + Main.OnPianobarOutputReceived -= AppendToTerminal; } #region Initialization @@ -61,16 +66,11 @@ namespace PainoBar_Helper #region Terminal Output - private void PianobarManager_TerminalOutputReceived(object? sender, string e) + private void AppendToTerminal(string data) { - if (string.IsNullOrEmpty(e)) + if (string.IsNullOrEmpty(data)) return; - string lineWithNewline = e + "\n"; - - // Buffer output even before form is shown - _outputBuffer.Append(lineWithNewline); - // Thread-safe UI update if (terminal_box != null) { @@ -78,12 +78,12 @@ namespace PainoBar_Helper { terminal_box.Invoke(() => { - AppendTerminalText(lineWithNewline); + AppendTerminalText(data + "\n"); }); } else { - AppendTerminalText(lineWithNewline); + AppendTerminalText(data + "\n"); } } } @@ -125,11 +125,8 @@ namespace PainoBar_Helper try { - // Echo the command to the terminal (simulating user input display) - AppendTerminalText($"> {command}\n"); - - // Send command to Pianobar with newline and flush - _pianobarManager.SendCommand(command); + // Send command directly to Pianobar stdin (no manual echo with ">") + Main.SendStdin(command); command_input_box.Clear(); } @@ -157,15 +154,5 @@ namespace PainoBar_Helper } #endregion - - #region Cleanup - - protected override void OnFormClosing(FormClosingEventArgs e) - { - _pianobarManager.OutputReceived -= PianobarManager_TerminalOutputReceived; - base.OnFormClosing(e); - } - - #endregion } } diff --git a/Win32ConsoleHelper.cs b/Win32ConsoleHelper.cs new file mode 100644 index 0000000..4cba0a9 --- /dev/null +++ b/Win32ConsoleHelper.cs @@ -0,0 +1,169 @@ +using System.Runtime.InteropServices; + +namespace PainoBar_Helper +{ + /// + /// Helper class for injecting keyboard input directly into a Win32 console process. + /// Required for Pianobar Windows port which uses _getch() instead of stdin. + /// + public static class Win32ConsoleHelper + { + #region Win32 API Imports + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AttachConsole(uint dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool FreeConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool WriteConsoleInput( + IntPtr hConsoleInput, + INPUT_RECORD[] lpBuffer, + uint nLength, + out uint lpNumberOfEventsWritten); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetStdHandle(int nStdHandle); + + private const int STD_INPUT_HANDLE = -10; + + #endregion + + #region Structures + + [StructLayout(LayoutKind.Explicit)] + private struct INPUT_RECORD + { + [FieldOffset(0)] + public ushort EventType; + [FieldOffset(4)] + public KEY_EVENT_RECORD KeyEvent; + } + + [StructLayout(LayoutKind.Sequential)] + private struct KEY_EVENT_RECORD + { + public bool bKeyDown; + public ushort wRepeatCount; + public ushort wVirtualKeyCode; + public ushort wVirtualScanCode; + public char UnicodeChar; + public uint dwControlKeyState; + } + + private const ushort KEY_EVENT = 1; + private const ushort VK_RETURN = 0x0D; + + #endregion + + #region Public Methods + + /// + /// Sends keyboard input directly to a console process using Win32 API. + /// + /// The process ID of the target console application + /// The text to send (Enter key will be automatically appended) + /// True if successful, false otherwise + public static bool SendConsoleInput(int processId, string text) + { + if (string.IsNullOrEmpty(text)) + return false; + + try + { + // Attach to the target process's console + if (!AttachConsole((uint)processId)) + { + int error = Marshal.GetLastWin32Error(); + System.Diagnostics.Debug.WriteLine($"AttachConsole failed with error: {error}"); + return false; + } + + try + { + // Get the console input handle + IntPtr hStdIn = GetStdHandle(STD_INPUT_HANDLE); + if (hStdIn == IntPtr.Zero || hStdIn == new IntPtr(-1)) + { + System.Diagnostics.Debug.WriteLine("GetStdHandle failed"); + return false; + } + + // Build input records for each character + Enter key + List inputRecords = new List(); + + // Add key events for each character in the text + foreach (char c in text) + { + // Key down event + inputRecords.Add(CreateKeyEvent(c, true)); + // Key up event + inputRecords.Add(CreateKeyEvent(c, false)); + } + + // Add Enter key (VK_RETURN) at the end + inputRecords.Add(CreateKeyEvent('\r', true, VK_RETURN)); + inputRecords.Add(CreateKeyEvent('\r', false, VK_RETURN)); + + // Write all input records to the console input buffer + uint written; + bool success = WriteConsoleInput( + hStdIn, + inputRecords.ToArray(), + (uint)inputRecords.Count, + out written); + + if (!success) + { + int error = Marshal.GetLastWin32Error(); + System.Diagnostics.Debug.WriteLine($"WriteConsoleInput failed with error: {error}"); + return false; + } + + System.Diagnostics.Debug.WriteLine($"Successfully wrote {written} input events for: {text}"); + return true; + } + finally + { + // Always detach from the console + FreeConsole(); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"SendConsoleInput exception: {ex.Message}"); + return false; + } + } + + #endregion + + #region Private Helper Methods + + private static INPUT_RECORD CreateKeyEvent(char character, bool keyDown, ushort virtualKeyCode = 0) + { + // If no virtual key code provided, use the character's value + if (virtualKeyCode == 0) + { + virtualKeyCode = (ushort)char.ToUpper(character); + } + + return new INPUT_RECORD + { + EventType = KEY_EVENT, + KeyEvent = new KEY_EVENT_RECORD + { + bKeyDown = keyDown, + wRepeatCount = 1, + wVirtualKeyCode = virtualKeyCode, + wVirtualScanCode = 0, + UnicodeChar = character, + dwControlKeyState = 0 + } + }; + } + + #endregion + } +}