Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ csharpdialog --title "Auto-close" --message "This will close in 10 seconds" --ti
|--------|-------------|---------|
| `--commandfile` | Enable live updates via command file | `--commandfile "C:\temp\commands.txt"` |

### Lock / Dismissal Control (swiftDialog parity)
| Option | Description | Example |
|--------|-------------|---------|
| `--button1disabled` | Disable button1; window refuses to close (X, Alt+F4) until `quit`, quit key, timeout, or `button1: enable` | `--button1disabled` |
| `--quitkey` | Single character; Ctrl+\<char\> closes the dialog | `--quitkey 0` |
| `--hidedefaultkeyboardaction` | Suppress Esc/Enter/Alt+F4 dismissal | `--hidedefaultkeyboardaction` |

Use all three together for a non-blocking but undismissable progress window
that only the controlling script can close (via the command file), matching
swiftDialog's locked provisioning dialogs:

```powershell
dialog --window --centeronscreen --topmost --button1text "Please wait" `
--button1disabled --hidedefaultkeyboardaction --quitkey 0 `
--commandfile "C:\temp\commands.txt"
```

### Styling (Legacy)
| Option | Description | Example |
|--------|-------------|---------|
Expand Down Expand Up @@ -191,6 +208,12 @@ message: Updated message
button1text: Continue
```

#### Enable / Disable Button1 (releases or engages the close lock)
```
button1text: Get Started
button1: enable
```

#### Close Dialog
```
quit
Expand Down
23 changes: 21 additions & 2 deletions src/CsharpDialog.Core/CommandLineParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,34 @@ public static DialogConfiguration ParseArguments(string[] args)
i++;
break;
case "--button1":
case "--button1text":
if (!string.IsNullOrEmpty(value))
{
config.Buttons.Add(new DialogButton { Text = value, Action = "button1", IsDefault = true });
i++;
}
break;
case "--button2":
case "--button2text":
if (!string.IsNullOrEmpty(value))
{
config.Buttons.Add(new DialogButton { Text = value, Action = "button2" });
i++;
}
break;
case "--button1disabled":
config.Button1Disabled = true;
break;
case "--quitkey":
if (!string.IsNullOrWhiteSpace(value))
{
config.QuitKey = value.Trim().Substring(0, 1);
i++;
}
break;
case "--hidedefaultkeyboardaction":
config.HideDefaultKeyboardAction = true;
break;
case "--timeout":
if (int.TryParse(value, out int timeout))
{
Expand Down Expand Up @@ -195,8 +210,12 @@ private static void ShowHelp()
Console.WriteLine(" --title, -t <text> Set dialog title");
Console.WriteLine(" --message, -m <text> Set dialog message");
Console.WriteLine(" --icon, -i <path> Set dialog icon");
Console.WriteLine(" --button1 <text> Set first button text");
Console.WriteLine(" --button2 <text> Set second button text");
Console.WriteLine(" --button1 <text> Set first button text (alias: --button1text)");
Console.WriteLine(" --button2 <text> Set second button text (alias: --button2text)");
Console.WriteLine(" --button1disabled Disable button1; window cannot be closed until");
Console.WriteLine(" quit command, quit key, timeout, or button1: enable");
Console.WriteLine(" --quitkey <char> Ctrl+<char> closes the dialog");
Console.WriteLine(" --hidedefaultkeyboardaction Suppress Esc/Enter/Alt+F4 dismissal");
Console.WriteLine(" --timeout <seconds> Auto-close after timeout");
Console.WriteLine(" --width <pixels> Set dialog width");
Console.WriteLine(" --height <pixels> Set dialog height");
Expand Down
21 changes: 21 additions & 0 deletions src/CsharpDialog.Core/DialogConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ public class DialogConfiguration
public string CommandFilePath { get; set; } = string.Empty;
public bool EnableCommandFile { get; set; } = false;
public bool AutoClearCommandFile { get; set; } = true;

// Lock / dismissal control (swiftDialog parity)
/// <summary>
/// Renders button1 disabled. While disabled the window also refuses to
/// close via the title-bar X, so the dialog can only be dismissed by a
/// quit command, the quit key, a timeout, or a later "button1: enable".
/// Mirrors swiftDialog's --button1disabled.
/// </summary>
public bool Button1Disabled { get; set; } = false;

/// <summary>
/// Single character; Ctrl+&lt;char&gt; closes the dialog (the Windows
/// analog of swiftDialog's Cmd-based --quitkey). Empty = no quit key.
/// </summary>
public string QuitKey { get; set; } = string.Empty;

/// <summary>
/// Suppresses the default keyboard dismissals (Esc, Enter, Alt+F4).
/// Mirrors swiftDialog's --hidedefaultkeyboardaction.
/// </summary>
public bool HideDefaultKeyboardAction { get; set; } = false;

// Progress bar configuration
public bool ShowProgressBar { get; set; } = false;
Expand Down
26 changes: 19 additions & 7 deletions src/CsharpDialog.Core/Services/CommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public class CommandParser : ICommandParser
private static readonly string[] ValidCommands = {
"title", "message", "progress", "progresstext", "progressincrement", "progressreset",
"quit", "listitem", "list", "config", "style", "theme", "execute", "executepowershell",
"executeoutput", "width", "height", "position", "icon", "image", "button1text", "button2text"
"executeoutput", "width", "height", "position", "icon", "image", "button1text", "button2text",
"button1", "button2"
};

/// <summary>
Comment on lines +14 to 18
Expand All @@ -25,16 +26,27 @@ public class CommandParser : ICommandParser
}

