Skip to content
81 changes: 81 additions & 0 deletions src/ScriptRunner/ScriptRunner.GUI/Controls/FormattedTextEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public FormattedTextEditor()
Padding = new Thickness(15);
TextArea.TextView.LinkTextForegroundBrush = Brushes.LightBlue;
TextArea.TextView.ElementGenerators.Add(new FilePathElementGenerator());

// Add context menu
ContextMenu = CreateContextMenu();

// Subscribe to scroll changes
this.Loaded += (_, _) =>
{
Expand All @@ -60,6 +64,83 @@ public FormattedTextEditor()
};
}

private ContextMenu CreateContextMenu()
{
var contextMenu = new ContextMenu();

var copySelectedItem = new MenuItem
{
Header = "Copy Selected"
};
copySelectedItem.Click += (_, _) =>
{
if (!string.IsNullOrEmpty(SelectedText))
{
CopyToClipboard(SelectedText);
}
};

var copyAllItem = new MenuItem
{
Header = "Copy All"
};
copyAllItem.Click += (_, _) =>
{
if (Document != null)
{
CopyToClipboard(Document.Text);
}
};

var selectAllItem = new MenuItem
{
Header = "Select All"
};
selectAllItem.Click += (_, _) =>
{
if (Document != null)
{
SelectionStart = 0;
SelectionLength = Document.TextLength;
}
};

var searchItem = new MenuItem
{
Header = "Search (Ctrl+F)"
};
searchItem.Click += (_, _) =>
{
// Trigger the built-in search functionality
var searchPanel = AvaloniaEdit.Search.SearchPanel.Install(this);
searchPanel?.Open();
};

contextMenu.Items.Add(copySelectedItem);
contextMenu.Items.Add(copyAllItem);
contextMenu.Items.Add(selectAllItem);
contextMenu.Items.Add(new Separator());
contextMenu.Items.Add(searchItem);

// Update menu items based on selection when opening
contextMenu.Opening += (_, _) =>
{
copySelectedItem.IsEnabled = !string.IsNullOrEmpty(SelectedText);
copyAllItem.IsEnabled = Document != null && !string.IsNullOrEmpty(Document.Text);
};

return contextMenu;
}

private async void CopyToClipboard(string text)
{
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
if (clipboard != null)
{
await clipboard.SetTextAsync(text);
}
}

private void OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
ScrollChanged?.Invoke(this, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class ScriptConfig
public List<TroubleshootingItem> Troubleshooting { get; set; } = new();
public List<TroubleshootingItem> InstallTroubleshooting { get; set; } = new();
public InlineCollection CommandFormatted { get; set; } = new();
public InlineCollection InstallCommandFormatted { get; set; } = new();
}

public class TroubleshootingItem
Expand Down Expand Up @@ -154,4 +155,3 @@ public class InteractiveInputItem
public string Label { get; set; }
public string Value { get; set; }
}

11 changes: 11 additions & 0 deletions src/ScriptRunner/ScriptRunner.GUI/ScriptReader/ConfigLoadResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using ScriptRunner.GUI.ScriptConfigs;

namespace ScriptRunner.GUI.ScriptReader;

public class ConfigLoadResult
{
public List<ScriptConfig> Configs { get; } = new();
public List<string> CorruptedFiles { get; } = new();
}

222 changes: 220 additions & 2 deletions src/ScriptRunner/ScriptRunner.GUI/ScriptReader/ScriptConfigReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,43 @@ namespace ScriptRunner.GUI.ScriptReader;

