diff --git a/schema/v1/ScriptRunnerSchema.json b/schema/v1/ScriptRunnerSchema.json
index 6fad487..d69c128 100644
--- a/schema/v1/ScriptRunnerSchema.json
+++ b/schema/v1/ScriptRunnerSchema.json
@@ -185,7 +185,32 @@
"type": "object",
"properties": {
"options": {
- "type": "string"
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["label", "value"],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
},
"searchable": {
"type": "boolean"
@@ -212,7 +237,32 @@
"type": "object",
"properties": {
"options": {
- "type": "string"
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["label", "value"],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
},
"delimiter": {
"type": "string"
@@ -282,12 +332,27 @@
},
"required": ["prompt"]
},
+ {
+ "properties": {
+ "prompt": {
+ "const": "multilineText"
+ },
+ "promptSettings": {
+ "type": "object",
+ "properties": {
+ "syntax": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "required": ["prompt"]
+ },
{
"properties": {
"prompt": {
"enum": [
- "text",
- "multilineText",
+ "text",
"password",
"filePicker",
"directoryPicker"
diff --git a/src/ScriptRunner/ScriptRunner.GUI/App.axaml b/src/ScriptRunner/ScriptRunner.GUI/App.axaml
index 0213660..304e126 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/App.axaml
+++ b/src/ScriptRunner/ScriptRunner.GUI/App.axaml
@@ -10,7 +10,7 @@
-
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/CheckBoxListBox.cs b/src/ScriptRunner/ScriptRunner.GUI/CheckBoxListBox.cs
index c5baa2f..51faaf2 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/CheckBoxListBox.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/CheckBoxListBox.cs
@@ -55,5 +55,15 @@ public CheckBoxListBox()
}
};
this.Styles.Add(style);
+
+ // Style for selected items
+ var selectedStyle = new Style(x => x.OfType().Class(":selected").Template().OfType())
+ {
+ Setters =
+ {
+ new Setter(ContentPresenter.BackgroundProperty, Avalonia.Media.Brushes.Transparent)
+ }
+ };
+ this.Styles.Add(selectedStyle);
}
}
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Converters/CategoryToColorConverter.cs b/src/ScriptRunner/ScriptRunner.GUI/Converters/CategoryToColorConverter.cs
new file mode 100644
index 0000000..b1cc00a
--- /dev/null
+++ b/src/ScriptRunner/ScriptRunner.GUI/Converters/CategoryToColorConverter.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace ScriptRunner.GUI.Converters;
+
+public class CategoryToColorConverter : IValueConverter
+{
+ private static readonly Color[] PredefinedColors = new[]
+ {
+ Color.FromRgb(70, 100, 180), // Darker Blue
+ Color.FromRgb(200, 90, 70), // Darker Coral
+ Color.FromRgb(80, 150, 80), // Darker Green
+ Color.FromRgb(150, 100, 150), // Darker Plum
+ Color.FromRgb(180, 140, 0), // Darker Gold
+ Color.FromRgb(85, 130, 160), // Darker Sky Blue
+ Color.FromRgb(180, 100, 120), // Darker Pink
+ Color.FromRgb(90, 160, 90), // Darker Pale Green
+ Color.FromRgb(180, 100, 80), // Darker Salmon
+ Color.FromRgb(110, 130, 150), // Darker Steel Blue
+ Color.FromRgb(160, 80, 160), // Darker Violet
+ Color.FromRgb(180, 130, 100), // Darker Peach
+ Color.FromRgb(100, 140, 170), // Darker Light Blue
+ Color.FromRgb(170, 80, 80), // Darker Coral Red
+ Color.FromRgb(120, 140, 70), // Olive Green
+ };
+
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is string category)
+ {
+ // Generate deterministic hash from category name
+ int hash = GetDeterministicHashCode(category);
+ int colorIndex = Math.Abs(hash) % PredefinedColors.Length;
+ return new SolidColorBrush(PredefinedColors[colorIndex]);
+ }
+
+ return new SolidColorBrush(Colors.Gray);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static int GetDeterministicHashCode(string str)
+ {
+ unchecked
+ {
+ int hash1 = 5381;
+ int hash2 = hash1;
+
+ for (int i = 0; i < str.Length && str[i] != '\0'; i += 2)
+ {
+ hash1 = ((hash1 << 5) + hash1) ^ str[i];
+ if (i == str.Length - 1 || str[i + 1] == '\0')
+ break;
+ hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
+ }
+
+ return hash1 + (hash2 * 1566083941);
+ }
+ }
+}
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Converters/StringEmptyToVisibilityConverter.cs b/src/ScriptRunner/ScriptRunner.GUI/Converters/StringEmptyToVisibilityConverter.cs
new file mode 100644
index 0000000..93152bf
--- /dev/null
+++ b/src/ScriptRunner/ScriptRunner.GUI/Converters/StringEmptyToVisibilityConverter.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace ScriptRunner.GUI.Converters;
+
+public class StringEmptyToVisibilityConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is string str)
+ {
+ return !string.IsNullOrEmpty(str);
+ }
+ return false;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Parameters/DropdownControl.cs b/src/ScriptRunner/ScriptRunner.GUI/Parameters/DropdownControl.cs
index d089fdd..c94aee3 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Parameters/DropdownControl.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/Parameters/DropdownControl.cs
@@ -1,4 +1,7 @@
-using Avalonia.Controls;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Avalonia.Controls;
+using ScriptRunner.GUI.ScriptConfigs;
using ScriptRunner.GUI.Views;
namespace ScriptRunner.GUI;
@@ -7,15 +10,33 @@ public class DropdownControl : IControlRecord
{
public Control Control { get; set; }
public Control InputControl { get; set; }
+ public ObservableCollection DropdownOptions { get; set; }
public string GetFormattedValue()
{
- return InputControl switch
+ var selectedItem = InputControl switch
{
- ComboBox cb => cb.SelectedItem?.ToString(),
+ ComboBox cb => cb.SelectedItem,
SearchableComboBox acb => acb.SelectedItem,
- _ => ""
- } ?? string.Empty;
+ _ => null
+ };
+
+ if (selectedItem is DropdownOption option)
+ {
+ return option.Value;
+ }
+
+ // For SearchableComboBox, we need to map the label back to value
+ if (selectedItem is string label && DropdownOptions != null)
+ {
+ var matchingOption = DropdownOptions.FirstOrDefault(opt => opt.Label == label);
+ if (matchingOption != null)
+ {
+ return matchingOption.Value;
+ }
+ }
+
+ return selectedItem?.ToString() ?? string.Empty;
}
public string Name { get; set; }
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Parameters/FileContent.cs b/src/ScriptRunner/ScriptRunner.GUI/Parameters/FileContent.cs
index 08c19ed..3a6bc7b 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Parameters/FileContent.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/Parameters/FileContent.cs
@@ -3,6 +3,7 @@
using System.Security.Cryptography;
using System.Text;
using Avalonia.Controls;
+using AvaloniaEdit;
namespace ScriptRunner.GUI;
@@ -20,7 +21,12 @@ public FileContent(string extension)
public string GetFormattedValue()
{
- var fileContent = ((TextBox)Control).Text;
+ var fileContent = Control switch
+ {
+ TextBox textBox => textBox.Text,
+ TextEditor textEditor => textEditor.Text,
+ _ => ((TextBox)Control).Text
+ };
var hash = string.IsNullOrWhiteSpace(fileContent)? "EMPTY" : ComputeSHA256(fileContent).Substring(0,10);
FileName = Path.Combine(Path.GetTempPath(), hash + "." + _extension);
File.WriteAllText(FileName, fileContent, Encoding.UTF8);
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Parameters/MultiSelectControl.cs b/src/ScriptRunner/ScriptRunner.GUI/Parameters/MultiSelectControl.cs
index cf9a196..d688808 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Parameters/MultiSelectControl.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/Parameters/MultiSelectControl.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using Avalonia.Controls;
+using ScriptRunner.GUI.ScriptConfigs;
namespace ScriptRunner.GUI;
@@ -13,7 +14,11 @@ public string GetFormattedValue()
var copy = new List();
foreach (var item in selectedItems)
{
- if (item.ToString() is { } nonNullItem)
+ if (item is DropdownOption option)
+ {
+ copy.Add(option.Value);
+ }
+ else if (item?.ToString() is { } nonNullItem)
{
copy.Add(nonNullItem);
}
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Parameters/ParamsPanelFactory.cs b/src/ScriptRunner/ScriptRunner.GUI/Parameters/ParamsPanelFactory.cs
index c8834c3..7c24bc3 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Parameters/ParamsPanelFactory.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/Parameters/ParamsPanelFactory.cs
@@ -4,6 +4,7 @@
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Reflection.Metadata;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Collections;
@@ -14,12 +15,17 @@
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
+using AvaloniaEdit;
+using AvaloniaEdit.Document;
+using AvaloniaEdit.Highlighting;
+using AvaloniaEdit.TextMate;
using Projektanker.Icons.Avalonia;
using ScriptRunner.GUI.Infrastructure;
using ScriptRunner.GUI.ScriptConfigs;
using ScriptRunner.GUI.Settings;
using ScriptRunner.GUI.ViewModels;
using ScriptRunner.GUI.Views;
+using TextMateSharp.Grammars;
using Path = System.IO.Path;
namespace ScriptRunner.GUI;
@@ -53,13 +59,21 @@ public ParamsPanel Create(ScriptConfig action, Dictionary values
controlRecord.Name = param.Name;
if (controlRecord.Control is Layoutable l)
{
- l.MaxWidth = 500;
+ // Don't set MaxWidth for multiline/file content controls since they have resize handles
+ if (param.Prompt is not (PromptType.Multilinetext or PromptType.FileContent))
+ {
+ l.MaxWidth = 500;
+ }
}
var label = new Label
{
- Content = string.IsNullOrWhiteSpace(param.Description)? param.Name: param.Description,
-
+ Content = new TextBlock
+ {
+ Text = string.IsNullOrWhiteSpace(param.Description) ? param.Name : param.Description,
+ TextWrapping = TextWrapping.Wrap,
+ MaxWidth = 300
+ }
};
ToolTip.SetTip(label, param.Name);
@@ -73,12 +87,13 @@ public ParamsPanel Create(ScriptConfig action, Dictionary values
var resizeHandle = new Border()
{
Height = 10,
+ Width = 10,
Background = Brushes.Transparent,
HorizontalAlignment = HorizontalAlignment.Right,
ZIndex = 1,
Margin = new Thickness(0,-10,0,0),
VerticalAlignment = VerticalAlignment.Bottom,
- Cursor = new Cursor(StandardCursorType.SizeNorthSouth)
+ Cursor = new Cursor(StandardCursorType.BottomRightCorner)
};
resizeHandle.Child = new Icon()
{
@@ -102,7 +117,13 @@ public ParamsPanel Create(ScriptConfig action, Dictionary values
var delta = currentPosition - _lastPointerPosition;
var textBox = controlRecord.Control;
+
+ // Resize vertically
textBox.Height = Math.Max(textBox.MinHeight, textBox.Height + delta.Y);
+
+ // Resize horizontally
+ var currentWidth = double.IsNaN(textBox.Width) ? textBox.Bounds.Width : textBox.Width;
+ textBox.Width = Math.Max(100, currentWidth + delta.X);
_lastPointerPosition = currentPosition;
}
@@ -158,9 +179,13 @@ public ParamsPanel Create(ScriptConfig action, Dictionary values
if (controlRecord is { Control: TextBox tb })
{
tb.Text = result?.Trim() ?? string.Empty;
- generateButton.Classes.Remove("spinning");
- generateButton.IsEnabled = true;
}
+ else if (controlRecord is { Control: TextEditor te })
+ {
+ te.Text = result?.Trim() ?? string.Empty;
+ }
+ generateButton.Classes.Remove("spinning");
+ generateButton.IsEnabled = true;
});
};
Attached.SetIcon(generateButton, "fas fa-wand-magic-sparkles");
@@ -241,32 +266,60 @@ private IControlRecord CreateControlRecord(ScriptParam p, string? value, int ind
};
case PromptType.Dropdown:
var delimiterForOptions = p.GetPromptSettings("delimiter", x => x, ",");
- var initialOptions = p.GetPromptSettings("options", out var options) ? options.Split(delimiterForOptions):Array.Empty();
- var observableOptions = new ObservableCollection(initialOptions);
+ var dropdownOptions = p.GetDropdownOptions(delimiterForOptions);
+ var observableDropdownOptions = new ObservableCollection(dropdownOptions);
var searchable = p.GetPromptSettings("searchable", bool.Parse, false);
var optionsGeneratorCommand = p.GetPromptSettings("optionsGeneratorCommand", out var optionsGeneratorCommandText) ? optionsGeneratorCommandText : null;
-
- if (observableOptions.Count == 0 && string.IsNullOrWhiteSpace(value) == false && string.IsNullOrWhiteSpace(optionsGeneratorCommand) == false)
+ // Find selected item by matching value
+ DropdownOption? selectedOption = null;
+ if (!string.IsNullOrWhiteSpace(value))
{
- observableOptions.Add(value);
+ selectedOption = observableDropdownOptions.FirstOrDefault(opt => opt.Value == value);
+ if (selectedOption == null && string.IsNullOrWhiteSpace(optionsGeneratorCommand) == false)
+ {
+ // Add the value as a temporary option if not found and generator is available
+ selectedOption = new DropdownOption(value);
+ observableDropdownOptions.Add(selectedOption);
+ }
}
- Control inputControl = searchable ? new SearchableComboBox()
+ Control inputControl;
+
+ if (searchable)
{
- Items = observableOptions,
- SelectedItem = value,
- TabIndex = index,
- IsTabStop = true,
- Width = 500
- }: new ComboBox
- {
- ItemsSource = observableOptions,
- SelectedItem = value,
- TabIndex = index,
- IsTabStop = true,
- Width = 500
- };
+ // For searchable, convert to strings (SearchableComboBox only supports strings)
+ var stringOptions = new ObservableCollection(dropdownOptions.Select(o => o.Label));
+ var selectedString = selectedOption?.Label;
+
+ var searchBox = new SearchableComboBox()
+ {
+ Items = stringOptions,
+ TabIndex = index,
+ IsTabStop = true,
+ Width = 500
+ };
+
+ // Set selected item after Items collection is set
+ if (!string.IsNullOrWhiteSpace(selectedString) && stringOptions.Contains(selectedString))
+ {
+ searchBox.SelectedItem = selectedString;
+ }
+
+ inputControl = searchBox;
+ }
+ else
+ {
+ inputControl = new ComboBox
+ {
+ ItemsSource = observableDropdownOptions,
+ SelectedItem = selectedOption,
+ TabIndex = index,
+ IsTabStop = true,
+ Width = 500
+ };
+ }
+
var actionPanel = new StackPanel()
{
Orientation = Orientation.Horizontal,
@@ -293,24 +346,42 @@ private IControlRecord CreateControlRecord(ScriptParam p, string? value, int ind
var result = await commandExecutor($"Generate options for '{p.Name}'", optionsGeneratorCommand) ?? "";
Dispatcher.UIThread.Post(() =>
{
- observableOptions.Clear();
- foreach (var option in result.Split(new[]{"\r", "\n",delimiterForOptions}, StringSplitOptions.RemoveEmptyEntries).Distinct().OrderBy(x=>x))
+ var newOptions = result.Split(new[]{"\r", "\n",delimiterForOptions}, StringSplitOptions.RemoveEmptyEntries)
+ .Distinct()
+ .OrderBy(x=>x)
+ .Select(opt => new DropdownOption(opt.Trim()))
+ .ToList();
+
+ if (searchable && inputControl is SearchableComboBox searchBox)
{
- observableOptions.Add(option);
+ searchBox.Items.Clear();
+ foreach (var option in newOptions)
+ {
+ searchBox.Items.Add(option.Label);
+ }
}
+ else if (inputControl is ComboBox comboBox)
+ {
+ observableDropdownOptions.Clear();
+ foreach (var option in newOptions)
+ {
+ observableDropdownOptions.Add(option);
+ }
+ }
+
generateButton.Classes.Remove("spinning");
generateButton.IsEnabled = true;
wasGenerated = true;
- if (inputControl is SearchableComboBox scb)
+ if (inputControl is SearchableComboBox scb2)
{
- scb.ShowAll();
+ scb2.ShowAll();
}
});
};
generateButton.Click += generate;
- if(inputControl is SearchableComboBox scb)
+ if(inputControl is SearchableComboBox searchableBox)
{
- scb.GotFocus += (sender, args) =>
+ searchableBox.GotFocus += (sender, args) =>
{
if (wasGenerated == false)
{
@@ -326,17 +397,24 @@ private IControlRecord CreateControlRecord(ScriptParam p, string? value, int ind
return new DropdownControl
{
Control = actionPanel,
- InputControl = inputControl
+ InputControl = inputControl,
+ DropdownOptions = observableDropdownOptions
};
case PromptType.Multiselect:
var delimiter = p.GetPromptSettings("delimiter", s => s, ",");
+ var multiSelectOptions = p.GetDropdownOptions(delimiter);
+
+ // Parse selected values
+ var selectedValues = (value ?? string.Empty).Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim()).ToList();
+ var selectedDropdownOptions = multiSelectOptions.Where(opt => selectedValues.Contains(opt.Value)).ToList();
+
return new MultiSelectControl
{
Control = new CheckBoxListBox
{
SelectionMode = SelectionMode.Multiple,
- ItemsSource = p.GetPromptSettings("options", out var multiSelectOptions) ? multiSelectOptions.Split(delimiter) : Array.Empty(),
- SelectedItems = new AvaloniaList((value ?? string.Empty).Split(delimiter)),
+ ItemsSource = multiSelectOptions,
+ SelectedItems = new AvaloniaList(selectedDropdownOptions),
TabIndex = index,
IsTabStop = true,
BorderBrush = new SolidColorBrush(Color.Parse("#99ffffff")),
@@ -403,6 +481,15 @@ private IControlRecord CreateControlRecord(ScriptParam p, string? value, int ind
UncheckedValue = p.GetPromptSettings("uncheckedValue", out var uncheckedValue)? uncheckedValue: defaultUnchecked,
};
case PromptType.Multilinetext:
+ if (p.GetPromptSettings("syntax", out var syntax) && !string.IsNullOrWhiteSpace(syntax))
+ {
+ return new TextControl
+ {
+ Control = CreateAvaloniaEdit(value, index, syntax)
+ };
+ }
+
+ // Use regular TextBox without syntax highlighting
return new TextControl
{
Control = new TextBox
@@ -414,7 +501,6 @@ private IControlRecord CreateControlRecord(ScriptParam p, string? value, int ind
TabIndex = index,
IsTabStop = true,
Width = 500,
-
}
};
case PromptType.FileContent:
@@ -426,19 +512,11 @@ private IControlRecord CreateControlRecord(ScriptParam p, string? value, int ind
var templateText = p.GetPromptSettings("templateText", out var rawTemplate)? rawTemplate: "";
var textForControl = File.Exists(value) ? File.ReadAllText(value) : templateText;
-
- return new FileContent(p.GetPromptSettings("extension", out var extension)?extension:"dat")
+
+ var fileExtension = p.GetPromptSettings("extension", out var extension)?extension:"dat";
+ return new FileContent(fileExtension)
{
- Control = new TextBox
- {
- TextWrapping = TextWrapping.Wrap,
- AcceptsReturn = true,
- Height = 100,
- Text = textForControl,
- TabIndex = index,
- IsTabStop = true,
- Width = 500
- }
+ Control = CreateAvaloniaEdit(textForControl, index, fileExtension.TrimStart('.'))
};
case PromptType.FilePicker:
@@ -489,4 +567,31 @@ private IControlRecord CreateControlRecord(ScriptParam p, string? value, int ind
throw new ArgumentOutOfRangeException(nameof(p.Prompt), p.Prompt, null);
}
}
-}
\ No newline at end of file
+
+ private static TextEditor CreateAvaloniaEdit(string? value, int index, string syntax)
+ {
+ var textEditor = new TextEditor
+ {
+ Document = new TextDocument(value ?? string.Empty),
+ TabIndex = index,
+ MinHeight = 100,
+ Height = 100,
+ Width = 500,
+ ShowLineNumbers = true,
+ FontFamily = new FontFamily("Cascadia Code,Consolas,Menlo,Monospace"),
+ Background = new SolidColorBrush(Color.FromRgb(30, 30, 30)),
+ BorderBrush = new SolidColorBrush(Color.FromArgb(153, 255, 255,255)),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(3)
+ };
+ textEditor.TextArea.TextView.Margin = new Thickness(10, 0);
+ var registry = new RegistryOptions(ThemeName.DarkPlus);
+ TextMate.Installation textMateInstallation = textEditor.InstallTextMate(registry);
+ if (registry.GetLanguageByExtension("." + syntax) is { } languageByExtension)
+ {
+ textMateInstallation.SetGrammar(registry.GetScopeByLanguageId(languageByExtension.Id));
+ }
+
+ return textEditor;
+ }
+}
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Parameters/TextControl.cs b/src/ScriptRunner/ScriptRunner.GUI/Parameters/TextControl.cs
index 781e216..379d24a 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Parameters/TextControl.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/Parameters/TextControl.cs
@@ -1,4 +1,5 @@
using Avalonia.Controls;
+using AvaloniaEdit;
namespace ScriptRunner.GUI;
@@ -8,7 +9,12 @@ public class TextControl : IControlRecord
public string GetFormattedValue()
{
- return ((TextBox)Control).Text;
+ return Control switch
+ {
+ TextBox textBox => textBox.Text,
+ TextEditor textEditor => textEditor.Text,
+ _ => ((TextBox)Control).Text
+ };
}
public string Name { get; set; }
diff --git a/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/DropdownOption.cs b/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/DropdownOption.cs
new file mode 100644
index 0000000..7430fa7
--- /dev/null
+++ b/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/DropdownOption.cs
@@ -0,0 +1,25 @@
+namespace ScriptRunner.GUI.ScriptConfigs;
+
+///
+/// Represents a dropdown or multiselect option with a display label and value
+///
+public class DropdownOption
+{
+ public string Label { get; set; }
+ public string Value { get; set; }
+
+ public DropdownOption(string label, string value)
+ {
+ Label = label;
+ Value = value;
+ }
+
+ public DropdownOption(string value)
+ {
+ Label = value;
+ Value = value;
+ }
+
+ public override string ToString() => Label;
+}
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/ScriptConfig.cs b/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/ScriptConfig.cs
index fce31e5..6e63972 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/ScriptConfig.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/ScriptConfig.cs
@@ -3,6 +3,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Linq;
+using System.Text.Json;
using ScriptRunner.GUI.ViewModels;
@@ -87,6 +89,58 @@ public T GetPromptSettings(string name, Func convert, T @default)
return @default;
}
+
+ public List GetDropdownOptions(string delimiter = ",")
+ {
+ if (!PromptSettings.TryGetValue("options", out var optionsValue))
+ {
+ return new List();
+ }
+
+ // Case 1: String with comma-separated values
+ if (optionsValue is string optionsString)
+ {
+ return optionsString
+ .Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => new DropdownOption(x.Trim()))
+ .ToList();
+ }
+
+ // Case 2: JsonElement (from deserialization)
+ if (optionsValue is JsonElement jsonElement)
+ {
+ if (jsonElement.ValueKind == JsonValueKind.String)
+ {
+ return jsonElement.GetString()!
+ .Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => new DropdownOption(x.Trim()))
+ .ToList();
+ }
+
+ if (jsonElement.ValueKind == JsonValueKind.Array)
+ {
+ var result = new List();
+ foreach (var item in jsonElement.EnumerateArray())
+ {
+ if (item.ValueKind == JsonValueKind.String)
+ {
+ // Array of strings
+ result.Add(new DropdownOption(item.GetString()!));
+ }
+ else if (item.ValueKind == JsonValueKind.Object)
+ {
+ // Array of objects with label and value
+ var label = item.GetProperty("label").GetString()!;
+ var value = item.GetProperty("value").GetString()!;
+ result.Add(new DropdownOption(label, value));
+ }
+ }
+ return result;
+ }
+ }
+
+ return new List();
+ }
}
public class InteractiveInputDescription
@@ -99,4 +153,5 @@ public class InteractiveInputItem
{
public string Label { get; set; }
public string Value { get; set; }
-}
\ No newline at end of file
+}
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/ScriptRunner.GUI.csproj b/src/ScriptRunner/ScriptRunner.GUI/ScriptRunner.GUI.csproj
index e32acda..868f0d0 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/ScriptRunner.GUI.csproj
+++ b/src/ScriptRunner/ScriptRunner.GUI/ScriptRunner.GUI.csproj
@@ -34,6 +34,9 @@
+
+
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Scripts/DropdownOptionsExample.json b/src/ScriptRunner/ScriptRunner.GUI/Scripts/DropdownOptionsExample.json
new file mode 100644
index 0000000..cb627fe
--- /dev/null
+++ b/src/ScriptRunner/ScriptRunner.GUI/Scripts/DropdownOptionsExample.json
@@ -0,0 +1,66 @@
+{
+ "$schema": "../../../../schema/v1/ScriptRunnerSchema.json",
+ "actions": [
+ {
+ "name": "Coma separated",
+ "description": "Demonstrates different formats for dropdown options",
+ "command": "echo",
+ "autoParameterBuilderStyle": "powershell",
+ "params": [
+ {
+ "name": "Environment",
+ "description": "Coma separated string",
+ "prompt": "dropdown",
+ "promptSettings": {
+ "options": "Development,Staging,Production"
+ }
+ },
+ {
+ "name": "List",
+ "description": "List of strings",
+ "prompt": "dropdown",
+ "promptSettings": {
+ "options": ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"]
+ }
+ },
+ {
+ "name": "Size",
+ "description": "List of label/value objects",
+ "prompt": "dropdown",
+ "promptSettings": {
+ "options": [
+ {"label": "Small (1 CPU, 2GB RAM)", "value": "t2.small"},
+ {"label": "Medium (2 CPU, 4GB RAM)", "value": "t2.medium"},
+ {"label": "Large (4 CPU, 8GB RAM)", "value": "t2.large"},
+ {"label": "X-Large (8 CPU, 16GB RAM)", "value": "t2.xlarge"}
+ ]
+ }
+ },
+ {
+ "name": "Features",
+ "description": "Select features (multiSelect with array of strings)",
+ "prompt": "multiSelect",
+ "promptSettings": {
+ "options": ["Monitoring", "Backups", "Auto-scaling", "High Availability"],
+ "delimiter": ","
+ }
+ },
+ {
+ "name": "Services",
+ "description": "Select services (multiSelect with label/value objects)",
+ "prompt": "multiSelect",
+ "promptSettings": {
+ "options": [
+ {"label": "Web Server (HTTPS)", "value": "web-https"},
+ {"label": "Database (PostgreSQL)", "value": "db-postgres"},
+ {"label": "Cache (Redis)", "value": "cache-redis"},
+ {"label": "Queue (RabbitMQ)", "value": "queue-rabbitmq"}
+ ],
+ "delimiter": ","
+ }
+ }
+ ]
+ }
+ ]
+}
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Scripts/ScriptRunnerSchema.json b/src/ScriptRunner/ScriptRunner.GUI/Scripts/ScriptRunnerSchema.json
deleted file mode 100644
index df4c822..0000000
--- a/src/ScriptRunner/ScriptRunner.GUI/Scripts/ScriptRunnerSchema.json
+++ /dev/null
@@ -1,239 +0,0 @@
-{
- "$schema": "http://json-schema.org/schema",
- "$id": "ScriptRunnerSchema",
- "title": "Product",
- "description": "ScriptRunnerSchema",
- "type": "object",
- "properties": {
- "actions": {
- "description": "A list of available actions",
- "type": "array",
- "items": {
- "type": "object",
- "additionalProperties": false,
- "required": ["name", "command"],
- "properties": {
- "name": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "command": {
- "type": "string"
- },
- "workingDirectory": {
- "type": "string"
- },
- "installCommand":{
- "type": "string"
- },
- "installCommandWorkingDirectory":{
- "type": "string"
- },
- "predefinedArgumentSets":{
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "description":{
- "type":"string"
- },
- "fallbackToDefault":{
- "type":"boolean"
- },
- "arguments":{
- "type":"object"
- }
- }
- }
- },
- "predefinedArgumentSetsOrdering": {
- "type": "string",
- "enum": [
- "ascending",
- "descending"
- ]
- },
- "environmentVariables":{
- "type":"object",
- "additionalProperties": true
- },
- "params": {
- "type": "array",
- "items": {
- "type": "object",
- "additionalProperties": true,
- "properties": {
- "name": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "default": {
- "type": "string"
- },
- "prompt": {
- "type":"string",
- "enum": [
- "text",
- "multilineText",
- "password",
- "checkbox",
- "dropdown",
- "multiSelect",
- "filePicker",
- "directoryPicker",
- "datePicker",
- "numeric",
- "timePicker"
- ]
- }
- },
- "required": ["name", "prompt"],
- "anyOf": [
- {
- "properties": {
- "prompt": {
- "const": "datePicker"
- },
- "promptSettings": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "format": {
- "type": "string"
- },
- "yearVisible": {
- "type": "string"
- },
- "monthVisible": {
- "type": "string"
- },
- "dayVisible": {
- "type": "string"
- },
- "todayAsDefault": {
- "type": "string"
- }
- }
- }
- },
- "required": ["prompt"]
- },
- {
- "properties": {
- "prompt": {
- "const": "timePicker"
- },
- "promptSettings": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "format": {
- "type": "string"
- }
- }
- }
- },
- "required": ["prompt"]
- },
- {
- "properties": {
- "prompt": {
- "const": "dropdown"
- },
- "promptSettings": {
- "type": "object",
- "properties": {
- "options": {
- "type": "string"
- }
- }
- }
- },
- "required": ["prompt"]
- },
- {
- "properties": {
- "prompt": {
- "const": "multiSelect"
- },
- "promptSettings": {
- "type": "object",
- "properties": {
- "options": {
- "type": "string"
- },
- "delimiter": {
- "type": "string"
- }
- }
- }
- },
- "required": ["prompt"]
- },
- {
- "properties": {
- "prompt": {
- "const": "checkbox"
- },
- "promptSettings": {
- "type": "object",
- "properties": {
- "checkedValue": {
- "type": "string"
- },
- "uncheckedValue": {
- "type": "string"
- }
- }
- }
- },
- "required": ["prompt"]
- },
- {
- "properties": {
- "prompt": {
- "const": "numeric"
- },
- "promptSettings": {
- "type": "object",
- "properties": {
- "min": {
- "type": "string"
- },
- "max": {
- "type": "string"
- },
- "step": {
- "type": "string"
- }
- }
- }
- },
- "required": ["prompt"]
- },
- {
- "properties": {
- "prompt": {
- "enum": [
- "text",
- "multilineText",
- "password",
- "filePicker",
- "directoryPicker"
- ]
- }
- },
- "required": ["prompt"]
- }
- ]
- }
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Scripts/TextInputScript.json b/src/ScriptRunner/ScriptRunner.GUI/Scripts/TextInputScript.json
index 7196f6b..a67b688 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Scripts/TextInputScript.json
+++ b/src/ScriptRunner/ScriptRunner.GUI/Scripts/TextInputScript.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://raw.githubusercontent.com/cezarypiatek/ScriptRunnerPOC/main/schema/v1/ScriptRunnerSchema.json",
+ "$schema": "../../../../schema/v1/ScriptRunnerSchema.json",
"actions": [
{
"name": "Ping",
@@ -25,9 +25,9 @@
"address": "wwww.github.com"
}
}
- ],
- "predefinedArgumentSetsOrdering": "ascending"
- },{
+ ]
+ },
+ {
"name": "AllControlsTest",
"description": "Demo of all available controls",
"command": "pwsh.exe -NoProfile -Command Write-Host '{p1} {p2} {p3} {p4} {p5} {p6} {p7} {p8} {p9} {p10} {p11} {p12}'",
@@ -44,6 +44,25 @@
"default": "Line 1\r\n Line 2",
"prompt": "multilineText"
},
+ {
+ "name": "p2",
+ "description": "Multiline JSONr",
+ "default": "Line 1\r\n Line 2",
+ "prompt": "multilineText",
+ "promptSettings": {
+ "syntax": "json"
+ }
+ },
+ {
+ "name": "p22",
+ "description": "Fil Content",
+ "default": "using System;\r\n\r\nclass Program {\r\n static void Main() {\r\n Console.WriteLine(\"Hello, World!\");\r\n }\r\n}",
+ "prompt": "fileContent",
+ "promptSettings": {
+ "extension": "cs",
+ "templateText": "using System;\r\n\r\nclass Program {\r\n static void Main() {\r\n Console.WriteLine(\"Hello, World!\");\r\n }\r\n}"
+ }
+ },
{
"name": "p3",
"description": "Checkbox parameter",
@@ -52,7 +71,7 @@
"promptSettings":{
"checkedValue": "checked",
"uncheckedValue": "unchecked"
- }
+ }
},
{
"name": "p4",
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Themes/StyleClasses.axaml b/src/ScriptRunner/ScriptRunner.GUI/Themes/StyleClasses.axaml
index 2d28ef0..bfaa321 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Themes/StyleClasses.axaml
+++ b/src/ScriptRunner/ScriptRunner.GUI/Themes/StyleClasses.axaml
@@ -1,7 +1,8 @@
+ xmlns:tanker="https://github.com/projektanker/icons.avalonia"
+ xmlns:gui="clr-namespace:ScriptRunner.GUI">
@@ -118,19 +119,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
-
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -185,17 +211,10 @@
Compacted
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/ActionDetailsSection.axaml.cs b/src/ScriptRunner/ScriptRunner.GUI/Views/ActionDetailsSection.axaml.cs
index b838245..80a6244 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Views/ActionDetailsSection.axaml.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/ActionDetailsSection.axaml.cs
@@ -65,4 +65,12 @@ private void SplitButton_OnClick(object? sender, RoutedEventArgs e)
else sp.Flyout.ShowAt(sp);
}
}
+
+ private void OnActionPanelScrollChange(object? sender, ScrollChangedEventArgs e)
+ {
+ if (sender is ScrollViewer sc && e.ExtentDelta.Y > 0)
+ {
+ sc.ScrollToHome();
+ }
+ }
}
\ No newline at end of file
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/ActionsList.axaml b/src/ScriptRunner/ScriptRunner.GUI/Views/ActionsList.axaml
index 5231dbd..8176b14 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Views/ActionsList.axaml
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/ActionsList.axaml
@@ -4,24 +4,31 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:ScriptRunner.GUI.ViewModels"
xmlns:scriptConfigs="clr-namespace:ScriptRunner.GUI.ScriptConfigs"
+ xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="viewModels:MainWindowViewModel"
x:Class="ScriptRunner.GUI.Views.ActionsList">
-
+
-
+
+
+
+
-
+
+
+
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/ContentWithSidebar.axaml b/src/ScriptRunner/ScriptRunner.GUI/Views/ContentWithSidebar.axaml
index 67862a5..d16a253 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Views/ContentWithSidebar.axaml
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/ContentWithSidebar.axaml
@@ -9,11 +9,11 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/DatePickerOverlay.axaml.cs b/src/ScriptRunner/ScriptRunner.GUI/Views/DatePickerOverlay.axaml.cs
new file mode 100644
index 0000000..8d5694e
--- /dev/null
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/DatePickerOverlay.axaml.cs
@@ -0,0 +1,43 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using ScriptRunner.GUI.ViewModels;
+
+namespace ScriptRunner.GUI.Views;
+
+public partial class DatePickerOverlay : UserControl
+{
+ public DatePickerOverlay()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void OnOverlayClicked(object? sender, PointerPressedEventArgs e)
+ {
+ // Close the overlay when clicking on the background
+ if (DataContext is MainWindowViewModel viewModel)
+ {
+ viewModel.IsDatePickerVisible = false;
+ }
+ }
+
+ private void OnDateItemClicked(object? sender, PointerPressedEventArgs e)
+ {
+ if (sender is Border border && border.DataContext is DateGroupInfo dateInfo)
+ {
+ if (DataContext is MainWindowViewModel viewModel)
+ {
+ viewModel.ScrollToDate(dateInfo.Date);
+ }
+ }
+ e.Handled = true;
+ }
+}
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/ExecutionLogList.axaml b/src/ScriptRunner/ScriptRunner.GUI/Views/ExecutionLogList.axaml
new file mode 100644
index 0000000..5336ba4
--- /dev/null
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/ExecutionLogList.axaml
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/ExecutionLogList.axaml.cs b/src/ScriptRunner/ScriptRunner.GUI/Views/ExecutionLogList.axaml.cs
new file mode 100644
index 0000000..2f4081d
--- /dev/null
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/ExecutionLogList.axaml.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+using ScriptRunner.GUI.ViewModels;
+
+namespace ScriptRunner.GUI.Views;
+
+public partial class ExecutionLogList : UserControl
+{
+ public static readonly StyledProperty?> ItemsProperty =
+ AvaloniaProperty.Register?>(nameof(Items));
+
+ public static readonly StyledProperty?> GroupedItemsProperty =
+ AvaloniaProperty.Register?>(nameof(GroupedItems));
+
+ public static readonly StyledProperty SelectedItemProperty =
+ AvaloniaProperty.Register(nameof(SelectedItem), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
+
+ public static readonly StyledProperty SelectedLogItemProperty =
+ AvaloniaProperty.Register(nameof(SelectedLogItem), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
+
+ public static readonly StyledProperty ShowDatePickerProperty =
+ AvaloniaProperty.Register(nameof(ShowDatePicker), defaultValue: false);
+
+ // Event for when date header is clicked
+ public event EventHandler? DateHeaderClicked;
+
+ private INotifyCollectionChanged? _currentCollection;
+
+ public IEnumerable? Items
+ {
+ get => GetValue(ItemsProperty);
+ set => SetValue(ItemsProperty, value);
+ }
+
+ public IEnumerable? GroupedItems
+ {
+ get => GetValue(GroupedItemsProperty);
+ private set => SetValue(GroupedItemsProperty, value);
+ }
+
+ public ExecutionLogAction? SelectedItem
+ {
+ get => GetValue(SelectedItemProperty);
+ set => SetValue(SelectedItemProperty, value);
+ }
+
+ public ExecutionLogItemBase? SelectedLogItem
+ {
+ get => GetValue(SelectedLogItemProperty);
+ set => SetValue(SelectedLogItemProperty, value);
+ }
+
+ public bool ShowDatePicker
+ {
+ get => GetValue(ShowDatePickerProperty);
+ set => SetValue(ShowDatePickerProperty, value);
+ }
+
+ public ExecutionLogList()
+ {
+ InitializeComponent();
+
+ // Watch for changes to Items and rebuild grouped list
+ this.GetObservable(ItemsProperty).Subscribe(items =>
+ {
+ // Unsubscribe from old collection
+ if (_currentCollection != null)
+ {
+ _currentCollection.CollectionChanged -= OnCollectionChanged;
+ }
+
+ // Subscribe to new collection if it's observable
+ if (items is INotifyCollectionChanged observable)
+ {
+ _currentCollection = observable;
+ observable.CollectionChanged += OnCollectionChanged;
+ }
+ else
+ {
+ _currentCollection = null;
+ }
+
+ RebuildGroupedList();
+ });
+
+ // Watch for changes to SelectedLogItem and update SelectedItem
+ this.GetObservable(SelectedLogItemProperty).Subscribe(item =>
+ {
+ if (item is ExecutionLogDateHeader)
+ {
+ // Ignore date header selections
+ SelectedLogItem = null;
+ return;
+ }
+
+ if (item is ExecutionLogItemAction actionItem)
+ {
+ SelectedItem = actionItem.Action;
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void RebuildGroupedList()
+ {
+ if (Items == null)
+ {
+ GroupedItems = null;
+ return;
+ }
+
+ var items = new List();
+ DateTime? lastDate = null;
+
+ foreach (var action in Items)
+ {
+ var actionDate = action.Timestamp.Date;
+
+ // Add date header if the date changed
+ if (lastDate == null || lastDate != actionDate)
+ {
+ items.Add(new ExecutionLogDateHeader(actionDate));
+ lastDate = actionDate;
+ }
+
+ items.Add(new ExecutionLogItemAction(action));
+ }
+
+ GroupedItems = items;
+ }
+
+ private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ RebuildGroupedList();
+ }
+
+ public void OnDateHeaderClicked(object? sender, PointerPressedEventArgs e)
+ {
+ // Show the date picker overlay when a date header is clicked (if enabled)
+ if (ShowDatePicker)
+ {
+ // Raise the event so parent can show date picker
+ DateHeaderClicked?.Invoke(this, EventArgs.Empty);
+ }
+ e.Handled = true;
+ }
+
+ public async Task ScrollToDate(DateTime date)
+ {
+ var listBox = this.FindControl("ExecutionLogListBox");
+ if (listBox == null || GroupedItems == null) return;
+
+ var items = GroupedItems.ToList();
+ var targetItem = items.FirstOrDefault(item =>
+ item is ExecutionLogDateHeader header && header.Date == date.Date);
+
+ if (targetItem != null)
+ {
+ listBox.ScrollIntoView(targetItem);
+
+ await Task.Delay(200);
+
+ var itemContainer = listBox.ContainerFromItem(targetItem);
+ if (itemContainer != null)
+ {
+ var border = itemContainer.GetVisualDescendants()
+ .OfType()
+ .FirstOrDefault(b => b.Name == "DateHeaderBorder");
+
+ if (border != null)
+ {
+ border.Classes.Add("highlight");
+ await Task.Delay(2000);
+ border.Classes.Remove("highlight");
+ }
+ }
+ }
+ }
+}
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml b/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml
index 32f8a5e..cec6890 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml
@@ -24,16 +24,6 @@
BorderBrush="#3baced">
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -86,6 +74,18 @@
+
+
+
+
+
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml.cs b/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml.cs
index 984c910..e454731 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml.cs
@@ -34,6 +34,26 @@ public MainWindow()
InitializeComponent();
ViewModel = Locator.Current.GetService();
Title = $"ScriptRunner {this.GetType().Assembly.GetName().Version}";
+
+ // Set up scroll action for date navigation
+ if (ViewModel != null)
+ {
+ ViewModel.ScrollToDateAction = ScrollToDate;
+ }
+
+ // Subscribe to date header click event from ExecutionLogList control
+ var executionLogList = this.FindControl("ExecutionLogListControl");
+ if (executionLogList != null)
+ {
+ executionLogList.DateHeaderClicked += (sender, args) =>
+ {
+ if (ViewModel != null)
+ {
+ ViewModel.IsDatePickerVisible = true;
+ }
+ };
+ }
+
if (AppSettingsService.Load().Layout is { } layoutSettings)
{
@@ -69,4 +89,14 @@ public MainWindow()
});
}
}
+
+ private async void ScrollToDate(DateTime date)
+ {
+ // Find the ExecutionLogList control
+ var executionLogList = this.FindControl("ExecutionLogListControl");
+ if (executionLogList != null)
+ {
+ await executionLogList.ScrollToDate(date);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml b/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml
index 098e40c..d49d0f9 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml
@@ -11,6 +11,13 @@
x:DataType="viewModels:MainWindowViewModel"
>
+
+
+
+
+
+
+
@@ -73,6 +80,7 @@
+ Follow output
@@ -83,3 +91,4 @@
+
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml.cs b/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml.cs
index 8be5aa9..6a580ad 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml.cs
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml.cs
@@ -10,6 +10,8 @@ namespace ScriptRunner.GUI.Views;
public partial class RunningJobsSection : UserControl
{
+ private bool _isUserScrolling = false;
+
public RunningJobsSection()
{
InitializeComponent();
@@ -22,9 +24,30 @@ private void InitializeComponent()
private void ScrollChangedHandler(object? sender, ScrollChangedEventArgs e)
{
- if (sender is ScrollViewer sc && e.ExtentDelta.Y > 0)
+ if (sender is ScrollViewer sc && sc.DataContext is RunningJobViewModel viewModel)
{
- sc.ScrollToEnd();
+ // If content was added (extent changed), auto-scroll if follow output is enabled
+ if (e.ExtentDelta.Y > 0 && viewModel.FollowOutput)
+ {
+ _isUserScrolling = false;
+ sc.ScrollToEnd();
+ }
+ // If user manually scrolled (offset changed without extent change)
+ else if (e.OffsetDelta.Y != 0 && e.ExtentDelta.Y == 0)
+ {
+ _isUserScrolling = true;
+ // Check if user scrolled away from bottom
+ var isAtBottom = Math.Abs(sc.Offset.Y - sc.ScrollBarMaximum.Y) < 1.0;
+ if (!isAtBottom && viewModel.FollowOutput)
+ {
+ viewModel.FollowOutput = false;
+ }
+ // If user scrolled back to bottom, re-enable follow output
+ else if (isAtBottom && !viewModel.FollowOutput)
+ {
+ viewModel.FollowOutput = true;
+ }
+ }
}
}
diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/SideMenu.axaml b/src/ScriptRunner/ScriptRunner.GUI/Views/SideMenu.axaml
index 637f78b..ece75c8 100644
--- a/src/ScriptRunner/ScriptRunner.GUI/Views/SideMenu.axaml
+++ b/src/ScriptRunner/ScriptRunner.GUI/Views/SideMenu.axaml
@@ -7,7 +7,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="viewModels:MainWindowViewModel"
x:Class="ScriptRunner.GUI.Views.SideMenu">
-
+