Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
283cbe4
first implementation of history
01Dri May 2, 2026
f4ac497
HasCalculationOperator method to just add calc in history if have a o…
01Dri May 2, 2026
05eae58
setting to enable or disable history
01Dri May 2, 2026
e4f5dee
badge in history item and fix some bugs
01Dri May 2, 2026
dc2dfb7
calculator tests with history
01Dri May 2, 2026
a7c1f33
validation to add or not result in history
01Dri May 6, 2026
df72b99
debounce logic to add result in history
01Dri May 10, 2026
4fc3016
code quality
01Dri May 10, 2026
6d93ce0
fix unit tests and code quality
01Dri May 10, 2026
ed6cccf
order to remove the last item added
01Dri Jul 4, 2026
83efd29
new modes to save history
01Dri Jul 4, 2026
e7d0dd3
query and enter mode
01Dri Jul 4, 2026
0d1b2bb
Merge branch 'dev' into feature/history-calc-plugin
01Dri Jul 4, 2026
2f7f2bb
Update Plugins/Flow.Launcher.Plugin.Calculator/Storage/History.cs
01Dri Jul 4, 2026
264d9ad
code quality after ai review
01Dri Jul 4, 2026
a745873
Merge branch 'feature/history-calc-plugin' of https://github.com/01Dr…
01Dri Jul 4, 2026
a7efe5d
revert changes pt-br
01Dri Jul 5, 2026
285859d
Change history badge icon to clearer one
DavidGBrett Jul 14, 2026
197bb14
Merge branch 'dev' into feature/history-calc-plugin
01Dri Oct 14, 2026
5f56d2a
refactor: review adjusts
01Dri Oct 14, 2026
00c3354
Capitalize Just now string in calculator plugin
DavidGBrett Oct 17, 2026
9ca1fa8
Give calculation result max score so its above the history results in…
DavidGBrett Oct 17, 2026
87cd651
refactor: fix pr comments
01Dri Oct 18, 2026
a09ec49
refactor: back result score
01Dri Oct 18, 2026
2df2c6c
Reduce the score of the calculation result slightly so that its still…
DavidGBrett Oct 18, 2026
2b1e56f
Keep the calculation result above history without overriding topmost …
DavidGBrett Oct 18, 2026
56818d4
Save calculator history when result is executed
DavidGBrett Oct 18, 2026
791f2f8
Fix history flushing for result execution
DavidGBrett Oct 18, 2026
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
44 changes: 43 additions & 1 deletion Flow.Launcher.Test/Plugins/CalculatorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Reflection;
using Flow.Launcher.Plugin.Calculator;
using Flow.Launcher.Plugin.Calculator.Storage;
using Mages.Core;
using NUnit.Framework;
using NUnit.Framework.Legacy;
Expand All @@ -17,7 +18,8 @@ public class CalculatorPluginTest
DecimalSeparator = DecimalSeparator.UseSystemLocale,
MaxDecimalPlaces = 10,
ShowErrorMessage = false, // Make sure we return the empty results when error occurs
UseThousandsSeparator = true // Default value
UseThousandsSeparator = true, // Default value
EnableHistory = true,
};
private readonly Engine _engine = new(new Configuration
{
Expand All @@ -42,6 +44,12 @@ public CalculatorPluginTest()
engineField.SetValue(null, _engine);
}

[SetUp]
public void SetUp()
{
GetHistory().Items.Clear();
}

[Test]
public void ThousandsSeparatorTest_Enabled()
{
Expand Down Expand Up @@ -80,6 +88,27 @@ public void ThousandsSeparatorTest_LargeNumber()
ClassicAssert.AreEqual("1234567", result);
}

[Test]
public void CalculationHistory_IsStoredWhenEnabled()
{
_settings.EnableHistory = true;

_plugin.Query(new Plugin.Query { Search = "1+1" });

ClassicAssert.AreEqual(1, GetHistory().Items.Count);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
ClassicAssert.AreEqual("1+1", GetHistory().Items[0].Query);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

[Test]
public void CalculationHistory_IsNotStoredWhenDisabled()
{
_settings.EnableHistory = false;

_plugin.Query(new Plugin.Query { Search = "1+1" });

ClassicAssert.AreEqual(0, GetHistory().Items.Count);
}
Comment thread
Jack251970 marked this conversation as resolved.

// Basic operations
[TestCase(@"1+1", "2")]
[TestCase(@"2-1", "1")]
Expand Down Expand Up @@ -130,5 +159,18 @@ private string GetCalculationResult(string expression)
});
return results.Count > 0 ? results[0].Title : string.Empty;
}

