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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ For documentation-only changes, explain why build or test commands were skipped.
- Radial Actions is a lightweight Windows utility opened by a global hotkey. Startup, hotkey registration, tray behavior, and menu display should stay fast and predictable.
- User-configured actions must not execute during tests, settings load, preview rendering, or validation.
- Invalid or stale settings should normalize to safe defaults instead of crashing on startup.
- Shell actions must preserve explicit target, arguments, and working directory behavior.
- Open actions (formerly Shell) must preserve explicit target, arguments, and working directory behavior.
- Key actions must accept known media/volume actions and validated custom shortcuts; invalid shortcuts should fail clearly.
- Update checks must remain controlled by settings and should not introduce surprise network work in deterministic tests.

Expand Down
14 changes: 7 additions & 7 deletions RadialActions.Tests/Actions/ActionDefaultsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void ApplyKeyDefaults_UpdatesIconOnlyForBlankDefaultOrPreviousAutoValue()
}

[Fact]
public void ApplyShellDefaults_PreservesManualNameIconAndWorkingDirectory()
public void ApplyOpenDefaults_PreservesManualNameIconAndWorkingDirectory()
{
Directory.CreateDirectory(_tempRoot);
var firstRoot = Path.Combine(_tempRoot, "First");
Expand All @@ -42,29 +42,29 @@ public void ApplyShellDefaults_PreservesManualNameIconAndWorkingDirectory()
var secondPath = Path.Combine(secondRoot, "Second.exe");

var service = new ActionDefaultsService();
var action = PieAction.CreateShellAction("Manual Name", firstPath, "\U0001F6E0\uFE0F", workingDirectory: _tempRoot);
var action = PieAction.CreateOpenAction("Manual Name", firstPath, "\U0001F6E0\uFE0F", workingDirectory: _tempRoot);

service.TrackExistingDefaults([action]);

action.Parameter = secondPath;
service.ApplyShellDefaults(action, action.Parameter);
service.ApplyOpenDefaults(action, action.Parameter);

Assert.Equal("Manual Name", action.Name);
Assert.Equal("\U0001F6E0\uFE0F", action.Icon);
Assert.Equal(_tempRoot, action.WorkingDirectory);
}

[Fact]
public void ApplyShellDefaults_LegacyStarIcon_UpgradesToSelectedDefaultIcon()
public void ApplyOpenDefaults_LegacyStarIcon_UpgradesToSelectedDefaultIcon()
{
Directory.CreateDirectory(_tempRoot);
var targetPath = Path.Combine(_tempRoot, "First.exe");
var service = new ActionDefaultsService();
var action = PieAction.CreateShellAction(PieAction.DefaultName, targetPath, ActionDefaultsService.LegacyDefaultIcon);
var action = PieAction.CreateOpenAction(PieAction.DefaultName, targetPath, ActionDefaultsService.LegacyDefaultIcon);

service.ApplyShellDefaults(action, action.Parameter);
service.ApplyOpenDefaults(action, action.Parameter);

Assert.Equal(ShellActionDefaults.FileIcon, action.Icon);
Assert.Equal(OpenActionDefaults.FileIcon, action.Icon);
}

public void Dispose()
Expand Down
16 changes: 8 additions & 8 deletions RadialActions.Tests/Actions/ActionDropFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,20 +157,20 @@ public void CreateAction_BlankTarget_UsesDefaultNameAndFileIcon()
{
var action = ActionDropFactory.CreateAction(string.Empty);

Assert.Equal(ActionType.Shell, action.Type);
Assert.Equal(ActionType.Open, action.Type);
Assert.Equal(PieAction.DefaultName, action.Name);
Assert.Equal(ShellActionDefaults.FileIcon, action.Icon);
Assert.Equal(OpenActionDefaults.FileIcon, action.Icon);
}

[Fact]
public void CreateAction_Url_FillsWebDefaults()
{
var action = ActionDropFactory.CreateAction("https://example.com/path");

Assert.Equal(ActionType.Shell, action.Type);
Assert.Equal(ActionType.Open, action.Type);
Assert.Equal("https://example.com/path", action.Parameter);
Assert.Equal("example.com", action.Name);
Assert.Equal(ShellActionDefaults.WebIcon, action.Icon);
Assert.Equal(OpenActionDefaults.WebIcon, action.Icon);
Assert.Equal(string.Empty, action.WorkingDirectory);
}

Expand All @@ -183,10 +183,10 @@ public void CreateAction_Directory_FillsFolderDefaults()

var action = ActionDropFactory.CreateAction(folder);

