From 6ff450bde14e69851fb82b441e8103acb6aa0748 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Wed, 27 May 2026 23:07:11 -0300 Subject: [PATCH 01/11] Remove unused EditWindow --- Silkstring/Plugin.cs | 4 - Silkstring/Windows/EditWindow.cs | 126 ------------------------------- 2 files changed, 130 deletions(-) delete mode 100644 Silkstring/Windows/EditWindow.cs diff --git a/Silkstring/Plugin.cs b/Silkstring/Plugin.cs index 3c5c315..16b0053 100644 --- a/Silkstring/Plugin.cs +++ b/Silkstring/Plugin.cs @@ -36,7 +36,6 @@ public sealed unsafe class Plugin : IDalamudPlugin public Configuration Configuration { get; init; } public readonly WindowSystem WindowSystem = new("Silkstring"); - private EditWindow EditWindow { get; init; } private ConfigWindow ConfigWindow { get; init; } private MainWindow MainWindow { get; init; } @@ -53,13 +52,11 @@ public Plugin() ProcessChatInputDetour); processChatInputHook.Enable(); - EditWindow = new EditWindow(this); ConfigWindow = new ConfigWindow(this); MainWindow = new MainWindow(this, ToggleConfigUi); WindowSystem.AddWindow(MainWindow); WindowSystem.AddWindow(ConfigWindow); - WindowSystem.AddWindow(EditWindow); CommandManager.AddHandler(CommandName, new CommandInfo(OnCommand) { @@ -86,7 +83,6 @@ public void Dispose() MainWindow.Dispose(); ConfigWindow.Dispose(); - EditWindow.Dispose(); CommandManager.RemoveHandler(CommandName); } diff --git a/Silkstring/Windows/EditWindow.cs b/Silkstring/Windows/EditWindow.cs deleted file mode 100644 index cc2dd5f..0000000 --- a/Silkstring/Windows/EditWindow.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Windowing; -using Silkstring.Models; - -namespace Silkstring.Windows; - -public class EditWindow : Window, IDisposable -{ - private readonly Configuration configuration; - private AliasEntry? aliasCache; - private AliasEntry? originalAlias; - private string multilineBuffer = string.Empty; - - public EditWindow(Plugin plugin) : base("Edit Alias###EditWindow") - { - configuration = plugin.Configuration; - } - public void Dispose() { } - - public override void PreDraw() - { - SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new Vector2(400, 200), - MaximumSize = new Vector2(float.MaxValue, float.MaxValue) - }; - } - - public override void Draw() - { - if (aliasCache == null || originalAlias == null) return; - ImGui.InputText("Alias", ref aliasCache.Name); - - if (configuration.MultilineCommands) - { - DrawMultilineView(); - } - else - { - ImGui.Columns(2); - - ImGui.Text("Command"); - ImGui.NextColumn(); - ImGui.NextColumn(); - - foreach (var command in aliasCache.Output) - { - if (command.UniqueId == 0) - command.UniqueId = aliasCache.Output.Count == 0 ? 1 : aliasCache.Output.Max(c => c.UniqueId) + 1; - DrawListView(command); - } - aliasCache.Output.RemoveAll(c => c.Delete); - } - - ImGui.Columns(1); - - if (!configuration.MultilineCommands) - { - ImGui.Separator(); - if (ImGui.Button("Add Command")) - { - aliasCache.Output.Add(new CommandEntry()); - } - } - - var availableHeight = ImGui.GetContentRegionAvail().Y; - var buttonHeight = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y + ImGui.GetStyle().WindowPadding.Y * 2 + 4; - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + availableHeight - buttonHeight); - ImGui.Separator(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 25); - if (ImGui.Button("Save")) - { - if (configuration.MultilineCommands) - { - aliasCache.Output = multilineBuffer - .Split('\n', StringSplitOptions.RemoveEmptyEntries) - .Select(c => new CommandEntry { Command = c.Trim() }) - .ToList(); - } - - originalAlias.Name = aliasCache.Name; - originalAlias.Enabled = aliasCache.Enabled; - originalAlias.Output = aliasCache.Output; - configuration.Save(); - IsOpen = false; - } - - var closeButtonWidth = ImGui.CalcTextSize("Close").X + ImGui.GetStyle().FramePadding.X * 2; - ImGui.SameLine(ImGui.GetWindowWidth() - closeButtonWidth - ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().WindowPadding.X - 16); - if (ImGui.Button("Cancel")) - { - IsOpen = false; - } - } - - public void Open(AliasEntry alias) - { - aliasCache = alias.Clone(); - originalAlias = alias; - multilineBuffer = string.Join("\n", aliasCache.Output.Select(c => c.Command)); - IsOpen = true; - } - - private void DrawListView(CommandEntry command) - { - ImGui.SetNextItemWidth(-1); - ImGui.InputText("###command" + command.UniqueId, ref command.Command, 100); - ImGui.NextColumn(); - - var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; - ImGui.BeginDisabled(!canDelete); - if (ImGui.Button("Delete###delete" + command.UniqueId)) command.Delete = true; - ImGui.EndDisabled(); - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip("Hold Shift + Ctrl to delete"); - ImGui.NextColumn(); - } - - private void DrawMultilineView() - { - var footerHeight = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y + ImGui.GetStyle().WindowPadding.Y * 2 + 4; - ImGui.InputTextMultiline("###multilineCommands", ref multilineBuffer, 5000, new Vector2(-1, ImGui.GetContentRegionAvail().Y - footerHeight)); - } -} From 613b9af5fd1b325f337d2dd53b33594dfa989c60 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Wed, 27 May 2026 23:32:54 -0300 Subject: [PATCH 02/11] Adjust filtering to handle display names as well --- Silkstring/Models/AliasEntry.cs | 5 ++--- Silkstring/UI/AliasEditPanel.cs | 14 ++++++-------- Silkstring/UI/AliasSelectPanel.cs | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Silkstring/Models/AliasEntry.cs b/Silkstring/Models/AliasEntry.cs index a4abae8..487650f 100644 --- a/Silkstring/Models/AliasEntry.cs +++ b/Silkstring/Models/AliasEntry.cs @@ -21,13 +21,12 @@ public class AliasEntry public bool IsValid() { - var names = Name.Split('|', StringSplitOptions.TrimEntries); + var names = Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (names.Length == 0) return false; foreach (var name in names) { - if (string.IsNullOrWhiteSpace(name)) return false; - if (Blacklist.Contains(name)) return false; + if (Blacklist.Contains(name, StringComparer.OrdinalIgnoreCase)) return false; if (name.Contains(' ')) return false; if (name.Contains('/')) return false; } diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index 4965dd1..6d81bbd 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -96,17 +96,15 @@ private void DrawMultlineView(AliasEntry alias) private void DrawListView(AliasEntry alias) { ImGui.Text("Commands:"); - if (ImGui.BeginChild("###commandList")) + ImGui.BeginChild("###commandList"); + foreach (var command in alias.Output) { - foreach (var command in alias.Output) - { - if (command.UniqueId == 0) command.UniqueId = alias.Output.Count == 0 ? 1 : alias.Output.Max(c => c.UniqueId) + 1; - DrawCommandRow(command); - } - - alias.Output.RemoveAll(c => c.Delete); + if (command.UniqueId == 0) command.UniqueId = alias.Output.Count == 0 ? 1 : alias.Output.Max(c => c.UniqueId) + 1; + DrawCommandRow(command); } + alias.Output.RemoveAll(c => c.Delete); + if (ImGuiComponents.IconButton((int)FontAwesomeIcon.Plus, FontAwesomeIcon.Plus)) { alias.Output.Add(new CommandEntry()); diff --git a/Silkstring/UI/AliasSelectPanel.cs b/Silkstring/UI/AliasSelectPanel.cs index b9e594a..dc0173d 100644 --- a/Silkstring/UI/AliasSelectPanel.cs +++ b/Silkstring/UI/AliasSelectPanel.cs @@ -17,7 +17,7 @@ public class AliasSelectPanel private readonly Action _openSettings; private string _filter = string.Empty; - private bool MatchesFilter(AliasEntry alias) => string.IsNullOrWhiteSpace(_filter) || alias.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase); + private bool MatchesFilter(AliasEntry alias) => string.IsNullOrWhiteSpace(_filter) || alias.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase) || alias.DisplayName.Contains(_filter, StringComparison.OrdinalIgnoreCase); private AliasEntry? _draggedAlias; private AliasFolder? _draggedFromFolder; From 8bf42fda6ca63322bd4b4df104ed62627d1409ae Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Wed, 27 May 2026 23:51:32 -0300 Subject: [PATCH 03/11] More verbose error handling and allow cancellation of command execution --- Silkstring/Plugin.cs | 13 +++++++++++-- Silkstring/Services/CommandHandler.cs | 12 +++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Silkstring/Plugin.cs b/Silkstring/Plugin.cs index 16b0053..2ea3e9d 100644 --- a/Silkstring/Plugin.cs +++ b/Silkstring/Plugin.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using Dalamud.Game.Command; using Dalamud.Hooking; using Dalamud.IoC; @@ -41,6 +42,8 @@ public sealed unsafe class Plugin : IDalamudPlugin private Hook processChatInputHook; + private readonly CancellationTokenSource _cts = new(); + public Plugin() { Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); @@ -72,6 +75,9 @@ public void Dispose() { ECommonsMain.Dispose(); + _cts.Cancel(); + _cts.Dispose(); + PluginInterface.UiBuilder.Draw -= WindowSystem.Draw; PluginInterface.UiBuilder.OpenConfigUi -= ToggleConfigUi; PluginInterface.UiBuilder.OpenMainUi -= ToggleMainUi; @@ -117,7 +123,10 @@ private void ProcessChatInputDetour(ShellCommandModule* shellCommandModule, Utf8 .Where(c => !string.IsNullOrWhiteSpace(c.Command)) .Select(c => "/" + c.Command.TrimStart('/')) .ToList(); - _ = CommandHandler.ExecuteAsync(commands, Configuration.CommandDelay); + + + CommandHandler.ExecuteAsync(commands, Configuration.CommandDelay) + .ContinueWith(t => Log.Error(t.Exception, "Command execution failed"), TaskContinuationOptions.OnlyOnFaulted); return; } } @@ -125,7 +134,7 @@ private void ProcessChatInputDetour(ShellCommandModule* shellCommandModule, Utf8 } catch (Exception ex) { - Log.Error(ex.Message); + Log.Error(ex, "An error occured while processing command"); } processChatInputHook.Original(shellCommandModule, message, uiModule); } diff --git a/Silkstring/Services/CommandHandler.cs b/Silkstring/Services/CommandHandler.cs index 21b4e49..e1d7fbd 100644 --- a/Silkstring/Services/CommandHandler.cs +++ b/Silkstring/Services/CommandHandler.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using ECommons.Automation; @@ -6,15 +7,12 @@ namespace Silkstring.Services; public static class CommandHandler { - public static async Task ExecuteAsync(List commands, int delayMs = 100) + public static async Task ExecuteAsync(IReadOnlyList commands, int delayMs = 100, CancellationToken cancellationToken = default) { - foreach (var command in commands) + for (var i = 0; i < commands.Count; i++) { - await Plugin.Framework.RunOnFrameworkThread(() => - { - Chat.SendMessage(command); - }); - await Task.Delay(delayMs); + await Plugin.Framework.RunOnFrameworkThread(() => Chat.SendMessage(commands[i])); + if (i < commands.Count - 1) await Task.Delay(delayMs, cancellationToken); } } } From 41cb3860bd7508688dd4c08d81f093f679fa45c8 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Thu, 28 May 2026 00:15:56 -0300 Subject: [PATCH 04/11] Adjusted UniqueId logic for efficiency --- Silkstring/Models/AliasEntry.cs | 10 ++++++++++ Silkstring/Models/CommandEntry.cs | 10 ++++++++++ Silkstring/UI/AliasEditPanel.cs | 1 - Silkstring/UI/AliasSelectPanel.cs | 10 ---------- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Silkstring/Models/AliasEntry.cs b/Silkstring/Models/AliasEntry.cs index 487650f..834daf7 100644 --- a/Silkstring/Models/AliasEntry.cs +++ b/Silkstring/Models/AliasEntry.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; +using System.Threading; namespace Silkstring.Models; public class AliasEntry { + private static int _nextId = 0; + public static readonly string[] Blacklist = ["silkstring", "xlplugins", "xlsettings", "xldclose", "xldev"]; public string DisplayName = string.Empty; @@ -17,8 +21,14 @@ public class AliasEntry public bool Delete; [NonSerialized] + [JsonIgnore] public int UniqueId; + public AliasEntry() + { + UniqueId = Interlocked.Increment(ref _nextId); + } + public bool IsValid() { var names = Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); diff --git a/Silkstring/Models/CommandEntry.cs b/Silkstring/Models/CommandEntry.cs index e29b3be..8025bea 100644 --- a/Silkstring/Models/CommandEntry.cs +++ b/Silkstring/Models/CommandEntry.cs @@ -1,17 +1,27 @@ using System; +using System.Text.Json.Serialization; +using System.Threading; namespace Silkstring.Models; public class CommandEntry { + private static int _nextId = 0; + public string Command = string.Empty; [NonSerialized] public bool Delete; [NonSerialized] + [JsonIgnore] public int UniqueId; + public CommandEntry() + { + UniqueId = Interlocked.Increment(ref _nextId); + } + public CommandEntry Clone() { return new CommandEntry() diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index 6d81bbd..908e9e7 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -99,7 +99,6 @@ private void DrawListView(AliasEntry alias) ImGui.BeginChild("###commandList"); foreach (var command in alias.Output) { - if (command.UniqueId == 0) command.UniqueId = alias.Output.Count == 0 ? 1 : alias.Output.Max(c => c.UniqueId) + 1; DrawCommandRow(command); } diff --git a/Silkstring/UI/AliasSelectPanel.cs b/Silkstring/UI/AliasSelectPanel.cs index dc0173d..4266c4d 100644 --- a/Silkstring/UI/AliasSelectPanel.cs +++ b/Silkstring/UI/AliasSelectPanel.cs @@ -14,7 +14,6 @@ public class AliasSelectPanel { private readonly Configuration _configuration; private readonly MainWindow _mainWindow; - private readonly Action _openSettings; private string _filter = string.Empty; private bool MatchesFilter(AliasEntry alias) => string.IsNullOrWhiteSpace(_filter) || alias.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase) || alias.DisplayName.Contains(_filter, StringComparison.OrdinalIgnoreCase); @@ -39,7 +38,6 @@ public AliasSelectPanel(Configuration configuration, MainWindow mainWindow, Acti { _configuration = configuration; _mainWindow = mainWindow; - _openSettings = openSettings; } public void Draw() @@ -181,12 +179,6 @@ private void DrawUnsorted() private void DrawAliasRow(AliasEntry alias, AliasFolder? owningFolder) { - if (alias.UniqueId == 0) - { - var allAliases = _configuration.Aliases.Concat(_configuration.Folders.SelectMany(f => f.Aliases)); - alias.UniqueId = allAliases.Any() ? allAliases.Max(a => a.UniqueId) + 1 : 1; - } - if (_renamingAlias == alias) { if (_focusRenameAlias) @@ -296,7 +288,6 @@ private void DrawFooter() var imported = JsonSerializer.Deserialize(json, new JsonSerializerOptions { IncludeFields = true }); if (imported != null) { - imported.UniqueId = 0; _configuration.Aliases.Add(imported); _configuration.Save(); _mainWindow.SelectedAlias = imported; @@ -313,7 +304,6 @@ private void DrawFooter() { var source = _mainWindow.SelectedAlias!; var cloned = source.Clone(); - cloned.UniqueId = 0; if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Add(cloned); else _configuration.Aliases.Add(cloned); From 6ff815cd3026e9a0be9b9a178c0948e20d5e64e3 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Thu, 28 May 2026 00:39:19 -0300 Subject: [PATCH 05/11] Adjust selection to use events --- Silkstring/UI/AliasEditPanel.cs | 8 ++++---- Silkstring/UI/AliasSelectPanel.cs | 13 +++++-------- Silkstring/Windows/MainWindow.cs | 19 +++++++++++++++---- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index 908e9e7..d4e2429 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -4,7 +4,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Components; -using ECommons.ImGuiMethods; using Silkstring.Models; using Silkstring.Windows; @@ -13,20 +12,21 @@ namespace Silkstring.Ui; public class AliasEditPanel { private readonly Configuration _configuration; - private readonly MainWindow _mainWindow; + + private AliasEntry? _selectedAlias; private string _multilineBuffer = string.Empty; private int _multilineAliasId = -1; public AliasEditPanel(Configuration configuration, MainWindow mainWindow) { + mainWindow.SelectionChanged += (alias, _) => _selectedAlias = alias; _configuration = configuration; - _mainWindow = mainWindow; } public void Draw() { - var alias = _mainWindow.SelectedAlias; + var alias = _selectedAlias; if (alias == null) { diff --git a/Silkstring/UI/AliasSelectPanel.cs b/Silkstring/UI/AliasSelectPanel.cs index 4266c4d..8132463 100644 --- a/Silkstring/UI/AliasSelectPanel.cs +++ b/Silkstring/UI/AliasSelectPanel.cs @@ -205,8 +205,7 @@ private void DrawAliasRow(AliasEntry alias, AliasFolder? owningFolder) var isSelected = _mainWindow.SelectedAlias == alias; if (ImGui.Selectable(label, isSelected)) { - _mainWindow.SelectedAlias = alias; - _mainWindow.SelectedFolder = owningFolder; + _mainWindow.SetSelection(alias, owningFolder); } if (ImGui.BeginPopupContextItem($"###aliasContext{alias.UniqueId}")) @@ -272,8 +271,7 @@ private void DrawFooter() var newAlias = new AliasEntry(); _configuration.Aliases.Add(newAlias); _configuration.Save(); - _mainWindow.SelectedAlias = newAlias; - _mainWindow.SelectedFolder = null; + _mainWindow.SetSelection(newAlias, null); _renamingAlias = newAlias; _renameAliasBuffer = string.Empty; _focusRenameAlias = true; @@ -290,7 +288,7 @@ private void DrawFooter() { _configuration.Aliases.Add(imported); _configuration.Save(); - _mainWindow.SelectedAlias = imported; + _mainWindow.SetSelection(imported, null); _renamingAlias = imported; _renameAliasBuffer = imported.DisplayName; _focusRenameAlias = true; @@ -309,7 +307,7 @@ private void DrawFooter() else _configuration.Aliases.Add(cloned); _configuration.Save(); - _mainWindow.SelectedAlias = cloned; + _mainWindow.SetSelection(cloned, _mainWindow.SelectedFolder); _renamingAlias = cloned; _renameAliasBuffer = cloned.DisplayName; _focusRenameAlias = true; @@ -335,8 +333,7 @@ private void DrawFooter() if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Remove(_mainWindow.SelectedAlias!); else _configuration.Aliases.Remove(_mainWindow.SelectedAlias!); - _mainWindow.SelectedAlias = null; - _mainWindow.SelectedFolder = null; + _mainWindow.SetSelection(null, null); _configuration.Save(); } diff --git a/Silkstring/Windows/MainWindow.cs b/Silkstring/Windows/MainWindow.cs index bbbea95..4db1cd6 100644 --- a/Silkstring/Windows/MainWindow.cs +++ b/Silkstring/Windows/MainWindow.cs @@ -10,16 +10,20 @@ namespace Silkstring.Windows; public class MainWindow : Window, IDisposable { + public event Action? SelectionChanged; + private readonly AliasSelectPanel _selectPanel; private readonly AliasEditPanel _editPanel; - private readonly Action _openSettings; - internal AliasEntry? SelectedAlias { get; set; } - internal AliasFolder? SelectedFolder { get; set; } + internal AliasEntry? _selectedAlias; + internal AliasFolder? _selectedFolder; + + internal AliasEntry? SelectedAlias => _selectedAlias; + internal AliasFolder? SelectedFolder => _selectedFolder; + public MainWindow(Plugin plugin, Action openSettings) : base("Silkstring###Main") { - _openSettings = openSettings; _selectPanel = new AliasSelectPanel(plugin.Configuration, this, openSettings); _editPanel = new AliasEditPanel(plugin.Configuration, this); @@ -55,4 +59,11 @@ public override void Draw() if (ImGui.BeginChild("###editor", new Vector2(0, 0), true)) _editPanel.Draw(); ImGui.EndChild(); } + + public void SetSelection(AliasEntry? alias, AliasFolder? folder) + { + _selectedAlias = alias; + _selectedFolder = folder; + SelectionChanged?.Invoke(alias, folder); + } } From 4256eca0fdf71d1917f59d9232d9104214a0a0aa Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Thu, 28 May 2026 00:54:44 -0300 Subject: [PATCH 06/11] Debounce saving on input fields --- Silkstring/Configuration.cs | 18 ++++++++++++++++++ Silkstring/Plugin.cs | 7 +++++++ Silkstring/UI/AliasEditPanel.cs | 6 +++--- Silkstring/Windows/ConfigWindow.cs | 4 ++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Silkstring/Configuration.cs b/Silkstring/Configuration.cs index 01bdebb..34b4b7f 100644 --- a/Silkstring/Configuration.cs +++ b/Silkstring/Configuration.cs @@ -8,6 +8,9 @@ namespace Silkstring; [Serializable] public class Configuration : IPluginConfiguration { + private bool _isDirty = false; + private DateTime _lastDirty = DateTime.MinValue; + public int Version { get; set; } = 1; public List Folders = new(); public List Aliases = new(); @@ -18,4 +21,19 @@ public void Save() { Plugin.PluginInterface.SavePluginConfig(this); } + + public void MarkDirty() + { + _isDirty = true; + _lastDirty = DateTime.UtcNow; + } + + public void TrySave(TimeSpan debounce) + { + if (_isDirty && DateTime.UtcNow - _lastDirty > debounce) + { + Save(); + _isDirty = false; + } + } } diff --git a/Silkstring/Plugin.cs b/Silkstring/Plugin.cs index 2ea3e9d..825035e 100644 --- a/Silkstring/Plugin.cs +++ b/Silkstring/Plugin.cs @@ -69,6 +69,7 @@ public Plugin() PluginInterface.UiBuilder.Draw += WindowSystem.Draw; PluginInterface.UiBuilder.OpenConfigUi += ToggleConfigUi; PluginInterface.UiBuilder.OpenMainUi += ToggleMainUi; + Framework.Update += OnFrameworkUpdate; } public void Dispose() @@ -81,6 +82,7 @@ public void Dispose() PluginInterface.UiBuilder.Draw -= WindowSystem.Draw; PluginInterface.UiBuilder.OpenConfigUi -= ToggleConfigUi; PluginInterface.UiBuilder.OpenMainUi -= ToggleMainUi; + Framework.Update -= OnFrameworkUpdate; processChatInputHook?.Disable(); processChatInputHook?.Dispose(); @@ -98,6 +100,11 @@ private void OnCommand(string command, string args) MainWindow.Toggle(); } + private void OnFrameworkUpdate(IFramework framework) + { + Configuration.TrySave(TimeSpan.FromMilliseconds(500)); + } + public void ToggleConfigUi() => ConfigWindow.Toggle(); public void ToggleMainUi() => MainWindow.Toggle(); diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index d4e2429..e2ec623 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -54,7 +54,7 @@ private void DrawAliasHeader(AliasEntry alias) if (ImGui.Checkbox($"###enabled{alias.UniqueId}", ref alias.Enabled)) _configuration.Save(); ImGui.SameLine(); ImGui.SetNextItemWidth(-1); - if(ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) _configuration.Save(); + if(ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) _configuration.MarkDirty(); if(ImGui.IsItemHovered()) ImGui.SetTooltip("Seperate multiple aliases with | e.g. mew|meow|mreow"); } @@ -89,7 +89,7 @@ private void DrawMultlineView(AliasEntry alias) if (alias.Output.Count > lines.Count) alias.Output.RemoveRange(lines.Count, alias.Output.Count - lines.Count); - _configuration.Save(); + _configuration.MarkDirty(); } } @@ -116,7 +116,7 @@ private void DrawListView(AliasEntry alias) private void DrawCommandRow(CommandEntry command) { ImGui.SetNextItemWidth(-60); - if (ImGui.InputText($"###cmd{command.UniqueId}", ref command.Command, 200)) _configuration.Save(); + if (ImGui.InputText($"###cmd{command.UniqueId}", ref command.Command, 200)) _configuration.MarkDirty(); ImGui.SameLine(); var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; diff --git a/Silkstring/Windows/ConfigWindow.cs b/Silkstring/Windows/ConfigWindow.cs index 84ace2a..1c265e9 100644 --- a/Silkstring/Windows/ConfigWindow.cs +++ b/Silkstring/Windows/ConfigWindow.cs @@ -34,14 +34,14 @@ public override void Draw() { delay = Math.Clamp(delay, 0, 1000); configuration.CommandDelay = delay; - configuration.Save(); + configuration.MarkDirty(); } ImGui.SameLine(); ImGui.SetNextItemWidth(200); if (ImGui.SliderInt("###delaySlider", ref delay, 0, 1000)) { configuration.CommandDelay = delay; - configuration.Save(); + configuration.MarkDirty(); } var multiline = configuration.MultilineCommands; From 3ec9ec605c6c9e89afd9e8208862125f14178f8c Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Thu, 28 May 2026 01:10:33 -0300 Subject: [PATCH 07/11] Added feedback to failed imports --- Silkstring/Plugin.cs | 5 ++++- Silkstring/UI/AliasEditPanel.cs | 4 ++-- Silkstring/UI/AliasSelectPanel.cs | 17 +++++++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Silkstring/Plugin.cs b/Silkstring/Plugin.cs index 825035e..5e0c388 100644 --- a/Silkstring/Plugin.cs +++ b/Silkstring/Plugin.cs @@ -32,6 +32,9 @@ public sealed unsafe class Plugin : IDalamudPlugin [PluginService] internal static IFramework Framework { get; private set; } = null!; + [PluginService] + internal static INotificationManager NotificationManager { get; private set; } = null!; + private const string CommandName = "/silkstring"; public Configuration Configuration { get; init; } @@ -128,7 +131,7 @@ private void ProcessChatInputDetour(ShellCommandModule* shellCommandModule, Utf8 { var commands = alias.Output .Where(c => !string.IsNullOrWhiteSpace(c.Command)) - .Select(c => "/" + c.Command.TrimStart('/')) + .Select(c => "/" + c.Strip()) .ToList(); diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index e2ec623..6750ad7 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -62,7 +62,7 @@ private void DrawCommandList(AliasEntry alias) { if (_configuration.MultilineCommands) { - DrawMultlineView(alias); + DrawMultilineView(alias); } else { @@ -70,7 +70,7 @@ private void DrawCommandList(AliasEntry alias) } } - private void DrawMultlineView(AliasEntry alias) + private void DrawMultilineView(AliasEntry alias) { if (_multilineAliasId != alias.UniqueId) { diff --git a/Silkstring/UI/AliasSelectPanel.cs b/Silkstring/UI/AliasSelectPanel.cs index 8132463..75815b3 100644 --- a/Silkstring/UI/AliasSelectPanel.cs +++ b/Silkstring/UI/AliasSelectPanel.cs @@ -5,6 +5,8 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification; +using Serilog; using Silkstring.Models; using Silkstring.Windows; @@ -283,7 +285,8 @@ private void DrawFooter() try { var json = ImGui.GetClipboardText(); - var imported = JsonSerializer.Deserialize(json, new JsonSerializerOptions { IncludeFields = true }); + var imported = + JsonSerializer.Deserialize(json, new JsonSerializerOptions { IncludeFields = true }); if (imported != null) { _configuration.Aliases.Add(imported); @@ -294,7 +297,17 @@ private void DrawFooter() _focusRenameAlias = true; } } - catch {} + catch (Exception ex) + { + Log.Error(ex, "Failed to import alias from clipboard"); + Plugin.NotificationManager.AddNotification(new Notification + { + Title = "Import Failure", + Content = "Could not import alias: Clipboard contents are not valid.", + Type = NotificationType.Error + }); + + } } ImGui.SameLine(); From 1cd87233b891a8bf64924b19225e0c6ac9d94f43 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Thu, 28 May 2026 18:54:35 -0300 Subject: [PATCH 08/11] Misc code cleaning and optimization --- Silkstring/Configuration.cs | 12 +++++++-- Silkstring/Models/AliasEntry.cs | 9 ++++--- Silkstring/Models/AliasFolder.cs | 13 ++++++++++ Silkstring/Plugin.cs | 37 ++++++++++++--------------- Silkstring/Services/CommandHandler.cs | 3 ++- Silkstring/UI/AliasEditPanel.cs | 6 ++--- Silkstring/UI/AliasSelectPanel.cs | 32 +++++++++++++---------- Silkstring/Windows/ConfigWindow.cs | 6 ----- Silkstring/Windows/MainWindow.cs | 15 ++++++----- 9 files changed, 78 insertions(+), 55 deletions(-) diff --git a/Silkstring/Configuration.cs b/Silkstring/Configuration.cs index 34b4b7f..c6e3a61 100644 --- a/Silkstring/Configuration.cs +++ b/Silkstring/Configuration.cs @@ -8,14 +8,22 @@ namespace Silkstring; [Serializable] public class Configuration : IPluginConfiguration { + [NonSerialized] private bool _isDirty = false; + [NonSerialized] private DateTime _lastDirty = DateTime.MinValue; + private int _commandDelay = 100; + public int CommandDelay + { + get => _commandDelay; + set => _commandDelay = Math.Clamp(value, 0, 1000); + } + public int Version { get; set; } = 1; public List Folders = new(); public List Aliases = new(); - public int CommandDelay { get; set; } = 100; - public bool MultilineCommands { get; set; } = false; + public bool MultilineCommands { get; set; } public void Save() { diff --git a/Silkstring/Models/AliasEntry.cs b/Silkstring/Models/AliasEntry.cs index 834daf7..68a40d1 100644 --- a/Silkstring/Models/AliasEntry.cs +++ b/Silkstring/Models/AliasEntry.cs @@ -10,7 +10,10 @@ public class AliasEntry { private static int _nextId = 0; - public static readonly string[] Blacklist = ["silkstring", "xlplugins", "xlsettings", "xldclose", "xldev"]; + public static readonly HashSet Blacklist = new(StringComparer.OrdinalIgnoreCase) + { + "silkstring", "xlplugins", "xlsettings", "xldclose", "xldev" + }; public string DisplayName = string.Empty; public bool Enabled = true; @@ -36,11 +39,11 @@ public bool IsValid() foreach (var name in names) { - if (Blacklist.Contains(name, StringComparer.OrdinalIgnoreCase)) return false; + if (Blacklist.Contains(name)) return false; if (name.Contains(' ')) return false; if (name.Contains('/')) return false; } - if (Output.Count == 0) return false; + if (Output.Count == 0) return false; return !Output.Any(command => string.IsNullOrWhiteSpace(command.Command)); } diff --git a/Silkstring/Models/AliasFolder.cs b/Silkstring/Models/AliasFolder.cs index d7dd9be..6f3a12a 100644 --- a/Silkstring/Models/AliasFolder.cs +++ b/Silkstring/Models/AliasFolder.cs @@ -1,9 +1,22 @@ +using System; using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading; namespace Silkstring.Models; public class AliasFolder { + private static int _nextId = 0; public string Name { get; set; } = string.Empty; public List Aliases { get; set; } = new(); + + [NonSerialized] + [JsonIgnore] + public int UniqueId; + + public AliasFolder() + { + UniqueId = Interlocked.Increment(ref _nextId); + } } diff --git a/Silkstring/Plugin.cs b/Silkstring/Plugin.cs index 5e0c388..6d038fc 100644 --- a/Silkstring/Plugin.cs +++ b/Silkstring/Plugin.cs @@ -119,32 +119,29 @@ private void ProcessChatInputDetour(ShellCommandModule* shellCommandModule, Utf8 if (inputString.StartsWith('/')) { var splitString = inputString.Split(' '); - if (splitString.Length > 0) + var commandName = splitString[0][1..]; + var alias = Configuration.Aliases.Concat( + Configuration.Folders.SelectMany(g => g.Aliases)).FirstOrDefault(a => + a.Enabled && + a.IsValid() && + a.Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); + if (alias != null) { - var commandName = splitString[0][1..]; - var alias = Configuration.Aliases.Concat( - Configuration.Folders.SelectMany(g => g.Aliases)).FirstOrDefault(a => - a.Enabled && - a.IsValid() && - a.Name.Split('|', StringSplitOptions.TrimEntries).Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); - if (alias != null) - { - var commands = alias.Output - .Where(c => !string.IsNullOrWhiteSpace(c.Command)) - .Select(c => "/" + c.Strip()) - .ToList(); - - - CommandHandler.ExecuteAsync(commands, Configuration.CommandDelay) - .ContinueWith(t => Log.Error(t.Exception, "Command execution failed"), TaskContinuationOptions.OnlyOnFaulted); - return; - } + var commands = alias.Output + .Where(c => !string.IsNullOrWhiteSpace(c.Command)) + .Select(c => "/" + c.Strip()) + .ToList(); + + + CommandHandler.ExecuteAsync(commands, Configuration.CommandDelay, _cts.Token) + .ContinueWith(t => Log.Error(t.Exception, "Command execution failed"), TaskContinuationOptions.OnlyOnFaulted); + return; } } } catch (Exception ex) { - Log.Error(ex, "An error occured while processing command"); + Log.Error(ex, "An error occurred while processing command"); } processChatInputHook.Original(shellCommandModule, message, uiModule); } diff --git a/Silkstring/Services/CommandHandler.cs b/Silkstring/Services/CommandHandler.cs index e1d7fbd..d9d9618 100644 --- a/Silkstring/Services/CommandHandler.cs +++ b/Silkstring/Services/CommandHandler.cs @@ -11,7 +11,8 @@ public static async Task ExecuteAsync(IReadOnlyList commands, int delayM { for (var i = 0; i < commands.Count; i++) { - await Plugin.Framework.RunOnFrameworkThread(() => Chat.SendMessage(commands[i])); + var cmd = commands[i]; + await Plugin.Framework.RunOnFrameworkThread(() => Chat.SendMessage(cmd)); if (i < commands.Count - 1) await Task.Delay(delayMs, cancellationToken); } } diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index 6750ad7..b64d92a 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -18,7 +18,7 @@ public class AliasEditPanel private string _multilineBuffer = string.Empty; private int _multilineAliasId = -1; - public AliasEditPanel(Configuration configuration, MainWindow mainWindow) + public AliasEditPanel(Configuration configuration, MainWindow mainWindow) { mainWindow.SelectionChanged += (alias, _) => _selectedAlias = alias; _configuration = configuration; @@ -55,7 +55,7 @@ private void DrawAliasHeader(AliasEntry alias) ImGui.SameLine(); ImGui.SetNextItemWidth(-1); if(ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) _configuration.MarkDirty(); - if(ImGui.IsItemHovered()) ImGui.SetTooltip("Seperate multiple aliases with | e.g. mew|meow|mreow"); + if(ImGui.IsItemHovered()) ImGui.SetTooltip("Separate multiple aliases with | e.g. mew|meow|mreow"); } private void DrawCommandList(AliasEntry alias) @@ -84,7 +84,7 @@ private void DrawMultilineView(AliasEntry alias) for (var i = 0; i < lines.Count; i++) { if (i < alias.Output.Count) alias.Output[i].Command = lines[i]; - else alias.Output.Add(new CommandEntry { Command = lines[i] }); + else alias.Output.Add(new CommandEntry { Command = lines[i] }); } if (alias.Output.Count > lines.Count) alias.Output.RemoveRange(lines.Count, alias.Output.Count - lines.Count); diff --git a/Silkstring/UI/AliasSelectPanel.cs b/Silkstring/UI/AliasSelectPanel.cs index 75815b3..3f93114 100644 --- a/Silkstring/UI/AliasSelectPanel.cs +++ b/Silkstring/UI/AliasSelectPanel.cs @@ -18,7 +18,6 @@ public class AliasSelectPanel private readonly MainWindow _mainWindow; private string _filter = string.Empty; - private bool MatchesFilter(AliasEntry alias) => string.IsNullOrWhiteSpace(_filter) || alias.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase) || alias.DisplayName.Contains(_filter, StringComparison.OrdinalIgnoreCase); private AliasEntry? _draggedAlias; private AliasFolder? _draggedFromFolder; @@ -33,10 +32,11 @@ public class AliasSelectPanel private bool _focusRenameAlias = false; private const string DragDropType = "ALIAS"; + private const int FooterButtonCount = 5; private static readonly Vector4 FolderColor = new(0.7f, 0.5f, 1.0f, 1.0f); - public AliasSelectPanel(Configuration configuration, MainWindow mainWindow, Action openSettings) + public AliasSelectPanel(Configuration configuration, MainWindow mainWindow) { _configuration = configuration; _mainWindow = mainWindow; @@ -50,17 +50,15 @@ public void Draw() var frameHeight = ImGui.GetFrameHeight(); var listHeight = ImGui.GetContentRegionAvail().Y - frameHeight - ImGui.GetStyle().ItemSpacing.Y; - if (ImGui.BeginChild("###aliasList", new Vector2(0, listHeight))) - { - DrawFolders(); - DrawUnsorted(); - } + ImGui.BeginChild("###aliasList", new Vector2(0, listHeight)); + DrawFolders(); + DrawUnsorted(); ImGui.EndChild(); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - - if (ImGui.BeginChild("###footer", new Vector2(-1, frameHeight))) DrawFooter(); + ImGui.BeginChild("###footer", new Vector2(-1, frameHeight)); + DrawFooter(); ImGui.EndChild(); ImGui.PopStyleVar(2); } @@ -87,7 +85,7 @@ private void DrawFolders() } ImGui.SetNextItemWidth(-1); - ImGui.InputText($"###rename{folder.GetHashCode()}", ref _renameBuffer, 100); + ImGui.InputText($"###rename{folder.UniqueId}", ref _renameBuffer, 100); if (ImGui.IsItemDeactivated()) { @@ -107,12 +105,12 @@ private void DrawFolders() else { ImGui.PushStyleColor(ImGuiCol.Text, FolderColor); - open = ImGui.TreeNodeEx($"{folder.Name}###{folder.GetHashCode()}folder", + open = ImGui.TreeNodeEx($"{folder.Name}###{folder.UniqueId}folder", ImGuiTreeNodeFlags.SpanAvailWidth); ImGui.PopStyleColor(); needsTreePop = open; - if (ImGui.BeginPopupContextItem($"###folderContext{folder.GetHashCode()}")) + if (ImGui.BeginPopupContextItem($"###folderContext{folder.UniqueId}")) { if (ImGui.MenuItem("Rename")) { @@ -262,8 +260,7 @@ private void MoveAlias(AliasEntry alias, AliasFolder? from, AliasFolder? toFolde private void DrawFooter() { var available = ImGui.GetContentRegionAvail(); - var buttonCount = 5; - var buttonSize = new Vector2(MathF.Floor(available.X / buttonCount), available.Y); + var buttonSize = new Vector2(MathF.Floor(available.X / FooterButtonCount), available.Y); var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); @@ -369,4 +366,11 @@ private bool DrawIconButton(FontAwesomeIcon icon, Vector2 size, string tooltip, return clicked; } + private bool MatchesFilter(AliasEntry alias) + { + if (string.IsNullOrWhiteSpace(_filter)) return true; + return alias.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase) || + alias.DisplayName.Contains(_filter, StringComparison.OrdinalIgnoreCase); + } + } diff --git a/Silkstring/Windows/ConfigWindow.cs b/Silkstring/Windows/ConfigWindow.cs index 1c265e9..427c628 100644 --- a/Silkstring/Windows/ConfigWindow.cs +++ b/Silkstring/Windows/ConfigWindow.cs @@ -32,7 +32,6 @@ public override void Draw() ImGui.SetNextItemWidth(150); if (ImGui.InputInt("###delayInput", ref delay, 10, 100)) { - delay = Math.Clamp(delay, 0, 1000); configuration.CommandDelay = delay; configuration.MarkDirty(); } @@ -66,9 +65,4 @@ public override void Draw() IsOpen = false; } } - - public void Open() - { - IsOpen = true; - } } diff --git a/Silkstring/Windows/MainWindow.cs b/Silkstring/Windows/MainWindow.cs index 4db1cd6..26b629c 100644 --- a/Silkstring/Windows/MainWindow.cs +++ b/Silkstring/Windows/MainWindow.cs @@ -15,8 +15,8 @@ public class MainWindow : Window, IDisposable private readonly AliasSelectPanel _selectPanel; private readonly AliasEditPanel _editPanel; - internal AliasEntry? _selectedAlias; - internal AliasFolder? _selectedFolder; + private AliasEntry? _selectedAlias; + private AliasFolder? _selectedFolder; internal AliasEntry? SelectedAlias => _selectedAlias; internal AliasFolder? SelectedFolder => _selectedFolder; @@ -24,7 +24,7 @@ public class MainWindow : Window, IDisposable public MainWindow(Plugin plugin, Action openSettings) : base("Silkstring###Main") { - _selectPanel = new AliasSelectPanel(plugin.Configuration, this, openSettings); + _selectPanel = new AliasSelectPanel(plugin.Configuration, this); _editPanel = new AliasEditPanel(plugin.Configuration, this); TitleBarButtons.Add(new TitleBarButton @@ -48,15 +48,18 @@ public override void PreDraw() public override void Draw() { - var scale= ImGui.GetIO().FontGlobalScale; + var scale = ImGui.GetIO().FontGlobalScale; var leftWidth = new Vector2(250 * scale, 0); - if (ImGui.BeginChild("###selector", leftWidth, true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) _selectPanel.Draw(); + ImGui.BeginChild("###selector", leftWidth, true, + ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse); + _selectPanel.Draw(); ImGui.EndChild(); ImGui.SameLine(); - if (ImGui.BeginChild("###editor", new Vector2(0, 0), true)) _editPanel.Draw(); + ImGui.BeginChild("###editor", new Vector2(0, 0), true); + _editPanel.Draw(); ImGui.EndChild(); } From 76ec2038d13d58c6a570d6b37e9f9c82f7a4e416 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Thu, 28 May 2026 21:24:05 -0300 Subject: [PATCH 09/11] Cleaned SelectPanel split footer to own class --- Silkstring/Models/AliasEntry.cs | 2 + .../UI/{ => SelectPanel}/AliasSelectPanel.cs | 140 +++--------------- .../UI/SelectPanel/SelectPanelFooter.cs | 131 ++++++++++++++++ 3 files changed, 150 insertions(+), 123 deletions(-) rename Silkstring/UI/{ => SelectPanel}/AliasSelectPanel.cs (60%) create mode 100644 Silkstring/UI/SelectPanel/SelectPanelFooter.cs diff --git a/Silkstring/Models/AliasEntry.cs b/Silkstring/Models/AliasEntry.cs index 68a40d1..c324689 100644 --- a/Silkstring/Models/AliasEntry.cs +++ b/Silkstring/Models/AliasEntry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; @@ -9,6 +10,7 @@ namespace Silkstring.Models; public class AliasEntry { private static int _nextId = 0; + public static readonly JsonSerializerOptions SerializerOptions = new() { IncludeFields = true }; public static readonly HashSet Blacklist = new(StringComparer.OrdinalIgnoreCase) { diff --git a/Silkstring/UI/AliasSelectPanel.cs b/Silkstring/UI/SelectPanel/AliasSelectPanel.cs similarity index 60% rename from Silkstring/UI/AliasSelectPanel.cs rename to Silkstring/UI/SelectPanel/AliasSelectPanel.cs index 3f93114..737fac9 100644 --- a/Silkstring/UI/AliasSelectPanel.cs +++ b/Silkstring/UI/SelectPanel/AliasSelectPanel.cs @@ -3,10 +3,6 @@ using System.Numerics; using System.Text.Json; using Dalamud.Bindings.ImGui; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using Dalamud.Interface.ImGuiNotification; -using Serilog; using Silkstring.Models; using Silkstring.Windows; @@ -16,6 +12,7 @@ public class AliasSelectPanel { private readonly Configuration _configuration; private readonly MainWindow _mainWindow; + private readonly SelectPanelFooter _footer; private string _filter = string.Empty; @@ -32,7 +29,6 @@ public class AliasSelectPanel private bool _focusRenameAlias = false; private const string DragDropType = "ALIAS"; - private const int FooterButtonCount = 5; private static readonly Vector4 FolderColor = new(0.7f, 0.5f, 1.0f, 1.0f); @@ -40,6 +36,7 @@ public AliasSelectPanel(Configuration configuration, MainWindow mainWindow) { _configuration = configuration; _mainWindow = mainWindow; + _footer = new SelectPanelFooter(configuration, mainWindow, BeginRenameAlias, BeginRenameFolder); } public void Draw() @@ -58,7 +55,7 @@ public void Draw() ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); ImGui.BeginChild("###footer", new Vector2(-1, frameHeight)); - DrawFooter(); + _footer.Draw(); ImGui.EndChild(); ImGui.PopStyleVar(2); } @@ -67,7 +64,7 @@ private void DrawFolders() { foreach (var folder in _configuration.Folders.ToList()) { - var filtered = folder.Aliases.Where(a => MatchesFilter(a)) + var filtered = folder.Aliases.Where(MatchesFilter) .OrderBy(a => string.IsNullOrWhiteSpace(a.DisplayName) ? a.Name : a.DisplayName) .ToList(); @@ -112,13 +109,7 @@ private void DrawFolders() if (ImGui.BeginPopupContextItem($"###folderContext{folder.UniqueId}")) { - if (ImGui.MenuItem("Rename")) - { - _renamingFolder = folder; - _renameBuffer = folder.Name; - _preRenameName = folder.Name; - _focusRename = true; - } + if (ImGui.MenuItem("Rename")) BeginRenameFolder(folder); if (ImGui.MenuItem("Delete")) { @@ -157,7 +148,7 @@ private void DrawFolders() private void DrawUnsorted() { - var filtered = _configuration.Aliases.Where(a => MatchesFilter(a)).OrderBy(a => string.IsNullOrWhiteSpace(a.DisplayName) ? a.Name : a.DisplayName).ToList(); + var filtered = _configuration.Aliases.Where(MatchesFilter).OrderBy(a => string.IsNullOrWhiteSpace(a.DisplayName) ? a.Name : a.DisplayName).ToList(); foreach (var alias in filtered) { @@ -212,9 +203,7 @@ private void DrawAliasRow(AliasEntry alias, AliasFolder? owningFolder) { if (ImGui.MenuItem("Rename")) { - _renamingAlias = alias; - _renameAliasBuffer = alias.DisplayName; - _focusRenameAlias = true; + BeginRenameAlias(alias); } if (!string.IsNullOrWhiteSpace(alias.DisplayName) && ImGui.MenuItem("Clear Display Name")) @@ -225,7 +214,7 @@ private void DrawAliasRow(AliasEntry alias, AliasFolder? owningFolder) if (ImGui.MenuItem("Export to Clipboard")) { - var json = JsonSerializer.Serialize(alias, new JsonSerializerOptions { IncludeFields = true }); + var json = JsonSerializer.Serialize(alias, AliasEntry.SerializerOptions); ImGui.SetClipboardText(json); } @@ -257,115 +246,20 @@ private void MoveAlias(AliasEntry alias, AliasFolder? from, AliasFolder? toFolde _draggedFromFolder = null; } - private void DrawFooter() + private void BeginRenameAlias(AliasEntry alias) { - var available = ImGui.GetContentRegionAvail(); - var buttonSize = new Vector2(MathF.Floor(available.X / FooterButtonCount), available.Y); - var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; - - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - - if (DrawIconButton(FontAwesomeIcon.Plus, buttonSize, "New Alias")) - { - var newAlias = new AliasEntry(); - _configuration.Aliases.Add(newAlias); - _configuration.Save(); - _mainWindow.SetSelection(newAlias, null); - _renamingAlias = newAlias; - _renameAliasBuffer = string.Empty; - _focusRenameAlias = true; - } - - ImGui.SameLine(); - if (DrawIconButton(FontAwesomeIcon.FileImport, buttonSize, "Import from Clipboard")) - { - try - { - var json = ImGui.GetClipboardText(); - var imported = - JsonSerializer.Deserialize(json, new JsonSerializerOptions { IncludeFields = true }); - if (imported != null) - { - _configuration.Aliases.Add(imported); - _configuration.Save(); - _mainWindow.SetSelection(imported, null); - _renamingAlias = imported; - _renameAliasBuffer = imported.DisplayName; - _focusRenameAlias = true; - } - } - catch (Exception ex) - { - Log.Error(ex, "Failed to import alias from clipboard"); - Plugin.NotificationManager.AddNotification(new Notification - { - Title = "Import Failure", - Content = "Could not import alias: Clipboard contents are not valid.", - Type = NotificationType.Error - }); - - } - } - - ImGui.SameLine(); - if(DrawIconButton(FontAwesomeIcon.Clone, buttonSize, "Clone Alias", disabled: _mainWindow.SelectedAlias == null)) - { - var source = _mainWindow.SelectedAlias!; - var cloned = source.Clone(); - - if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Add(cloned); - else _configuration.Aliases.Add(cloned); - - _configuration.Save(); - _mainWindow.SetSelection(cloned, _mainWindow.SelectedFolder); - _renamingAlias = cloned; - _renameAliasBuffer = cloned.DisplayName; - _focusRenameAlias = true; - } - - ImGui.SameLine(); - if (DrawIconButton(FontAwesomeIcon.FolderPlus, buttonSize, "New Folder")) - { - var newFolder = new AliasFolder { Name = "New Folder" }; - _configuration.Folders.Add(newFolder); - _configuration.Save(); - _renamingFolder = newFolder; - _renameBuffer = string.Empty; - _preRenameName = string.Empty; - _focusRename = true; - } - - ImGui.SameLine(); - if (DrawIconButton(FontAwesomeIcon.Trash, buttonSize, - canDelete ? "Delete Selected" : "Hold Shift + Ctrl to delete", - disabled: !canDelete || _mainWindow.SelectedAlias == null)) - { - if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Remove(_mainWindow.SelectedAlias!); - else _configuration.Aliases.Remove(_mainWindow.SelectedAlias!); - - _mainWindow.SetSelection(null, null); - _configuration.Save(); - } - - ImGui.PopStyleVar(); + _renamingAlias = alias; + _renameAliasBuffer = alias.DisplayName; + _focusRenameAlias = true; } - private bool DrawIconButton(FontAwesomeIcon icon, Vector2 size, string tooltip, bool disabled = false) + private void BeginRenameFolder(AliasFolder folder, bool isNew = false) { - var framePadding = ImGui.GetStyle().FramePadding; - var pX = Math.Max(0, (size.X - ImGui.GetFrameHeight()) / 2f + framePadding.X); - - if (disabled) ImGui.BeginDisabled(); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(pX, framePadding.Y)); - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); - var clicked = ImGuiComponents.IconButton((int)icon, icon); - ImGui.PopStyleVar(2); - if (disabled) ImGui.EndDisabled(); - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip(tooltip); - return clicked; + _renamingFolder = folder; + _renameBuffer = isNew ? string.Empty : folder.Name; + _preRenameName = isNew ? string.Empty : folder.Name; + _focusRename = true; } - private bool MatchesFilter(AliasEntry alias) { if (string.IsNullOrWhiteSpace(_filter)) return true; diff --git a/Silkstring/UI/SelectPanel/SelectPanelFooter.cs b/Silkstring/UI/SelectPanel/SelectPanelFooter.cs new file mode 100644 index 0000000..7f89975 --- /dev/null +++ b/Silkstring/UI/SelectPanel/SelectPanelFooter.cs @@ -0,0 +1,131 @@ +using System; +using System.Numerics; +using System.Text.Json; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification; +using Serilog; +using Silkstring.Models; +using Silkstring.Windows; + +namespace Silkstring.Ui; + +public class SelectPanelFooter +{ + private const int FooterButtonCount = 5; + + private readonly Configuration _configuration; + private readonly MainWindow _mainWindow; + private readonly Action _beginRenameAlias; + private readonly Action _beginRenameFolder; + + public SelectPanelFooter( + Configuration configuration, MainWindow mainWindow, Action beginRenameAlias, + Action beginRenameFolder) + { + _configuration = configuration; + _mainWindow = mainWindow; + _beginRenameAlias = beginRenameAlias; + _beginRenameFolder = beginRenameFolder; + } + public void Draw() + { + var available = ImGui.GetContentRegionAvail(); + var buttonSize = new Vector2(MathF.Floor(available.X / FooterButtonCount), available.Y); + var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + if (DrawIconButton(FontAwesomeIcon.Plus, buttonSize, "New Alias")) + { + var newAlias = new AliasEntry(); + _configuration.Aliases.Add(newAlias); + _configuration.Save(); + _mainWindow.SetSelection(newAlias, null); + _beginRenameAlias(newAlias); + } + + ImGui.SameLine(); + if (DrawIconButton(FontAwesomeIcon.FileImport, buttonSize, "Import from Clipboard")) + { + try + { + var json = ImGui.GetClipboardText(); + var imported = + JsonSerializer.Deserialize(json, AliasEntry.SerializerOptions); + if (imported != null) + { + _configuration.Aliases.Add(imported); + _configuration.Save(); + _mainWindow.SetSelection(imported, null); + _beginRenameAlias(imported); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to import alias from clipboard"); + Plugin.NotificationManager.AddNotification(new Notification + { + Title = "Import Failure", + Content = "Could not import alias: Clipboard contents are not valid.", + Type = NotificationType.Error + }); + + } + } + + ImGui.SameLine(); + if(DrawIconButton(FontAwesomeIcon.Clone, buttonSize, "Clone Alias", disabled: _mainWindow.SelectedAlias == null)) + { + var source = _mainWindow.SelectedAlias!; + var cloned = source.Clone(); + + if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Add(cloned); + else _configuration.Aliases.Add(cloned); + + _configuration.Save(); + _mainWindow.SetSelection(cloned, _mainWindow.SelectedFolder); + _beginRenameAlias(cloned); + } + + ImGui.SameLine(); + if (DrawIconButton(FontAwesomeIcon.FolderPlus, buttonSize, "New Folder")) + { + var newFolder = new AliasFolder { Name = "New Folder" }; + _configuration.Folders.Add(newFolder); + _configuration.Save(); + _beginRenameFolder(newFolder, true); + } + + ImGui.SameLine(); + if (DrawIconButton(FontAwesomeIcon.Trash, buttonSize, + canDelete ? "Delete Selected" : "Hold Shift + Ctrl to delete", + disabled: !canDelete || _mainWindow.SelectedAlias == null)) + { + if (_mainWindow.SelectedFolder != null) _mainWindow.SelectedFolder.Aliases.Remove(_mainWindow.SelectedAlias!); + else _configuration.Aliases.Remove(_mainWindow.SelectedAlias!); + + _mainWindow.SetSelection(null, null); + _configuration.Save(); + } + + ImGui.PopStyleVar(); + } + + private bool DrawIconButton(FontAwesomeIcon icon, Vector2 size, string tooltip, bool disabled = false) + { + var framePadding = ImGui.GetStyle().FramePadding; + var pX = Math.Max(0, (size.X - ImGui.GetFrameHeight()) / 2f + framePadding.X); + + if (disabled) ImGui.BeginDisabled(); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(pX, framePadding.Y)); + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); + var clicked = ImGuiComponents.IconButton((int)icon, icon); + ImGui.PopStyleVar(2); + if (disabled) ImGui.EndDisabled(); + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip(tooltip); + return clicked; + } +} From 5093fa1eb685cd85fe0285c90ee653bc8342e51b Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Thu, 28 May 2026 21:27:32 -0300 Subject: [PATCH 10/11] Tidied EditPanel --- Silkstring/UI/AliasEditPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index b64d92a..3f91387 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -54,8 +54,8 @@ private void DrawAliasHeader(AliasEntry alias) if (ImGui.Checkbox($"###enabled{alias.UniqueId}", ref alias.Enabled)) _configuration.Save(); ImGui.SameLine(); ImGui.SetNextItemWidth(-1); - if(ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) _configuration.MarkDirty(); - if(ImGui.IsItemHovered()) ImGui.SetTooltip("Separate multiple aliases with | e.g. mew|meow|mreow"); + if (ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) _configuration.MarkDirty(); + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Separate multiple aliases with | e.g. mew|meow|mreow"); } private void DrawCommandList(AliasEntry alias) @@ -80,7 +80,7 @@ private void DrawMultilineView(AliasEntry alias) if (ImGui.InputTextMultiline($"###multiline{alias.UniqueId}", ref _multilineBuffer, 5000, new Vector2(-1, ImGui.GetContentRegionAvail().Y))) { - var lines = _multilineBuffer.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim()).ToList(); + var lines = _multilineBuffer.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); for (var i = 0; i < lines.Count; i++) { if (i < alias.Output.Count) alias.Output[i].Command = lines[i]; From a48059bb01808bfa5b5b6accc1a2728afac28d35 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Thu, 28 May 2026 21:36:00 -0300 Subject: [PATCH 11/11] Add tooltip to enable/disable button --- Silkstring/UI/AliasEditPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index 3f91387..6cb2b61 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -51,7 +51,9 @@ private void DrawEmptyState() private void DrawAliasHeader(AliasEntry alias) { + var tooltipText = alias.Enabled ? "Disable this alias" : "Enable this alias"; if (ImGui.Checkbox($"###enabled{alias.UniqueId}", ref alias.Enabled)) _configuration.Save(); + if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltipText); ImGui.SameLine(); ImGui.SetNextItemWidth(-1); if (ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) _configuration.MarkDirty();