public static class ScriptConfigReader
{
public static ConfigLoadResult LoadWithErrorTracking(ConfigScriptEntry source,
ScriptRunnerAppSettings appSettings)
{
var result = new ConfigLoadResult();

if (string.IsNullOrWhiteSpace(source.Path))
{
return result;
}

if (source.Type == ConfigScriptType.File)
{
if (File.Exists(source.Path) == false)
{
return result;
}

LoadFileSourceWithTracking(source.Path, appSettings, result, source.Name);
return result;
}

if (source.Type == ConfigScriptType.Directory)
{
if (Directory.Exists(source.Path) == false)
{
return result;
}

foreach (var file in Directory.EnumerateFiles(source.Path, "*.json", source.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
{
LoadFileSourceWithTracking(file, appSettings, result, source.Name);
}
}

return result;
}

public static IEnumerable<ScriptConfig> Load(ConfigScriptEntry source,
ScriptRunnerAppSettings appSettings)
{
Expand Down Expand Up @@ -103,6 +140,166 @@ private static IAutoParameterBuilder CreateBuilder(ScriptConfig scriptConfig)
return EmptyAutoParameterBuilder.Instance;
}

private static void LoadFileSourceWithTracking(string fileName,
ScriptRunnerAppSettings appSettings, ConfigLoadResult result, string sourceName)
{
if (!File.Exists(fileName)) return;

try
{
var jsonString = File.ReadAllText(fileName);

if (jsonString.Contains("ScriptRunnerSchema.json") == false)
{
return;
}

var scriptConfig = JsonSerializer.Deserialize<ActionsConfig>(jsonString, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
Converters = { new PromptTypeJsonConverter(), new ParamTypeJsonConverter(), new JsonStringEnumConverter() }
})!;

foreach (var action in scriptConfig.Actions)
{
action.Source = fileName;
action.SourceName = sourceName;
action.Categories ??= new List<string>();

var mainCategory = string.IsNullOrWhiteSpace(action.SourceName) == false
? action.SourceName
: Path.GetFileName(fileName);
if (string.IsNullOrWhiteSpace(mainCategory) == false)
{
action.Categories.Add(mainCategory);
}

var parameterBuilder = CreateBuilder(action);

var actionDir = Path.GetDirectoryName(fileName);

string ResolveAbsolutePath(string path)
{
if (string.IsNullOrWhiteSpace(path) == false)
{
if (Path.IsPathRooted(path) == false)
{
return Path.GetFullPath(Path.Combine(actionDir!, path));
}
}

return path;
}

[return:NotNullIfNotNull("command")]
string? AdjustCommandPath(string? command)
{
if (string.IsNullOrWhiteSpace(command) == false && (command.StartsWith(".")))
{
var (commandPath, args) = MainWindowViewModel.SplitCommandAndArgs(command);
return (ResolveAbsolutePath(commandPath) + " " + args).Trim();
}

return command;
}

action.Command = AdjustCommandPath(action.Command);
action.InstallCommand = AdjustCommandPath(action.InstallCommand);

var autoGeneratedParameters = action.Params.Where(x=>x.SkipFromAutoParameterBuilder == false).Select(param => parameterBuilder.Build(param)).Where(paramString => string.IsNullOrWhiteSpace(paramString) == false);
action.Command += " "+string.Join(" ", autoGeneratedParameters);

if (string.IsNullOrWhiteSpace(action.Docs) == false )
{
var docPaths = ResolveAbsolutePath(action.Docs);
if (File.Exists(docPaths))
{
action.HasDocs = true;
action.DocsContent = File.ReadAllText(docPaths);
action.DocAssetPath = Path.GetDirectoryName(docPaths);
}
}

action.WorkingDirectory = string.IsNullOrWhiteSpace(action.WorkingDirectory) ? actionDir : ResolveAbsolutePath(action.WorkingDirectory);
action.InstallCommandWorkingDirectory = string.IsNullOrWhiteSpace(action.InstallCommandWorkingDirectory) ? actionDir : ResolveAbsolutePath(action.InstallCommandWorkingDirectory);

var defaultSet = new ArgumentSet()
{
Description = MainWindowViewModel.DefaultParameterSetName
};

foreach (var param in action.Params)
{
param.ValueGeneratorCommand = AdjustCommandPath(param.ValueGeneratorCommand);
defaultSet.Arguments[param.Name] = param.Default;
}

foreach (var set in action.PredefinedArgumentSets.Where(x => x.FallbackToDefault))
{
foreach (var (key, val) in defaultSet.Arguments)
{
if (set.Arguments.ContainsKey(key) == false)
{
set.Arguments[key] = val;
}
}
}

if (appSettings.ExtraParameterSets?.Where(x => x.ActionName == action.Name).ToList() is { } extraSets)
{
foreach (var extraSet in extraSets.Select(x => new ArgumentSet
{
Description = x.Description,
Arguments = x.Arguments
}))
{
var existing = action.PredefinedArgumentSets.FirstOrDefault(x => x.Description == extraSet.Description);
if (existing != null)
{
action.PredefinedArgumentSets[action.PredefinedArgumentSets.IndexOf(existing)] = extraSet;
}
else
{
action.PredefinedArgumentSets.Add(extraSet);
}
}
}

switch (action.PredefinedArgumentSetsOrdering)
{
case PredefinedArgumentSetsOrdering.Ascending:
action.PredefinedArgumentSets.Sort((s1, s2) => string.CompareOrdinal(s1.Description, s2.Description));
break;
case PredefinedArgumentSetsOrdering.Descending:
action.PredefinedArgumentSets.Sort((s1, s2) => string.CompareOrdinal(s2.Description, s1.Description));
break;
}

action.PredefinedArgumentSets.Insert(0, defaultSet);

foreach (var param in action.Params.Where(x=>x.Prompt == PromptType.FileContent))
{
foreach (var set in action.PredefinedArgumentSets)
{
if (set.Arguments.TryGetValue(param.Name, out var defaultValue))
{
set.Arguments[param.Name] = ResolveAbsolutePath(defaultValue);
}
}
}



result.Configs.Add(action);
}
}
catch (Exception ex)
{
result.CorruptedFiles.Add(fileName);
}
}

private static IEnumerable<ScriptConfig> LoadFileSource(string fileName,
ScriptRunnerAppSettings appSettings)
{
Expand Down Expand Up @@ -217,8 +414,6 @@ string ResolveAbsolutePath(string path)
action.PredefinedArgumentSets.Add(extraSet);
}
}


}

switch (action.PredefinedArgumentSetsOrdering)
Expand Down Expand Up @@ -263,6 +458,29 @@ string ResolveAbsolutePath(string path)

return inline;
}));

// Format InstallCommand if it exists
if (!string.IsNullOrWhiteSpace(action.InstallCommand))
{
var installWithMarkers = action.Params.Aggregate
(
seed: action.InstallCommand,
func: (string accumulate, ScriptParam source) =>
accumulate.Replace("{" + source.Name + "}", "[!@#]{" + source.Name + "}[!@#]")
);

action.InstallCommandFormatted.AddRange(installWithMarkers.Split("[!@#]").Select(x =>
{
var inline = new Run(x);
if (x.StartsWith("{"))
{
inline.Foreground = Brushes.LightGreen;
inline.FontWeight = FontWeight.ExtraBold;
}

return inline;
}));
}
}

return scriptConfig.Actions;
Expand Down
1 change: 0 additions & 1 deletion src/ScriptRunner/ScriptRunner.GUI/ScriptRunner.GUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<Folder Include="ScriptReader\" />
<None Remove=".gitignore" />
<None Remove="Themes\DarkTheme.xaml" />
</ItemGroup>
Expand Down
Loading
Loading