Assert.Equal(ActionType.Shell, action.Type);
Assert.Equal(ActionType.Open, action.Type);
Assert.Equal(folder, action.Parameter);
Assert.Equal("Games", action.Name);
Assert.Equal(ShellActionDefaults.FolderIcon, action.Icon);
Assert.Equal(OpenActionDefaults.FolderIcon, action.Icon);
Assert.Equal(folder, action.WorkingDirectory);
}

Expand All @@ -199,10 +199,10 @@ public void CreateAction_File_FillsFileDefaults()

var action = ActionDropFactory.CreateAction(file);

Assert.Equal(ActionType.Shell, action.Type);
Assert.Equal(ActionType.Open, action.Type);
Assert.Equal(file, action.Parameter);
Assert.Equal("Notes", action.Name);
Assert.Equal(ShellActionDefaults.FileIcon, action.Icon);
Assert.Equal(OpenActionDefaults.FileIcon, action.Icon);
Assert.Equal(_tempRoot, action.WorkingDirectory);
}

Expand Down
4 changes: 2 additions & 2 deletions RadialActions.Tests/Actions/ActionsSettingsViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public void AddDroppedTargets_InsertsAfterSelectionAndSelectsLast()
var addedA = viewModel.Actions[2];
var addedB = viewModel.Actions[3];
Assert.Equal([first, second, addedA, addedB, third], viewModel.Actions);
Assert.Equal(ActionType.Shell, addedA.Type);
Assert.Equal(ActionType.Open, addedA.Type);
Assert.Equal("https://a.com", addedA.Parameter);
Assert.Equal("https://b.com", addedB.Parameter);
Assert.Same(addedB, viewModel.SelectedAction);
Expand Down Expand Up @@ -121,7 +121,7 @@ public void ActionEditorViewModel_ActionTypes_HidesNoneType()
{
var viewModel = new ActionEditorViewModel(new ActionDefaultsService(), []);

Assert.Equal([ActionType.Key, ActionType.Shell], viewModel.ActionTypes.Select(option => option.Type));
Assert.Equal([ActionType.Key, ActionType.Open], viewModel.ActionTypes.Select(option => option.Type));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
namespace RadialActions.Tests;

public sealed class ShellActionDefaultsTests : IDisposable
public sealed class OpenActionDefaultsTests : IDisposable
{
private readonly string _tempRoot = Path.Combine(Path.GetTempPath(), "RadialActions.Tests", Guid.NewGuid().ToString("N"));

[Fact]
public void GetDefaults_EmptyTarget_ReturnsNull()
{
Assert.Null(ShellActionDefaults.FromTarget(string.Empty));
Assert.Null(OpenActionDefaults.FromTarget(string.Empty));
}

[Fact]
public void GetDefaults_Url_UsesHostNameAndWebIcon()
{
var defaults = ShellActionDefaults.FromTarget("https://docs.github.com/en");
var defaults = OpenActionDefaults.FromTarget("https://docs.github.com/en");

Assert.Equal(new ShellActionDefaults("docs.github.com", ShellActionDefaults.WebIcon, string.Empty), defaults);
Assert.Equal(new OpenActionDefaults("docs.github.com", OpenActionDefaults.WebIcon, string.Empty), defaults);
}

[Fact]
Expand All @@ -25,9 +25,9 @@ public void GetDefaults_FilePath_UsesFileNameIconAndWorkingDirectory()
var filePath = Path.Combine(_tempRoot, "Example App.exe");
File.WriteAllText(filePath, "test");

var defaults = ShellActionDefaults.FromTarget(filePath);
var defaults = OpenActionDefaults.FromTarget(filePath);

Assert.Equal(new ShellActionDefaults("Example App", ShellActionDefaults.FileIcon, _tempRoot), defaults);
Assert.Equal(new OpenActionDefaults("Example App", OpenActionDefaults.FileIcon, _tempRoot), defaults);
}

[Fact]
Expand All @@ -36,9 +36,9 @@ public void GetDefaults_FolderUri_UsesFolderNameIconAndWorkingDirectory()
var folderPath = Path.Combine(_tempRoot, "Docs");
Directory.CreateDirectory(folderPath);

var defaults = ShellActionDefaults.FromTarget(new Uri(folderPath).AbsoluteUri);
var defaults = OpenActionDefaults.FromTarget(new Uri(folderPath).AbsoluteUri);

Assert.Equal(new ShellActionDefaults("Docs", ShellActionDefaults.FolderIcon, folderPath), defaults);
Assert.Equal(new OpenActionDefaults("Docs", OpenActionDefaults.FolderIcon, folderPath), defaults);
}

