Files
FluentTerminal/FluentTerminal.App/Services/CommandHistoryService.cs
T
Aleksandar Pesic 7ab42ec1de Menu item keyboard accelerators implemented (#589)
* Added icon to "Recent >" menu item.

* Refactored app menu implementation, and implemented keyboard accelerators. Still doesn't work though.

* Few things fixed, but still doesn't work.

* Clicking menu items works.

* Fixing an issue with resources.

* New menu binding implemented. Still doesn't work though.

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

* Further refactoring app menu binding, in order to make it more general.

* New window mechanism simplified. Terminal-launching code refactored to be more readable. A bug with "SSH Info" fixed.

* Merging the latest changed.
2019-11-26 22:03:22 +01:00

372 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.Models;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
using Newtonsoft.Json;
namespace FluentTerminal.App.Services
{
public class CommandHistoryService : ICommandHistoryService
{
#region Constants
// Maximal number of commands to save to history
private const int MaxHistory = 1_000;
private static readonly DateTime NeverUsedTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
#endregion Constants
#region Static
private static string GetHash(string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
value = value.ToLowerInvariant();
byte[] hashed;
using (var md5 = MD5.Create())
{
hashed = md5.ComputeHash(Encoding.UTF32.GetBytes(value));
}
var builder = new StringBuilder();
foreach (var b in hashed)
{
builder.Append(b.ToString("X2"));
}
return $"hist_{builder}_{value.Length:##########}";
}
#endregion Static
#region Fields
private readonly IApplicationDataContainer _historyContainer;
private readonly ISettingsService _settingsService;
private List<ExecutedCommand> _history;
#endregion Fields
#region Constructor
public CommandHistoryService(ApplicationDataContainers containers, ISettingsService settingsService)
{
_settingsService = settingsService;
_historyContainer = containers.HistoryContainer;
}
#endregion Constructor
#region Methods
private List<ShellProfile> GetAllProfiles() =>
_settingsService.GetSshProfiles().Union(_settingsService.GetShellProfiles()).ToList();
private List<ExecutedCommand> GetRawHistory(Func<List<ShellProfile>> profilesProvider = null)
{
if (_history == null)
{
_history = LoadHistory();
CleanupHistory(profilesProvider);
}
return _history;
}
private List<ExecutedCommand> LoadHistory()
{
List<ExecutedCommand> history = null;
try
{
history = _historyContainer.GetAll()
.Select(c => JsonConvert.DeserializeObject<ExecutedCommand>((string) c)).ToList();
}
catch
{
// ignored
}
if (history == null)
{
// If there's an issue with getting history, we simply clear history as if the app haven't been used at all.
// Not too useful fix, but it shouldn't happen ever anyway.
_historyContainer.Clear();
return new List<ExecutedCommand>();
}
FixMoshBackwardCompatibility(history);
return history;
}
private void FixMoshBackwardCompatibility(List<ExecutedCommand> commands)
{
for (var i = 0; i < commands.Count; i++)
{
var command = commands[i];
if (command.ShellProfile == null) continue;
var newProfile = MoshBackwardCompatibility.FixProfile(command.ShellProfile);
if (ReferenceEquals(command.ShellProfile, newProfile)) continue;
if (!command.ProfileId.HasValue)
{
newProfile.Name = $"{newProfile.Location} {newProfile.Arguments}".Trim();
}
var newCommand = new ExecutedCommand
{
Value = newProfile.Name,
ProfileId = command.ProfileId,
LastExecution = command.LastExecution,
ExecutionCount = command.ExecutionCount,
ShellProfile = newProfile
};
commands.Insert(i, newCommand);
commands.RemoveAt(i + 1);
Delete(command);
Save(newCommand);
}
}
private void Save(ExecutedCommand executedCommand)
{
_historyContainer.WriteValueAsJson(GetHash(executedCommand.Value), executedCommand);
Messenger.Default.Send(new CommandHistoryChangedMessage());
}
/// <summary>
/// Removes all profiles from history if they are deleted in the settings page meanwhile.
/// </summary>
private void CleanupHistory(Func<List<ShellProfile>> profilesProvider = null)
{
if (_history == null)
{
// Not loaded yet, so we won't clear. This won't happen though.
return;
}
if (!_history.Any())
{
return;
}
var profiles = profilesProvider?.Invoke() ?? GetAllProfiles();
for (var i = 0; i < _history.Count; i++)
{
var command = _history[i];
if (command.ProfileId.HasValue && !profiles.Any(p => p.Id.Equals(command.ProfileId.Value)))
{
_history.Remove(command);
Delete(command);
i--;
continue;
}
if (command.ShellProfile == null)
{
var profile = profiles.FirstOrDefault(p =>
string.Equals(p.Name, command.Value, StringComparison.OrdinalIgnoreCase));
if (profile == null)
{
_history.Remove(command);
Delete(command);
i--;
}
else
{
command.ProfileId = profile.Id;
Save(command);
}
}
}
}
private IEnumerable<ExecutedCommand> GetHistory(bool byNumberOfUsages, bool includeProfiles,
Func<List<ShellProfile>> profilesProvider = null)
{
var allProfiles = new Lazy<List<ShellProfile>>(profilesProvider ?? GetAllProfiles);
var history = GetRawHistory(() => allProfiles.Value);
if (!history.Any())
{
yield break;
}
var ordered = byNumberOfUsages
? history.OrderByDescending(c => c.ExecutionCount).ThenByDescending(c => c.LastExecution).ToList()
: history.OrderByDescending(c => c.LastExecution).ThenByDescending(c => c.ExecutionCount).ToList();
foreach (var command in ordered)
{
if (command.ProfileId.HasValue)
{
var profile = allProfiles.Value.FirstOrDefault(p => p.Id.Equals(command.ProfileId.Value));
if (profile == null)
{
history.Remove(command);
Delete(command);
}
else
{
allProfiles.Value.Remove(profile);
var newCommand = command.Clone();
newCommand.ShellProfile = profile;
yield return newCommand;
}
}
else
{
yield return command.Clone();
}
}
if (!includeProfiles)
{
yield break;
}
foreach (var unusedProfile in allProfiles.Value)
{
yield return new ExecutedCommand
{
Value = unusedProfile.Name,
ProfileId = unusedProfile.Id,
ExecutionCount = 0,
LastExecution = NeverUsedTime,
ShellProfile = unusedProfile
};
}
}
/// <inheritdoc cref="ICommandHistoryService.GetHistoryRecentFirst"/>
public List<ExecutedCommand> GetHistoryRecentFirst(bool includeProfiles = false, int top = int.MaxValue,
Func<List<ShellProfile>> profileProvider = null) =>
GetHistory(false, includeProfiles, profileProvider).Take(top).ToList();
/// <inheritdoc cref="ICommandHistoryService.GetHistoryMostUsedFirst"/>
public List<ExecutedCommand> GetHistoryMostUsedFirst(bool includeProfiles = false, int top = int.MaxValue,
Func<List<ShellProfile>> profileProvider = null) =>
GetHistory(true, includeProfiles, profileProvider).Take(top).ToList();
public void MarkUsed(ShellProfile profile)
{
var history = GetRawHistory();
var existing = history.FirstOrDefault(c =>
c.ProfileId?.Equals(profile.Id) ??
string.Equals(profile.Name, c.Value, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
if (string.IsNullOrEmpty(profile.Name))
{
profile.Name = $"{profile.Location} {profile.Arguments}".Trim();
}
existing = new ExecutedCommand {Value = profile.Name};
if (GetAllProfiles().Any(p => p.Id.Equals(profile.Id)))
{
existing.ProfileId = profile.Id;
}
else
{
existing.ShellProfile = profile;
}
var overflow = history.Count - MaxHistory + 1;
if (overflow > 0)
{
// We already have max number of commands in history, so we need to delete some
// Let's first try with cleanup
CleanupHistory();
overflow = history.Count - MaxHistory + 1;
if (overflow > 0)
{
// We will remove the oldest commands
var toRemove = history.OrderBy(c => c.LastExecution).ThenBy(c => c.ExecutionCount)
.Take(overflow).ToList();
foreach (var command in toRemove)
{
history.Remove(command);
Delete(command);
}
}
}
history.Add(existing);
}
existing.LastExecution = DateTime.UtcNow;
existing.ExecutionCount++;
Save(existing);
}
public void Clear()
{
_history?.Clear();
_historyContainer.Clear();
Messenger.Default.Send(new CommandHistoryChangedMessage());
}
public void Delete(ExecutedCommand executedCommand)
{
_history?.RemoveAll(c => string.Equals(c.Value, executedCommand.Value, StringComparison.OrdinalIgnoreCase));
_historyContainer.Delete(GetHash(executedCommand.Value));
Messenger.Default.Send(new CommandHistoryChangedMessage());
}
#endregion Methods
}
}