var trimmedLine = commandLine.Trim();

// Basic command format: "command: value"

// Basic command format: "command: value". A bare command name with no
// colon is also accepted for value-less commands (e.g. "quit"), which
// is the form the documentation and existing scripts use.
var colonIndex = trimmedLine.IndexOf(':');
string commandType;
string commandValue;
if (colonIndex == -1)
{
return null; // Invalid format
commandType = trimmedLine.ToLowerInvariant();
commandValue = string.Empty;
if (!ValidCommands.Contains(commandType))
{
return null; // Not a bare valid command
}
}
else
{
commandType = trimmedLine[..colonIndex].Trim().ToLowerInvariant();
commandValue = trimmedLine[(colonIndex + 1)..].Trim();
}

var commandType = trimmedLine[..colonIndex].Trim().ToLowerInvariant();
var commandValue = trimmedLine[(colonIndex + 1)..].Trim();

if (!ValidCommands.Contains(commandType))
{
Expand Down
8 changes: 8 additions & 0 deletions src/CsharpDialog.WPF/ProgressDialogWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ public partial class ProgressDialogWindow : Window
{
private bool _isDarkMode;

/// <summary>
/// Set by legitimate dismissal paths (button click, quit command, quit
/// key, timeout) so the close-lock in WpfDialogService lets the window
/// close while --button1disabled is in effect.
/// </summary>
public bool AllowClose { get; set; } = false;

// Win32 API for dark mode title bar
[DllImport("dwmapi.dll", PreserveSig = true)]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
Expand Down Expand Up @@ -124,6 +131,7 @@ private void ApplyTheme(bool isDarkMode)

private void OkButton_Click(object sender, RoutedEventArgs e)
{
AllowClose = true;
this.Close();
}
}
Expand Down
146 changes: 144 additions & 2 deletions src/CsharpDialog.WPF/Services/WpfDialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public class WpfDialogService : IDialogService
private readonly Dictionary<string, ListItemControl> _listItems = new();
private int _nextListItemIndex = 0;
private bool _isDarkMode = false;
// True while button1 is disabled: the window refuses to close via the
// title-bar X or Alt+F4. The lock clears when a legitimate dismissal
// path sets _window.AllowClose (quit command, quit key, timeout, button
// click) or immediately on a runtime "button1: enable" command.
// swiftDialog --button1disabled parity.
private bool _closeLocked = false;

public event EventHandler<CommandReceivedEventArgs>? CommandReceived;
public bool IsCommandMonitoringActive => _commandMonitor?.IsMonitoring ?? false;
Expand Down Expand Up @@ -68,7 +74,56 @@ public DialogResult ShowDialog(DialogConfiguration configuration)

if (!string.IsNullOrEmpty(configuration.Title))
_window.Title = configuration.Title;


// Lock / dismissal control (swiftDialog parity). The handler is
// always attached because the lock can also be engaged at runtime
// via a "button1: disable" command-file command.
_closeLocked = configuration.Button1Disabled;
_window.Closing += (s, e) =>
{
if (_closeLocked && !_window.AllowClose)
{
e.Cancel = true;
Console.WriteLine($"[DEBUG] Close prevented - button1disabled lock active");
}
};

if (configuration.HideDefaultKeyboardAction || !string.IsNullOrEmpty(configuration.QuitKey))
{
var quitKey = configuration.QuitKey;
_window.PreviewKeyDown += (s, e) =>
{
// Quit key: Ctrl+<char> closes the dialog (Windows analog
// of swiftDialog's Cmd-based --quitkey).
if (!string.IsNullOrEmpty(quitKey) &&
System.Windows.Input.Keyboard.Modifiers == System.Windows.Input.ModifierKeys.Control &&
KeyMatchesChar(e.Key, quitKey[0]))
{
Console.WriteLine($"[DEBUG] Quit key pressed (Ctrl+{quitKey}) - closing");
result.ButtonPressed = "quit";
_window.AllowClose = true;
_window.Close();
e.Handled = true;
return;
}

if (configuration.HideDefaultKeyboardAction)
{
// Suppress Esc/Enter and Alt+F4 (the default keyboard
// dismissals). swiftDialog --hidedefaultkeyboardaction.
if (e.Key == System.Windows.Input.Key.Escape ||
e.Key == System.Windows.Input.Key.Enter ||
e.Key == System.Windows.Input.Key.Return ||
(e.SystemKey == System.Windows.Input.Key.F4 &&
(System.Windows.Input.Keyboard.Modifiers & System.Windows.Input.ModifierKeys.Alt) != 0))
{
e.Handled = true;
Console.WriteLine($"[DEBUG] Default keyboard action suppressed ({e.Key})");
}
}
};
}

// Wait for window to load before accessing content elements
_window.Loaded += (s, e) =>
{
Expand All @@ -88,6 +143,27 @@ public DialogResult ShowDialog(DialogConfiguration configuration)
if (messageText != null && !string.IsNullOrEmpty(configuration.Message))
messageText.Text = configuration.Message;

// Apply button1 text and disabled state (swiftDialog parity)
var okButton = _window.FindName("OkButton") as Button;
if (okButton != null)
{
var button1 = configuration.Buttons.FirstOrDefault(b => b.Action is "button1" or "ok");
if (button1 != null && !string.IsNullOrEmpty(button1.Text))
okButton.Content = button1.Text;

// Record the configured action so the dialog result (and
// the CLI exit-code mapping) reflects which button closed
// the window instead of always reporting the default "ok".
var button1Action = button1?.Action ?? "button1";
okButton.Click += (cs, ce) => result.ButtonPressed = button1Action;

if (configuration.Button1Disabled)
{
okButton.IsEnabled = false;
Console.WriteLine($"[DEBUG] Button1 disabled (--button1disabled)");
}
}

// Hide progress bar if not enabled
var progressBar = _window.FindName("ProgressBarControl") as ProgressBar;
var progressText = _window.FindName("ProgressText") as TextBlock;
Expand Down Expand Up @@ -186,6 +262,7 @@ public DialogResult ShowDialog(DialogConfiguration configuration)
timeoutTimer.Stop();
Console.WriteLine($"[DEBUG] Timeout reached - closing window");
result.ButtonPressed = "timeout";
_window.AllowClose = true;
_window.Close();
};
timeoutTimer.Start();
Expand Down Expand Up @@ -312,8 +389,37 @@ private void ProcessCommandSync(Command command)
ProcessListItemCommand(command);
break;

case "button1text":
var okBtnText = _window!.FindName("OkButton") as Button;
if (okBtnText != null)
okBtnText.Content = command.Value;
break;

case "button1":
// "button1: enable" / "button1: disable" — toggles the button
// and the close lock together (swiftDialog parity: enabling
// button1 restores a legitimate dismissal path).
var okBtnState = _window!.FindName("OkButton") as Button;
if (okBtnState != null)
{
if (command.Value.Equals("enable", StringComparison.OrdinalIgnoreCase))
{
okBtnState.IsEnabled = true;
_closeLocked = false;
Console.WriteLine($"[DEBUG] Button1 enabled - close lock released");
}
else if (command.Value.Equals("disable", StringComparison.OrdinalIgnoreCase))
{
okBtnState.IsEnabled = false;
_closeLocked = true;
Console.WriteLine($"[DEBUG] Button1 disabled - close lock engaged");
}
}
break;

case "quit":
_window!.Close();
_window!.AllowClose = true;
_window.Close();
break;
}
}
Expand All @@ -324,6 +430,18 @@ private void ProcessCommandSync(Command command)
}
}