public void Dispose()
Expand Down
10 changes: 5 additions & 5 deletions RadialActions.Tests/Actions/PieActionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ public void CreateKeyAction_UnknownId_UsesFirstKnownAction()
}

[Fact]
public void CreateShellAction_SetsExpectedFields()
public void CreateOpenAction_SetsExpectedFields()
{
var action = PieAction.CreateShellAction("Docs", "https://example.com", "*", "--foo", "C:\\");
var action = PieAction.CreateOpenAction("Docs", "https://example.com", "*", "--foo", "C:\\");

Assert.Equal(ActionType.Shell, action.Type);
Assert.Equal(ActionType.Open, action.Type);
Assert.True(action.IsEnabled);
Assert.Equal("Docs", action.Name);
Assert.Equal("*", action.Icon);
Expand Down Expand Up @@ -59,9 +59,9 @@ public void Execute_NoneAction_ThrowsInvalidOperationException()
}

[Fact]
public void Execute_ShellActionWithoutTarget_ThrowsInvalidOperationException()
public void Execute_OpenActionWithoutTarget_ThrowsInvalidOperationException()
{
var action = PieAction.CreateShellAction("Docs", string.Empty);
var action = PieAction.CreateOpenAction("Docs", string.Empty);

var ex = Assert.Throws<InvalidOperationException>(() => action.Execute());

Expand Down
4 changes: 2 additions & 2 deletions RadialActions.Tests/Settings/SettingsSerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public void SerializeToJson_RoundTripsCoreValues()
settings.Actions = new System.Collections.ObjectModel.ObservableCollection<PieAction>
{
PieAction.CreateKeyAction("Mute"),
PieAction.CreateShellAction("Explorer", "explorer.exe")
PieAction.CreateOpenAction("Explorer", "explorer.exe")
};
settings.Actions[1].IsEnabled = false;

Expand All @@ -83,7 +83,7 @@ public void SerializeToJson_RoundTripsCoreValues()
Assert.Equal(ActionType.Key, loaded.Actions[0].Type);
Assert.Equal("Mute", loaded.Actions[0].Parameter);
Assert.True(loaded.Actions[0].IsEnabled);
Assert.Equal(ActionType.Shell, loaded.Actions[1].Type);
Assert.Equal(ActionType.Open, loaded.Actions[1].Type);
Assert.Equal("explorer.exe", loaded.Actions[1].Parameter);
Assert.False(loaded.Actions[1].IsEnabled);
}
Expand Down
20 changes: 10 additions & 10 deletions RadialActions/Actions/Action.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ public enum ActionType
Key = 1,

/// <summary>
/// Launch an app, open a file, or open a URL using shell execution.
/// Open an app, file, folder, or URL using shell execution.
/// </summary>
Shell = 2,
Open = 2,
}

/// <summary>
Expand Down Expand Up @@ -114,13 +114,13 @@ public static bool TryGetKeyAction(string id, out KeyActionDefinition definition
private string _parameter = string.Empty;

/// <summary>
/// Additional arguments for shell actions.
/// Additional arguments for Open actions.
/// </summary>
[ObservableProperty]
private string _arguments = string.Empty;

/// <summary>
/// Optional working directory for shell actions.
/// Optional working directory for Open actions.
/// </summary>
[ObservableProperty]
private string _workingDirectory = string.Empty;
Expand Down Expand Up @@ -157,12 +157,12 @@ public static PieAction CreateKeyAction(string keyActionId)
}

/// <summary>
/// Creates a shell action.
/// Creates an Open action.
/// </summary>
public static PieAction CreateShellAction(string name, string target, string icon = "📁", string arguments = "", string workingDirectory = "")
public static PieAction CreateOpenAction(string name, string target, string icon = "📁", string arguments = "", string workingDirectory = "")
=> new(name, icon)
{
Type = ActionType.Shell,
Type = ActionType.Open,
Parameter = target,
Arguments = arguments,
WorkingDirectory = workingDirectory
Expand All @@ -182,8 +182,8 @@ public void Execute()
case ActionType.Key:
ExecuteKey();
return;
case ActionType.Shell:
ExecuteShell();
case ActionType.Open:
ExecuteOpen();
return;
default:
throw new NotSupportedException("Action type is not supported");
Expand All @@ -207,7 +207,7 @@ private void ExecuteKey()
ActionUtil.SimulateKeyboardShortcut(Parameter);
}