private static History GetHistory()
{
var historyField = typeof(Main).GetField("History", BindingFlags.NonPublic | BindingFlags.Static);
Assert.That(historyField, Is.Not.Null,
"Could not find static field 'History' on Flow.Launcher.Plugin.Calculator.Main");

var history = historyField!.GetValue(null) as History;
Assert.That(history, Is.Not.Null,
"Static field 'History' on Flow.Launcher.Plugin.Calculator.Main' was not a calculator History instance");

return history!;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
Comment thread
DavidGBrett marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@
<system:String x:Key="flowlauncher_plugin_calculator_use_thousands_separator">Show thousands separator in results</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_failed_to_copy">Copy failed, please try later</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_show_error_message">Show error message when calculation fails</system:String>
</ResourceDictionary>
<system:String x:Key="flowlauncher_plugin_calculator_enable_history">Enable History</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_history_subtitle">Calculated at {0}</system:String>
</ResourceDictionary>
81 changes: 55 additions & 26 deletions Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows.Controls;
using Flow.Launcher.Plugin.Calculator.Storage;
using Flow.Launcher.Plugin.Calculator.ViewModels;
using Flow.Launcher.Plugin.Calculator.Views;
using Mages.Core;
Expand All @@ -24,8 +25,10 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
private const string Comma = ",";
private const string Dot = ".";
private const string IcoPath = "Images/calculator.png";

private static readonly List<Result> EmptyResults = [];

private History History { get; set; } = null!;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
internal static PluginInitContext Context { get; private set; } = null!;

private Settings _settings;
Expand All @@ -35,6 +38,7 @@ public void Init(PluginInitContext context)
{
Context = context;
_settings = context.API.LoadSettingJsonStorage<Settings>();
History = context.API.LoadSettingJsonStorage<History>();
_viewModel = new SettingsViewModel(_settings);

MagesEngine = new Engine(new Configuration
Expand All @@ -57,7 +61,7 @@ public List<Result> Query(Query query)
{
var search = query.Search;
bool isFunctionPresent = FunctionRegex.IsMatch(search);

bool isValidResultToAddHistory = true;
// Mages is case sensitive, so we need to convert all function names to lower case.
search = FunctionRegex.Replace(search, m => m.Value.ToLowerInvariant());

Expand Down Expand Up @@ -119,43 +123,47 @@ public List<Result> Query(Query query)
if (result.ToString() == "NaN")
{
result = Localize.flowlauncher_plugin_calculator_not_a_number();
isValidResultToAddHistory = false;
}

if (result is Function)
{
result = Localize.flowlauncher_plugin_calculator_expression_not_complete();
isValidResultToAddHistory = false;
}

if (!string.IsNullOrEmpty(result.ToString()))
{
decimal roundedResult = Math.Round(Convert.ToDecimal(result), _settings.MaxDecimalPlaces, MidpointRounding.AwayFromZero);
string newResult = FormatResult(roundedResult);

var results = new List<Result>();
var action = CreateClipboardAction(newResult);
var resultObject = new Result
{
Title = newResult,
IcoPath = IcoPath,
Score = 300,
// Check context nullability for unit testing
SubTitle = Context == null
? string.Empty
: Localize.flowlauncher_plugin_calculator_copy_number_to_clipboard(),
CopyText = newResult,
Action = action
};


if (isValidResultToAddHistory)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
{
History.AddOrUpdate(new History.PendingHistoryItem(resultObject, expression, action));
}

results.Add(resultObject);
var historyItems = _settings.EnableHistory
? History.GetItemsExcluding(expression)
: [];
return results.Concat(historyItems).ToList();

return
[
new Result
{
Title = newResult,
IcoPath = IcoPath,
Score = 300,
// Check context nullability for unit testing
SubTitle = Context == null ? string.Empty : Localize.flowlauncher_plugin_calculator_copy_number_to_clipboard(),
CopyText = newResult,
Action = c =>
{
try
{
Context.API.CopyToClipboard(newResult);
return true;
}
catch (ExternalException)
{
Context.API.ShowMsgBox(Localize.flowlauncher_plugin_calculator_failed_to_copy());
return false;
}
}
}
];
}
}
catch (Exception)
Expand All @@ -175,6 +183,27 @@ public List<Result> Query(Query query)
return EmptyResults;
}


private Func<ActionContext, bool> CreateClipboardAction(string newResult)
{
return (_) =>
{
try
{
Context.API.CopyToClipboard(newResult);

return true;
}
catch (ExternalException)
{
Context.API.ShowMsgBox(
Localize.flowlauncher_plugin_calculator_failed_to_copy()
);
return false;
}
};
}

private static string PowMatchEvaluator(Match m)
{
// m.Groups[1].Value will be `(...)` with parens
Expand Down
2 changes: 2 additions & 0 deletions Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public class Settings
public bool ShowErrorMessage { get; set; } = false;

public bool UseThousandsSeparator { get; set; } = true;

public bool EnableHistory { get; set; } = false;
}
85 changes: 85 additions & 0 deletions Plugins/Flow.Launcher.Plugin.Calculator/Storage/History.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;

namespace Flow.Launcher.Plugin.Calculator.Storage;

public class History
{
private const int MaxItems = 5;
private const int DebounceDelayMs = 800;

[JsonInclude]
public List<HistoryItem> Items { get; set; } = [];

[JsonIgnore]
private readonly Lock _syncRoot = new();

[JsonIgnore]
private Timer _debounceTimer;

[JsonIgnore]
private PendingHistoryItem _pendingItem;

public void AddOrUpdate(PendingHistoryItem pendingItem)
{
lock (_syncRoot)
{
_pendingItem = pendingItem;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

if (_debounceTimer == null)
{
_debounceTimer = new Timer(_ => FlushPendingItem(), null, DebounceDelayMs, Timeout.Infinite);
}
else
{
_debounceTimer.Change(DebounceDelayMs, Timeout.Infinite);
}
}
}

public List<HistoryItem> GetItemsExcluding(string expression)
{
lock (_syncRoot)
{
return Items
.Where(x => x.Query != expression)
.OrderByDescending(x => x.CalculatedAt)
.ToList();
}
}

private void FlushPendingItem()
{
lock (_syncRoot)
{
if (_pendingItem == null)
{
return;
}

var currentItem = Items.FirstOrDefault(x => x.Query == _pendingItem.Expression);
if (currentItem != null)
{
currentItem.Refresh(_pendingItem.Result, _pendingItem.Action);
}
else
{
var item = new HistoryItem(_pendingItem.Result, _pendingItem.Expression, _pendingItem.Action);

if (Items.Count >= MaxItems)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
{
Items.RemoveAt(0);
}
Comment thread
01Dri marked this conversation as resolved.
Outdated

Items.Add(item);
}

_pendingItem = null;
}
}

public sealed record PendingHistoryItem(Result Result, string Expression, Func<ActionContext, bool> Action);
}
46 changes: 46 additions & 0 deletions Plugins/Flow.Launcher.Plugin.Calculator/Storage/HistoryItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Text.Json.Serialization;

namespace Flow.Launcher.Plugin.Calculator.Storage;

public class HistoryItem : Result
{
public string Query { get; set; } = string.Empty;
public DateTime CalculatedAt { get; set; }

[JsonIgnore]
private const string BadgeIconPath = "Images/history.png";
public HistoryItem()
{
}

public HistoryItem(Result result, string expression, Func<ActionContext, bool> action)
{
CalculatedAt = DateTime.Now;
Title = expression;
SubTitle = CreateSubTitle(result.Title);
Score = 300;
IcoPath = result.IcoPath;
Query = expression;
BadgeIcoPath = BadgeIconPath;
ShowBadge = true;
CopyText = result.Title;
Action = action;
}

public void Refresh(Result result, Func<ActionContext, bool> action)
{
CalculatedAt = DateTime.Now;
SubTitle = CreateSubTitle(result.Title);
CopyText = result.Title;
Action = action;
}


private string CreateSubTitle(string value)
{
return
$"{value} - {Localize.flowlauncher_plugin_calculator_copy_number_to_clipboard()}" +
$"\n{Localize.flowlauncher_plugin_calculator_history_subtitle(CalculatedAt)}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
Expand Down Expand Up @@ -79,5 +80,15 @@
VerticalAlignment="Center"
Content="{DynamicResource flowlauncher_plugin_calculator_show_error_message}"
IsChecked="{Binding Settings.ShowErrorMessage, Mode=TwoWay}" />

<CheckBox
Grid.Row="4"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{DynamicResource flowlauncher_plugin_calculator_enable_history}"
IsChecked="{Binding Settings.EnableHistory, Mode=TwoWay}" />
</Grid>
</UserControl>
Loading