// Maps a WPF Key to a quit-key character (letters and digits).
private static bool KeyMatchesChar(System.Windows.Input.Key key, char ch)
{
var upper = char.ToUpperInvariant(ch);
if (upper >= 'A' && upper <= 'Z')
return key.ToString().Equals(upper.ToString(), StringComparison.Ordinal);
if (upper >= '0' && upper <= '9')
return key.ToString().Equals("D" + upper, StringComparison.Ordinal) ||
key.ToString().Equals("NumPad" + upper, StringComparison.Ordinal);
return false;
}

private void OnCommandError(object? sender, CommandFileErrorEventArgs e)
{
Console.WriteLine($"Command file error: {e.Message}");
Expand Down Expand Up @@ -370,7 +488,31 @@ await _window.Dispatcher.InvokeAsync(() =>
ProcessListItemCommand(command);
break;

case "button1text":
var okBtnText = _window.FindName("OkButton") as Button;
if (okBtnText != null)
okBtnText.Content = command.Value;
break;

case "button1":
var okBtnState = _window.FindName("OkButton") as Button;
if (okBtnState != null)
{
if (command.Value.Equals("enable", StringComparison.OrdinalIgnoreCase))
{
okBtnState.IsEnabled = true;
_closeLocked = false;
}
else if (command.Value.Equals("disable", StringComparison.OrdinalIgnoreCase))
{
okBtnState.IsEnabled = false;
_closeLocked = true;
}
}
break;

case "quit":
_window.AllowClose = true;
_window.Close();
break;
}
Expand Down
Loading