private void ExecuteShell()
private void ExecuteOpen()
{
if (string.IsNullOrWhiteSpace(Parameter))
throw new InvalidOperationException("Launch target not configured");
Expand Down
24 changes: 12 additions & 12 deletions RadialActions/Actions/ActionDefaultsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ public sealed class ActionDefaultsService
public const string LegacyDefaultIcon = "\u2B50";

private readonly Dictionary<PieAction, KeyActionDefinition> _autoKeyDefaults = [];
private readonly Dictionary<PieAction, ShellActionDefaults> _autoShellDefaults = [];
private readonly Dictionary<PieAction, OpenActionDefaults> _autoOpenDefaults = [];

public ShellActionDefaults? GetShellDefaults(string target) => ShellActionDefaults.FromTarget(target);
public OpenActionDefaults? GetOpenDefaults(string target) => OpenActionDefaults.FromTarget(target);

public void Forget(PieAction action)
{
_autoKeyDefaults.Remove(action);
_autoShellDefaults.Remove(action);
_autoOpenDefaults.Remove(action);
}

public void TrackExistingDefaults(IEnumerable<PieAction> actions)
{
_autoKeyDefaults.Clear();
_autoShellDefaults.Clear();
_autoOpenDefaults.Clear();

foreach (var action in actions)
{
Expand All @@ -29,12 +29,12 @@ public void TrackExistingDefaults(IEnumerable<PieAction> actions)
_autoKeyDefaults[action] = definition;
}
}
else if (action.Type == ActionType.Shell)
else if (action.Type == ActionType.Open)
{
var defaults = GetShellDefaults(action.Parameter);
var defaults = GetOpenDefaults(action.Parameter);
if (defaults.HasValue)
{
_autoShellDefaults[action] = defaults.Value;
_autoOpenDefaults[action] = defaults.Value;
}
}
}
Expand Down Expand Up @@ -69,16 +69,16 @@ public void ApplyKeyDefaults(PieAction action, KeyActionDefinition definition)
_autoKeyDefaults[action] = definition;
}

public void ApplyShellDefaults(PieAction action, string target)
public void ApplyOpenDefaults(PieAction action, string target)
{
if (action.Type != ActionType.Shell)
if (action.Type != ActionType.Open)
return;

var defaults = GetShellDefaults(target);
var defaults = GetOpenDefaults(target);
if (!defaults.HasValue)
return;

_autoShellDefaults.TryGetValue(action, out var previous);
_autoOpenDefaults.TryGetValue(action, out var previous);
var next = defaults.Value;

if (ShouldApplyDefault(action.Name, PieAction.DefaultName, previous.Name ?? string.Empty))
Expand All @@ -98,7 +98,7 @@ public void ApplyShellDefaults(PieAction action, string target)
action.WorkingDirectory = next.WorkingDirectory;
}

_autoShellDefaults[action] = next;
_autoOpenDefaults[action] = next;
}

private static bool ShouldApplyDefault(string currentValue, string defaultValue, string previousValue)
Expand Down
10 changes: 5 additions & 5 deletions RadialActions/Actions/ActionDropFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace RadialActions;

/// <summary>
/// Turns data dropped onto the actions list (files, folders, apps, or links) into prefilled shell actions.
/// Turns data dropped onto the actions list (files, folders, apps, or links) into prefilled Open actions.
/// </summary>
public static class ActionDropFactory
{
Expand Down Expand Up @@ -57,14 +57,14 @@ void Add(string candidate)
}

/// <summary>
/// Builds a shell action for a single dropped target, prefilling its name, icon, and working directory from <see cref="ShellActionDefaults"/>.
/// Builds an Open action for a single dropped target, prefilling its name, icon, and working directory from <see cref="OpenActionDefaults"/>.
/// </summary>
public static PieAction CreateAction(string target)
{
var defaults = ShellActionDefaults.FromTarget(target)
?? new ShellActionDefaults(PieAction.DefaultName, ShellActionDefaults.FileIcon, string.Empty);
var defaults = OpenActionDefaults.FromTarget(target)
?? new OpenActionDefaults(PieAction.DefaultName, OpenActionDefaults.FileIcon, string.Empty);

return PieAction.CreateShellAction(defaults.Name, target, defaults.Icon, workingDirectory: defaults.WorkingDirectory);
return PieAction.CreateOpenAction(defaults.Name, target, defaults.Icon, workingDirectory: defaults.WorkingDirectory);
}

// Reads the most specific link text the drop advertises, preferring the dedicated URL formats over plain text.
Expand Down
Loading