mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 19:52:10 +00:00
Merge 3a3e8780e8 into df4663fff4
This commit is contained in:
commit
6273583230
@ -1615,6 +1615,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T114706247
|
||||
-- Select a log file
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1231773010"] = "Select a log file"
|
||||
|
||||
-- Show only matching lines
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T131451442"] = "Show only matching lines"
|
||||
|
||||
-- Log level
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1318706515"] = "Log level"
|
||||
|
||||
@ -1645,6 +1648,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T228853842
|
||||
-- Showing {0} of {1} lines. Last refresh: {2}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2378353570"] = "Showing {0} of {1} lines. Last refresh: {2}."
|
||||
|
||||
-- Disable auto-refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T240826492"] = "Disable auto-refresh"
|
||||
|
||||
-- Enable auto-refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2467283075"] = "Enable auto-refresh"
|
||||
|
||||
-- No matching log lines.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2511997530"] = "No matching log lines."
|
||||
|
||||
@ -1657,8 +1666,11 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T268681396
|
||||
-- Loaded {0} lines. Last refresh: {1}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2920304709"] = "Loaded {0} lines. Last refresh: {1}."
|
||||
|
||||
-- Filter only
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3152625639"] = "Filter only"
|
||||
-- Disable inverted dropdown filters
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3035207754"] = "Disable inverted dropdown filters"
|
||||
|
||||
-- Copy visible lines
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3292961985"] = "Copy visible lines"
|
||||
|
||||
-- Loading log file...
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481"] = "Loading log file..."
|
||||
@ -1666,21 +1678,24 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481
|
||||
-- Unknown error
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3461425987"] = "Unknown error"
|
||||
|
||||
-- Exclude
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3551498825"] = "Exclude"
|
||||
|
||||
-- The log file path is not available yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3686775689"] = "The log file path is not available yet."
|
||||
|
||||
-- Logger
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T376222229"] = "Logger"
|
||||
|
||||
-- Auto-refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3837203600"] = "Auto-refresh"
|
||||
|
||||
-- not loaded yet
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3863250749"] = "not loaded yet"
|
||||
|
||||
-- Loading...
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T397479987"] = "Loading..."
|
||||
|
||||
-- Select a dropdown filter to invert
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3991737584"] = "Select a dropdown filter to invert"
|
||||
|
||||
-- Usage log
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4031747274"] = "Usage log"
|
||||
|
||||
@ -1693,11 +1708,17 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T413024177
|
||||
-- Opened the log file location.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4162897654"] = "Opened the log file location."
|
||||
|
||||
-- Hide timestamps
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4263992002"] = "Hide timestamps"
|
||||
|
||||
-- Show timestamps
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133"] = "Show timestamps"
|
||||
|
||||
-- Clear
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T77955010"] = "Clear"
|
||||
-- Highlight matches without hiding lines
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T661180902"] = "Highlight matches without hiding lines"
|
||||
|
||||
-- Invert dropdown filters
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T8477641"] = "Invert dropdown filters"
|
||||
|
||||
-- You can enter text, attach one or more documents, or use both. At least one input is required.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required."
|
||||
|
||||
@ -13,17 +13,22 @@
|
||||
<MudSelectItem T="LogFileKind" Value="@LogFileKind.APP">@T("Usage log")</MudSelectItem>
|
||||
<MudSelectItem T="LogFileKind" Value="@LogFileKind.STARTUP">@T("Startup log")</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudCheckBox T="bool" Value="@this.autoRefresh" ValueChanged="@this.AutoRefreshChanged" Label="@T("Auto-refresh")" Color="Color.Primary" />
|
||||
<MudTooltip Text="@this.AutoRefreshTooltip" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Update" Variant="@this.AutoRefreshButtonVariant" Color="@this.AutoRefreshButtonColor" OnClick="@this.ToggleAutoRefresh" />
|
||||
</MudTooltip>
|
||||
@if (!this.autoRefresh)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.RefreshLogAsync" Disabled="@this.isLoading">
|
||||
@T("Refresh")
|
||||
</MudButton>
|
||||
<MudTooltip Text="@T("Refresh")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Variant="Variant.Filled" Color="Color.Primary" OnClick="@this.RefreshLogAsync" Disabled="@this.isLoading" />
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudNumericField T="int" Value="@this.maxLines" ValueChanged="@this.MaxLinesChanged" Label="@T("Max lines")" Variant="Variant.Outlined" Margin="Margin.Dense" Min="@MIN_MAX_LINES" Max="@MAX_MAX_LINES" Step="500" Class="log-viewer-number" />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Default" StartIcon="@Icons.Material.Filled.FolderOpen" OnClick="@this.OpenCurrentLogInFileManager" Disabled="@(!this.CanOpenCurrentLogPath)">
|
||||
@T("Open in folder")
|
||||
</MudButton>
|
||||
<MudTooltip Text="@T("Open in folder")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Default" OnClick="@this.OpenCurrentLogInFileManager" Disabled="@(!this.CanOpenCurrentLogPath)" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Copy visible lines")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Variant="Variant.Outlined" Color="Color.Default" Disabled="@(!this.CanCopyDisplayedLines)" OnClick="@this.CopyDisplayedLines" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" AlignItems="@AlignItems.Center" Spacing="2" Class="mb-1">
|
||||
@ -51,15 +56,20 @@
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudCheckBox T="bool" @bind-Value="@this.ShowTimestamps" Label="@T("Show timestamps")" Color="Color.Primary" />
|
||||
<MudTooltip Text="@this.ShowTimestampsTooltip" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Schedule" Variant="@this.ShowTimestampsButtonVariant" Color="@this.ShowTimestampsButtonColor" OnClick="@this.ToggleShowTimestamps" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@this.InvertFiltersTooltip" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.SearchOff" Variant="@this.InvertFiltersButtonVariant" Color="@this.InvertFiltersButtonColor" OnClick="@this.ToggleInvertFilters" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" AlignItems="@AlignItems.Center" Spacing="2" Class="mb-1">
|
||||
<MudTextField T="string" @bind-Text="@this.FilterText" Immediate="@true" Label="@T("Find")" Variant="Variant.Outlined" Margin="Margin.Dense" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" Class="log-viewer-filter" />
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" StartIcon="@Icons.Material.Filled.Clear" Disabled="@(!this.HasActiveFilter)" OnClick="@this.ClearFilters">
|
||||
@T("Clear")
|
||||
</MudButton>
|
||||
<MudCheckBox T="bool" @bind-Value="@this.FilterOnly" Label="@T("Filter only")" Color="Color.Primary" />
|
||||
<MudTextField T="string" @bind-Text="@this.FilterText" Immediate="@true" Label="@T("Find")" Variant="Variant.Outlined" Margin="Margin.Dense" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" Clearable="@true" ClearIcon="@Icons.Material.Filled.Clear" OnClearButtonClick="@(() => this.ClearFilterText())" Class="log-viewer-filter" />
|
||||
<MudTooltip Text="@this.FilterOnlyTooltip" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Visibility" Variant="@this.FilterOnlyButtonVariant" Color="@this.FilterOnlyButtonColor" OnClick="@this.ToggleFilterOnly" />
|
||||
</MudTooltip>
|
||||
<MudTextField T="string" @bind-Text="@this.ExcludeText" Immediate="@true" Label="@T("Exclude")" Variant="Variant.Outlined" Margin="Margin.Dense" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Block" Clearable="@true" ClearIcon="@Icons.Material.Filled.Clear" OnClearButtonClick="@(() => this.ClearExcludeText())" Class="log-viewer-filter" />
|
||||
</MudStack>
|
||||
|
||||
<MudText Typo="Typo.body2" Class="log-viewer-path mb-1">
|
||||
|
||||
@ -14,7 +14,6 @@ namespace AIStudio.Assistants.LogViewer;
|
||||
public partial class AssistantLogViewer : MSGComponentBase
|
||||
{
|
||||
private static readonly TimeSpan AUTO_REFRESH_INTERVAL = TimeSpan.FromSeconds(5);
|
||||
private static readonly char[] WORD_SPLIT_CHARS = [' ', '\t', '\r', '\n'];
|
||||
private static readonly Dictionary<string, int> LOG_LEVEL_ORDER = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["ERROR"] = 0,
|
||||
@ -57,12 +56,15 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
private List<string> sourceDetailOptions = [OTHER_OPTION_VALUE];
|
||||
|
||||
private string[] activeSearchTerms = [];
|
||||
private string[] activeExcludeTerms = [];
|
||||
private CancellationTokenSource? autoRefreshCancellationTokenSource;
|
||||
private string filterText = string.Empty;
|
||||
private string excludeText = string.Empty;
|
||||
private string loadError = string.Empty;
|
||||
private bool isLoading;
|
||||
private bool autoRefresh;
|
||||
private bool filterOnly = true;
|
||||
private bool invertFilters;
|
||||
private bool showTimestamps = true;
|
||||
private int maxLines = DEFAULT_MAX_LINES;
|
||||
private int totalLineCount;
|
||||
@ -75,7 +77,40 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
|
||||
private bool HasDropdownFilter => this.selectedLogLevels.Count > 0 || this.selectedLoggers.Count > 0 || this.selectedSourceDetails.Count > 0;
|
||||
|
||||
private bool HasActiveFilter => !string.IsNullOrWhiteSpace(this.filterText) || this.HasDropdownFilter;
|
||||
private bool CanCopyDisplayedLines => this.displayLines.Count > 0;
|
||||
|
||||
private string AutoRefreshTooltip => this.autoRefresh ? T("Disable auto-refresh") : T("Enable auto-refresh");
|
||||
|
||||
private Variant AutoRefreshButtonVariant => GetToggleVariant(this.autoRefresh);
|
||||
|
||||
private Color AutoRefreshButtonColor => GetToggleColor(this.autoRefresh);
|
||||
|
||||
private string FilterOnlyTooltip => this.filterOnly ? T("Highlight matches without hiding lines") : T("Show only matching lines");
|
||||
|
||||
private Variant FilterOnlyButtonVariant => GetToggleVariant(this.filterOnly);
|
||||
|
||||
private Color FilterOnlyButtonColor => GetToggleColor(this.filterOnly);
|
||||
|
||||
private string ShowTimestampsTooltip => this.showTimestamps ? T("Hide timestamps") : T("Show timestamps");
|
||||
|
||||
private Variant ShowTimestampsButtonVariant => GetToggleVariant(this.showTimestamps);
|
||||
|
||||
private Color ShowTimestampsButtonColor => GetToggleColor(this.showTimestamps);
|
||||
|
||||
private string InvertFiltersTooltip
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!this.HasDropdownFilter)
|
||||
return T("Select a dropdown filter to invert");
|
||||
|
||||
return this.invertFilters ? T("Disable inverted dropdown filters") : T("Invert dropdown filters");
|
||||
}
|
||||
}
|
||||
|
||||
private Variant InvertFiltersButtonVariant => GetToggleVariant(this.invertFilters);
|
||||
|
||||
private Color InvertFiltersButtonColor => GetToggleColor(this.invertFilters);
|
||||
|
||||
private string FilterText
|
||||
{
|
||||
@ -90,6 +125,19 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private string ExcludeText
|
||||
{
|
||||
get => this.excludeText;
|
||||
set
|
||||
{
|
||||
if (this.excludeText == value)
|
||||
return;
|
||||
|
||||
this.excludeText = value;
|
||||
this.RefreshDisplayLines();
|
||||
}
|
||||
}
|
||||
|
||||
private bool FilterOnly
|
||||
{
|
||||
get => this.filterOnly;
|
||||
@ -103,6 +151,20 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private bool InvertFilters
|
||||
{
|
||||
get => this.invertFilters;
|
||||
set
|
||||
{
|
||||
var normalizedValue = value && this.HasDropdownFilter;
|
||||
if (this.invertFilters == normalizedValue)
|
||||
return;
|
||||
|
||||
this.invertFilters = normalizedValue;
|
||||
this.RefreshDisplayLines();
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShowTimestamps
|
||||
{
|
||||
get => this.showTimestamps;
|
||||
@ -171,6 +233,7 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
private Task SelectedLogLevelsChanged(IEnumerable<string?>? selectedValues)
|
||||
{
|
||||
UpdateSelectedValues(this.selectedLogLevels, selectedValues);
|
||||
this.DisableInvertFiltersIfNoDropdownFilter();
|
||||
this.RefreshDisplayLines();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@ -178,6 +241,7 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
private Task SelectedLoggersChanged(IEnumerable<string?>? selectedValues)
|
||||
{
|
||||
UpdateSelectedValues(this.selectedLoggers, selectedValues);
|
||||
this.DisableInvertFiltersIfNoDropdownFilter();
|
||||
this.RefreshDisplayLines();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@ -185,6 +249,7 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
private Task SelectedSourceDetailsChanged(IEnumerable<string?>? selectedValues)
|
||||
{
|
||||
UpdateSelectedValues(this.selectedSourceDetails, selectedValues);
|
||||
this.DisableInvertFiltersIfNoDropdownFilter();
|
||||
this.RefreshDisplayLines();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@ -200,6 +265,26 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task ToggleAutoRefresh()
|
||||
{
|
||||
await this.AutoRefreshChanged(!this.autoRefresh);
|
||||
}
|
||||
|
||||
private void ToggleFilterOnly()
|
||||
{
|
||||
this.FilterOnly = !this.filterOnly;
|
||||
}
|
||||
|
||||
private void ToggleShowTimestamps()
|
||||
{
|
||||
this.ShowTimestamps = !this.showTimestamps;
|
||||
}
|
||||
|
||||
private void ToggleInvertFilters()
|
||||
{
|
||||
this.InvertFilters = !this.invertFilters;
|
||||
}
|
||||
|
||||
private async Task MaxLinesChanged(int value)
|
||||
{
|
||||
var normalizedValue = Math.Clamp(value, MIN_MAX_LINES, MAX_MAX_LINES);
|
||||
@ -210,6 +295,12 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
await this.RefreshLogAsync();
|
||||
}
|
||||
|
||||
private async Task CopyDisplayedLines()
|
||||
{
|
||||
var text = string.Join(Environment.NewLine, this.displayLines.Select(this.GetPlainRenderedLine));
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, text);
|
||||
}
|
||||
|
||||
private async Task OpenCurrentLogInFileManager()
|
||||
{
|
||||
var path = this.CurrentLogPath;
|
||||
@ -257,12 +348,15 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
});
|
||||
}
|
||||
|
||||
private void ClearFilters()
|
||||
private void ClearFilterText()
|
||||
{
|
||||
this.filterText = string.Empty;
|
||||
this.selectedLogLevels.Clear();
|
||||
this.selectedLoggers.Clear();
|
||||
this.selectedSourceDetails.Clear();
|
||||
this.RefreshDisplayLines();
|
||||
}
|
||||
|
||||
private void ClearExcludeText()
|
||||
{
|
||||
this.excludeText = string.Empty;
|
||||
this.RefreshDisplayLines();
|
||||
}
|
||||
|
||||
@ -360,11 +454,29 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
NormalizeSelectedValues(this.selectedLogLevels, this.logLevelOptions);
|
||||
NormalizeSelectedValues(this.selectedLoggers, this.loggerOptions);
|
||||
NormalizeSelectedValues(this.selectedSourceDetails, this.sourceDetailOptions);
|
||||
this.DisableInvertFiltersIfNoDropdownFilter();
|
||||
}
|
||||
|
||||
private void DisableInvertFiltersIfNoDropdownFilter()
|
||||
{
|
||||
if (!this.HasDropdownFilter)
|
||||
this.invertFilters = false;
|
||||
}
|
||||
|
||||
private static Variant GetToggleVariant(bool isActive)
|
||||
{
|
||||
return isActive ? Variant.Filled : Variant.Outlined;
|
||||
}
|
||||
|
||||
private static Color GetToggleColor(bool isActive)
|
||||
{
|
||||
return isActive ? Color.Primary : Color.Default;
|
||||
}
|
||||
|
||||
private void RefreshDisplayLines()
|
||||
{
|
||||
this.activeSearchTerms = BuildSearchTerms(this.filterText);
|
||||
this.activeExcludeTerms = BuildSearchTerms(this.excludeText);
|
||||
this.displayLines = this.loadedLines
|
||||
.Where(this.LineMatchesFilters)
|
||||
.ToList();
|
||||
@ -372,19 +484,26 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
|
||||
private bool LineMatchesFilters(LogLine line)
|
||||
{
|
||||
if (!MatchesSelection(line.Segments.Level, this.selectedLogLevels))
|
||||
if (!this.LineMatchesDropdownFilters(line))
|
||||
return false;
|
||||
|
||||
if (!MatchesSelection(line.Segments.Logger, this.selectedLoggers))
|
||||
var renderedLine = this.GetPlainRenderedLine(line);
|
||||
if (this.filterOnly && this.activeSearchTerms.Length > 0 && !MatchesSearchTerms(renderedLine, this.activeSearchTerms))
|
||||
return false;
|
||||
|
||||
if (!MatchesSelection(line.Segments.SourceDetails, this.selectedSourceDetails))
|
||||
return false;
|
||||
return this.activeExcludeTerms.Length == 0 || !MatchesSearchTerms(renderedLine, this.activeExcludeTerms);
|
||||
}
|
||||
|
||||
if (!this.filterOnly || this.activeSearchTerms.Length == 0)
|
||||
private bool LineMatchesDropdownFilters(LogLine line)
|
||||
{
|
||||
var matchesDropdownFilters = MatchesSelection(line.Segments.Level, this.selectedLogLevels)
|
||||
&& MatchesSelection(line.Segments.Logger, this.selectedLoggers)
|
||||
&& MatchesSelection(line.Segments.SourceDetails, this.selectedSourceDetails);
|
||||
|
||||
if (!this.HasDropdownFilter)
|
||||
return true;
|
||||
|
||||
return MatchesSearchTerms(this.GetPlainRenderedLine(line), this.activeSearchTerms);
|
||||
return this.invertFilters ? !matchesDropdownFilters : matchesDropdownFilters;
|
||||
}
|
||||
|
||||
private string RenderLine(LogLine line)
|
||||
@ -675,9 +794,14 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
}
|
||||
|
||||
private static bool MatchesSelection(string? value, HashSet<string> selectedValues)
|
||||
{
|
||||
return selectedValues.Count == 0 || ActiveSelectionMatches(value, selectedValues);
|
||||
}
|
||||
|
||||
private static bool ActiveSelectionMatches(string? value, HashSet<string> selectedValues)
|
||||
{
|
||||
if (selectedValues.Count == 0)
|
||||
return true;
|
||||
return false;
|
||||
|
||||
var normalizedValue = string.IsNullOrWhiteSpace(value) ? OTHER_OPTION_VALUE : value;
|
||||
return selectedValues.Contains(normalizedValue);
|
||||
@ -693,10 +817,7 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return [];
|
||||
|
||||
return text
|
||||
.Split(WORD_SPLIT_CHARS, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
return [text.Trim()];
|
||||
}
|
||||
|
||||
private static bool MatchesSearchTerms(string text, string[] terms)
|
||||
@ -767,4 +888,4 @@ public partial class AssistantLogViewer : MSGComponentBase
|
||||
private readonly record struct LogSnapshot(List<LogLine> Lines, int TotalLineCount, int SkippedLineCount);
|
||||
|
||||
private readonly record struct HighlightRange(int Start, int Length);
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,6 +119,7 @@
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
||||
<AssistantBlock TSettings="SettingsDialogCoding" Component="Components.CODING_ASSISTANT" Name="@T("Coding")" Description="@T("Get coding and debugging support from an LLM.")" Icon="@Icons.Material.Filled.Code" Link="@Routes.ASSISTANT_CODING"/>
|
||||
<AssistantBlock TSettings="SettingsDialogERIServer" Component="Components.ERI_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_RAG_2024" Name="@T("ERI Server")" Description="@T("Generate an ERI server to integrate business systems.")" Icon="@Icons.Material.Filled.PrivateConnectivity" Link="@Routes.ASSISTANT_ERI"/>
|
||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.LOG_VIEWER_ASSISTANT" Name="@T("Log Viewer")" Description="@T("View and filter AI Studio log files.")" Icon="@Icons.Material.Filled.Article" Link="@Routes.ASSISTANT_LOG_VIEWER"/>
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
|
||||
@ -1617,6 +1617,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T114706247
|
||||
-- Select a log file
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1231773010"] = "Protokolldatei auswählen"
|
||||
|
||||
-- Show only matching lines
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T131451442"] = "Nur passende Zeilen anzeigen"
|
||||
|
||||
-- Log level
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1318706515"] = "Protokollierungsstufe"
|
||||
|
||||
@ -1647,6 +1650,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T228853842
|
||||
-- Showing {0} of {1} lines. Last refresh: {2}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2378353570"] = "Anzeige von {0} von {1} Zeilen. Letzte Aktualisierung: {2}."
|
||||
|
||||
-- Disable auto-refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T240826492"] = "Automatische Aktualisierung deaktivieren"
|
||||
|
||||
-- Enable auto-refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2467283075"] = "Automatische Aktualisierung aktivieren"
|
||||
|
||||
-- No matching log lines.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2511997530"] = "Keine passenden Protokollzeilen."
|
||||
|
||||
@ -1659,8 +1668,11 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T268681396
|
||||
-- Loaded {0} lines. Last refresh: {1}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2920304709"] = "{0} Zeilen geladen. Letzte Aktualisierung: {1}."
|
||||
|
||||
-- Filter only
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3152625639"] = "Nur filtern"
|
||||
-- Disable inverted dropdown filters
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3035207754"] = "Invertierung der Dropdown-Filter deaktivieren"
|
||||
|
||||
-- Copy view
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3292961985"] = "Ansicht kopieren"
|
||||
|
||||
-- Loading log file...
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481"] = "Lade Protokolldatei..."
|
||||
@ -1668,21 +1680,24 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481
|
||||
-- Unknown error
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3461425987"] = "Unbekannter Fehler"
|
||||
|
||||
-- Exclude
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3551498825"] = "Ausschließen"
|
||||
|
||||
-- The log file path is not available yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3686775689"] = "Der Pfad zur Protokolldatei ist noch nicht verfügbar."
|
||||
|
||||
-- Logger
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T376222229"] = "Logger"
|
||||
|
||||
-- Auto-refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3837203600"] = "Automatisch aktualisieren"
|
||||
|
||||
-- not loaded yet
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3863250749"] = "noch nicht geladen"
|
||||
|
||||
-- Loading...
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T397479987"] = "Wird geladen..."
|
||||
|
||||
-- Select a dropdown filter to invert
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3991737584"] = "Wählen Sie einen Dropdown-Filter zur Invertierung aus"
|
||||
|
||||
-- Usage log
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4031747274"] = "Nutzungsprotokoll"
|
||||
|
||||
@ -1695,11 +1710,17 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T413024177
|
||||
-- Opened the log file location.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4162897654"] = "Der Speicherort der Protokolldatei wurde geöffnet."
|
||||
|
||||
-- Hide timestamps
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4263992002"] = "Zeitstempel ausblenden"
|
||||
|
||||
-- Show timestamps
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133"] = "Zeitstempel anzeigen"
|
||||
|
||||
-- Clear
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T77955010"] = "Löschen"
|
||||
-- Highlight matches without hiding lines
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T661180902"] = "Treffer hervorheben, ohne übrige Zeilen auszublenden"
|
||||
|
||||
-- Invert dropdown filters
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T8477641"] = "Dropdown-Filter invertieren"
|
||||
|
||||
-- You can enter text, attach one or more documents, or use both. At least one input is required.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "Sie können Text eingeben, ein oder mehrere Dokumente anhängen oder beides verwenden. Mindestens eine Eingabe ist erforderlich."
|
||||
|
||||
@ -1617,6 +1617,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T114706247
|
||||
-- Select a log file
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1231773010"] = "Select a log file"
|
||||
|
||||
-- Show only matching lines
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T131451442"] = "Show only matching lines"
|
||||
|
||||
-- Log level
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1318706515"] = "Log level"
|
||||
|
||||
@ -1647,6 +1650,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T228853842
|
||||
-- Showing {0} of {1} lines. Last refresh: {2}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2378353570"] = "Showing {0} of {1} lines. Last refresh: {2}."
|
||||
|
||||
-- Disable auto-refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T240826492"] = "Disable auto-refresh"
|
||||
|
||||
-- Enable auto-refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2467283075"] = "Enable auto-refresh"
|
||||
|
||||
-- No matching log lines.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2511997530"] = "No matching log lines."
|
||||
|
||||
@ -1659,8 +1668,11 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T268681396
|
||||
-- Loaded {0} lines. Last refresh: {1}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2920304709"] = "Loaded {0} lines. Last refresh: {1}."
|
||||
|
||||
-- Filter only
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3152625639"] = "Filter only"
|
||||
-- Disable inverted dropdown filters
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3035207754"] = "Disable inverted dropdown filters"
|
||||
|
||||
-- Copy view
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3292961985"] = "Copy view"
|
||||
|
||||
-- Loading log file...
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481"] = "Loading log file..."
|
||||
@ -1668,21 +1680,24 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481
|
||||
-- Unknown error
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3461425987"] = "Unknown error"
|
||||
|
||||
-- Exclude
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3551498825"] = "Exclude"
|
||||
|
||||
-- The log file path is not available yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3686775689"] = "The log file path is not available yet."
|
||||
|
||||
-- Logger
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T376222229"] = "Logger"
|
||||
|
||||
-- Auto-refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3837203600"] = "Auto-refresh"
|
||||
|
||||
-- not loaded yet
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3863250749"] = "not loaded yet"
|
||||
|
||||
-- Loading...
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T397479987"] = "Loading..."
|
||||
|
||||
-- Select a dropdown filter to invert
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3991737584"] = "Select a dropdown filter to invert"
|
||||
|
||||
-- Usage log
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4031747274"] = "Usage log"
|
||||
|
||||
@ -1695,11 +1710,17 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T413024177
|
||||
-- Opened the log file location.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4162897654"] = "Opened the log file location."
|
||||
|
||||
-- Hide timestamps
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4263992002"] = "Hide timestamps"
|
||||
|
||||
-- Show timestamps
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133"] = "Show timestamps"
|
||||
|
||||
-- Clear
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T77955010"] = "Clear"
|
||||
-- Highlight matches without hiding other lines
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T661180902"] = "Highlight matches without hiding other lines"
|
||||
|
||||
-- Invert dropdown filters
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T8477641"] = "Invert dropdown filters"
|
||||
|
||||
-- You can enter text, attach one or more documents, or use both. At least one input is required.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required."
|
||||
|
||||
@ -7,19 +7,6 @@ use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
|
||||
use crate::api_token::APIToken;
|
||||
use crate::app_window::MAIN_WINDOW;
|
||||
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
/// Microsoft documents CREATE_NO_WINDOW as a process creation flag with value 0x08000000.
|
||||
#[cfg(windows)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct PreviousDirectory {
|
||||
path: String,
|
||||
@ -325,7 +312,7 @@ pub async fn open_path_in_file_manager(
|
||||
|
||||
let Some(target) = resolve_file_manager_target(&requested_path) else {
|
||||
let issue = format!(
|
||||
"The path does not exist and its parent folder could not be found: {}",
|
||||
"The path does not exist or is not a file or folder: {}",
|
||||
requested_path.to_string_lossy(),
|
||||
);
|
||||
error!(Source = "Tauri"; "{issue}");
|
||||
@ -335,51 +322,35 @@ pub async fn open_path_in_file_manager(
|
||||
});
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
return match open_path_in_linux_file_manager(&target).await {
|
||||
Ok(()) => {
|
||||
info!("Opened file manager for path: {:?}", target.path);
|
||||
Json(OpenPathResponse {
|
||||
success: true,
|
||||
issue: String::new(),
|
||||
})
|
||||
}
|
||||
info!(
|
||||
Source = "Tauri";
|
||||
"Opening resolved file manager target: requested='{}', target='{}', reveal_file={}.",
|
||||
requested_path.display(),
|
||||
target.path.display(),
|
||||
target.reveal_file,
|
||||
);
|
||||
|
||||
Err(issue) => {
|
||||
error!(Source = "Tauri"; "{issue}");
|
||||
Json(OpenPathResponse {
|
||||
success: false,
|
||||
issue,
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
match open_file_manager_target(&target).await {
|
||||
Ok(()) => {
|
||||
info!(
|
||||
Source = "Tauri";
|
||||
"Opened file manager target: requested='{}', target='{}', reveal_file={}.",
|
||||
requested_path.display(),
|
||||
target.path.display(),
|
||||
target.reveal_file,
|
||||
);
|
||||
Json(OpenPathResponse {
|
||||
success: true,
|
||||
issue: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
let mut command = create_file_manager_command(&target);
|
||||
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
|
||||
match command.spawn() {
|
||||
Ok(_) => {
|
||||
info!("Opened file manager for path: {:?}", target.path);
|
||||
Json(OpenPathResponse {
|
||||
success: true,
|
||||
issue: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
let issue = format!("Failed to open the file manager: {error}");
|
||||
error!(Source = "Tauri"; "{issue}");
|
||||
Json(OpenPathResponse {
|
||||
success: false,
|
||||
issue,
|
||||
})
|
||||
}
|
||||
Err(issue) => {
|
||||
error!(Source = "Tauri"; "{issue}");
|
||||
Json(OpenPathResponse {
|
||||
success: false,
|
||||
issue,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -402,126 +373,142 @@ struct FileManagerTarget {
|
||||
reveal_file: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum LinuxPortalOperation {
|
||||
RevealFile,
|
||||
OpenDirectory,
|
||||
}
|
||||
|
||||
fn resolve_file_manager_target(requested_path: &Path) -> Option<FileManagerTarget> {
|
||||
if requested_path.is_file() {
|
||||
let metadata = requested_path.metadata().ok()?;
|
||||
|
||||
if metadata.is_file() {
|
||||
return Some(FileManagerTarget {
|
||||
path: requested_path.to_path_buf(),
|
||||
reveal_file: true,
|
||||
});
|
||||
}
|
||||
|
||||
if requested_path.is_dir() {
|
||||
if metadata.is_dir() {
|
||||
return Some(FileManagerTarget {
|
||||
path: requested_path.to_path_buf(),
|
||||
reveal_file: false,
|
||||
});
|
||||
}
|
||||
|
||||
requested_path.parent()
|
||||
.filter(|parent| parent.is_dir())
|
||||
.map(|parent| FileManagerTarget {
|
||||
path: parent.to_path_buf(),
|
||||
reveal_file: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn linux_portal_operation(target: &FileManagerTarget) -> LinuxPortalOperation {
|
||||
if target.reveal_file {
|
||||
LinuxPortalOperation::RevealFile
|
||||
} else {
|
||||
LinuxPortalOperation::OpenDirectory
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn xdg_open_fallback_path(target: &FileManagerTarget) -> &Path {
|
||||
if target.reveal_file {
|
||||
target.path.parent().unwrap_or(&target.path)
|
||||
} else {
|
||||
&target.path
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
enum LinuxPortalError {
|
||||
Unavailable(String),
|
||||
RequestFailed(String),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn open_path_with_linux_portal(target: &FileManagerTarget) -> Result<(), LinuxPortalError> {
|
||||
let file = std::fs::File::open(&target.path)
|
||||
.map_err(|error| LinuxPortalError::Unavailable(format!("Failed to open the path for the desktop portal: {error}")))?;
|
||||
|
||||
let request = match linux_portal_operation(target) {
|
||||
LinuxPortalOperation::RevealFile => OpenDirectoryRequest::default().send(&file).await,
|
||||
LinuxPortalOperation::OpenDirectory => OpenFileRequest::default().send_file(&file).await,
|
||||
}
|
||||
.map_err(|error| LinuxPortalError::Unavailable(format!("Desktop portal invocation failed: {error}")))?;
|
||||
|
||||
request.response()
|
||||
.map_err(|error| LinuxPortalError::RequestFailed(format!("Desktop portal request failed: {error}")))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn open_path_with_xdg_open(target: &FileManagerTarget) -> Result<(), String> {
|
||||
let fallback_path = xdg_open_fallback_path(target);
|
||||
let status = tokio::process::Command::new("xdg-open")
|
||||
.arg(fallback_path)
|
||||
.status()
|
||||
.await
|
||||
.map_err(|error| format!("xdg-open failed to start for '{}': {error}", fallback_path.to_string_lossy()))?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("xdg-open failed for '{}' with exit status {status}", fallback_path.to_string_lossy()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn open_path_in_linux_file_manager(target: &FileManagerTarget) -> Result<(), String> {
|
||||
match open_path_with_linux_portal(target).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(LinuxPortalError::RequestFailed(error)) => Err(error),
|
||||
Err(LinuxPortalError::Unavailable(portal_error)) => {
|
||||
match open_path_with_xdg_open(target).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(fallback_error) => Err(format!("{portal_error} Fallback failed: {fallback_error}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn create_file_manager_command(target: &FileManagerTarget) -> Command {
|
||||
let mut command = Command::new("explorer.exe");
|
||||
async fn open_file_manager_target(target: &FileManagerTarget) -> Result<(), String> {
|
||||
if target.reveal_file {
|
||||
command.arg(format!("/select,{}", target.path.to_string_lossy()));
|
||||
} else {
|
||||
command.arg(&target.path);
|
||||
return tauri_plugin_opener::reveal_item_in_dir(&target.path)
|
||||
.map_err(|error| format!("Failed to reveal '{}' in the file manager: {error}", target.path.display()));
|
||||
}
|
||||
|
||||
command
|
||||
tauri_plugin_opener::open_path(&target.path, None::<&str>)
|
||||
.map_err(|error| format!("Failed to open '{}' in the file manager: {error}", target.path.display()))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn create_file_manager_command(target: &FileManagerTarget) -> Command {
|
||||
let mut command = Command::new("open");
|
||||
async fn open_file_manager_target(target: &FileManagerTarget) -> Result<(), String> {
|
||||
let mut command = tokio::process::Command::new("open");
|
||||
if target.reveal_file {
|
||||
command.arg("-R");
|
||||
}
|
||||
|
||||
command.arg(&target.path);
|
||||
run_file_manager_command(command, &format!("macOS open for '{}'", target.path.display())).await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn open_file_manager_target(target: &FileManagerTarget) -> Result<(), String> {
|
||||
if target.reveal_file {
|
||||
return match reveal_linux_file(&target.path).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(reveal_error) => {
|
||||
let parent_directory = target.path.parent().ok_or_else(|| {
|
||||
format!("Failed to determine the parent folder for '{}'.", target.path.display())
|
||||
})?;
|
||||
info!(
|
||||
Source = "Tauri";
|
||||
"Linux file reveal failed for '{}': {} Opening parent directory '{}'.",
|
||||
target.path.display(),
|
||||
reveal_error,
|
||||
parent_directory.display(),
|
||||
);
|
||||
open_linux_directory(parent_directory).await.map_err(|fallback_error| {
|
||||
format!(
|
||||
"Failed to reveal '{}' in the file manager: {} Fallback failed: {}",
|
||||
target.path.display(),
|
||||
reveal_error,
|
||||
fallback_error,
|
||||
)
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
open_linux_directory(&target.path).await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn reveal_linux_file(path: &Path) -> Result<(), String> {
|
||||
let mut command = tokio::process::Command::new("dbus-send");
|
||||
command
|
||||
.arg("--session")
|
||||
.arg("--dest=org.freedesktop.FileManager1")
|
||||
.arg("--type=method_call")
|
||||
.arg("/org/freedesktop/FileManager1")
|
||||
.arg("org.freedesktop.FileManager1.ShowItems")
|
||||
.arg(format!("array:string:{}", path_to_file_uri(path)))
|
||||
.arg("string:");
|
||||
|
||||
run_file_manager_command(command, &format!("Linux FileManager1 ShowItems for '{}'", path.display())).await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn open_linux_directory(path: &Path) -> Result<(), String> {
|
||||
let mut command = tokio::process::Command::new("xdg-open");
|
||||
command.arg(path);
|
||||
run_file_manager_command(command, &format!("Linux xdg-open for '{}'", path.display())).await
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn path_to_file_uri(path: &Path) -> String {
|
||||
let path = path.to_string_lossy().replace('\\', "/");
|
||||
let mut uri = String::from("file://");
|
||||
if !path.starts_with('/') {
|
||||
uri.push('/');
|
||||
}
|
||||
|
||||
for byte in path.as_bytes() {
|
||||
match *byte {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'/' | b'-' | b'_' | b'.' | b'~' => uri.push(*byte as char),
|
||||
_ => uri.push_str(&format!("%{:02X}", *byte)),
|
||||
}
|
||||
}
|
||||
|
||||
uri
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
async fn run_file_manager_command(
|
||||
mut command: tokio::process::Command,
|
||||
description: &str,
|
||||
) -> Result<(), String> {
|
||||
let status = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
command.status(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| format!("{description} timed out."))?
|
||||
.map_err(|error| format!("{description} failed to start: {error}"))?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("{description} failed with exit status {status}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
||||
async fn open_file_manager_target(target: &FileManagerTarget) -> Result<(), String> {
|
||||
Err(format!("Opening '{}' in the file manager is not supported on this platform.", target.path.display()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -530,7 +517,7 @@ mod tests {
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn existing_file_is_revealed_and_falls_back_to_its_parent() {
|
||||
fn existing_file_is_revealed() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("application.log");
|
||||
fs::write(&file_path, "log").unwrap();
|
||||
@ -539,8 +526,6 @@ mod tests {
|
||||
|
||||
assert_eq!(target.path, file_path);
|
||||
assert!(target.reveal_file);
|
||||
assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::RevealFile);
|
||||
assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -551,21 +536,14 @@ mod tests {
|
||||
|
||||
assert_eq!(target.path, temp_dir.path());
|
||||
assert!(!target.reveal_file);
|
||||
assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::OpenDirectory);
|
||||
assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_uses_its_existing_parent_directory() {
|
||||
fn missing_file_with_existing_parent_is_rejected() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let missing_file = temp_dir.path().join("missing.log");
|
||||
|
||||
let target = resolve_file_manager_target(&missing_file).unwrap();
|
||||
|
||||
assert_eq!(target.path, temp_dir.path());
|
||||
assert!(!target.reveal_file);
|
||||
assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::OpenDirectory);
|
||||
assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
|
||||
assert!(resolve_file_manager_target(&missing_file).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -575,4 +553,14 @@ mod tests {
|
||||
|
||||
assert!(resolve_file_manager_target(&invalid_path).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_file_uri_encodes_spaces_in_log_paths() {
|
||||
let path = PathBuf::from("/home/koud_pa/.local/share/org.mindworkai.AIStudio/data/.AI Studio Events.log");
|
||||
|
||||
assert_eq!(
|
||||
path_to_file_uri(&path),
|
||||
"file:///home/koud_pa/.local/share/org.mindworkai.AIStudio/data/.AI%20Studio%20Events.log",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ pub fn init_logging(bundle_identifier: &str) {
|
||||
.suffix("log");
|
||||
|
||||
// Store the startup log path:
|
||||
store_startup_log_path(&LOG_STARTUP_PATH, &log_path);
|
||||
let startup_log_path = store_startup_log_path(&LOG_STARTUP_PATH, &log_path);
|
||||
|
||||
let runtime_logger = Logger::try_with_str(log_config).expect("Cannot create logging")
|
||||
.log_to_file(log_path)
|
||||
@ -83,13 +83,17 @@ pub fn init_logging(bundle_identifier: &str) {
|
||||
|
||||
LOGGER.set(runtime_logger).expect("Cannot set LOGGER");
|
||||
|
||||
log::info!(Source = "Tauri"; "Startup log file path: {startup_log_path}");
|
||||
|
||||
if let Some(fallback_warning) = fallback_warning {
|
||||
log::warn!("{fallback_warning}");
|
||||
}
|
||||
}
|
||||
|
||||
fn store_startup_log_path(storage: &OnceLock<String>, log_path: &FileSpec) {
|
||||
let _ = storage.set(convert_log_path_to_string(log_path));
|
||||
fn store_startup_log_path(storage: &OnceLock<String>, log_path: &FileSpec) -> String {
|
||||
let log_path = convert_log_path_to_string(log_path);
|
||||
let _ = storage.set(log_path.clone());
|
||||
log_path
|
||||
}
|
||||
|
||||
fn convert_log_path_to_string(log_path: &FileSpec) -> String {
|
||||
@ -129,11 +133,14 @@ fn get_startup_log_path(bundle_identifier: &str) -> (PathBuf, Option<String>) {
|
||||
).unwrap_or_else(|error| panic!("Cannot prepare a Flatpak startup log directory: {error}"));
|
||||
}
|
||||
|
||||
(get_non_flatpak_startup_log_path(
|
||||
get_non_flatpak_startup_log_path(
|
||||
bundle_identifier,
|
||||
dirs::data_local_dir(),
|
||||
home_directory(),
|
||||
current_dir().ok(),
|
||||
temp_dir(),
|
||||
), None)
|
||||
ensure_log_directory_is_writable,
|
||||
).unwrap_or_else(|error| panic!("Cannot prepare a startup log directory: {error}"))
|
||||
}
|
||||
|
||||
// Note: Rust plans to remove the deprecation flag for std::env::home_dir() in Rust 1.86.0.
|
||||
@ -142,25 +149,35 @@ fn home_directory() -> Option<PathBuf> {
|
||||
std::env::home_dir()
|
||||
}
|
||||
|
||||
fn get_non_flatpak_startup_log_path(
|
||||
fn get_non_flatpak_startup_log_path<F>(
|
||||
bundle_identifier: &str,
|
||||
data_local_directory: Option<PathBuf>,
|
||||
home_directory: Option<PathBuf>,
|
||||
working_directory: Option<PathBuf>,
|
||||
temporary_directory: PathBuf,
|
||||
) -> PathBuf {
|
||||
match home_directory {
|
||||
// Case: We could determine the home directory:
|
||||
Some(home_directory) => home_directory,
|
||||
|
||||
// Case: We could not determine the home directory. Let's try to use the working directory:
|
||||
None => match working_directory {
|
||||
|
||||
// Case: We could determine the working directory:
|
||||
Some(working_directory) => working_directory,
|
||||
|
||||
// Case: We could not determine the working directory. Let's use the temporary directory:
|
||||
None => temporary_directory,
|
||||
},
|
||||
mut ensure_writable: F,
|
||||
) -> Result<(PathBuf, Option<String>), String>
|
||||
where
|
||||
F: FnMut(&Path) -> Result<(), String>,
|
||||
{
|
||||
let standard_directory = data_local_directory.map(|directory| directory.join(bundle_identifier).join("data"));
|
||||
let mut fallback_candidates = Vec::new();
|
||||
if let Some(home_directory) = home_directory {
|
||||
fallback_candidates.push(("home directory", home_directory));
|
||||
}
|
||||
|
||||
if let Some(working_directory) = working_directory {
|
||||
fallback_candidates.push(("working directory", working_directory));
|
||||
}
|
||||
|
||||
fallback_candidates.push(("temporary directory", temporary_directory));
|
||||
|
||||
select_startup_log_path(
|
||||
"standard path",
|
||||
standard_directory,
|
||||
fallback_candidates,
|
||||
&mut ensure_writable,
|
||||
)
|
||||
}
|
||||
|
||||
fn select_flatpak_startup_log_path<F>(
|
||||
@ -174,49 +191,56 @@ where
|
||||
F: FnMut(&Path) -> Result<(), String>,
|
||||
{
|
||||
let standard_directory = data_local_directory.map(|directory| directory.join(bundle_identifier).join("data"));
|
||||
let persistent_fallback = persistent_data_directory.join(bundle_identifier).join("data");
|
||||
let temporary_fallback = temporary_directory.join(bundle_identifier).join("data");
|
||||
let fallback_candidates = vec![
|
||||
("persistent fallback", persistent_data_directory.join(bundle_identifier).join("data")),
|
||||
("temporary fallback", temporary_directory.join(bundle_identifier).join("data")),
|
||||
];
|
||||
|
||||
select_startup_log_path(
|
||||
"standard Flatpak path",
|
||||
standard_directory,
|
||||
fallback_candidates,
|
||||
&mut ensure_writable,
|
||||
)
|
||||
}
|
||||
|
||||
fn select_startup_log_path<F>(
|
||||
standard_name: &str,
|
||||
standard_directory: Option<PathBuf>,
|
||||
fallback_candidates: Vec<(&'static str, PathBuf)>,
|
||||
ensure_writable: &mut F,
|
||||
) -> Result<(PathBuf, Option<String>), String>
|
||||
where
|
||||
F: FnMut(&Path) -> Result<(), String>,
|
||||
{
|
||||
let mut failures = Vec::new();
|
||||
|
||||
if let Some(standard_directory) = standard_directory {
|
||||
match ensure_writable(&standard_directory) {
|
||||
Ok(()) => return Ok((standard_directory, None)),
|
||||
Err(error) => failures.push(format!("standard path failed: {error}")),
|
||||
Err(error) => failures.push(format!("{standard_name} failed: {error}")),
|
||||
}
|
||||
} else {
|
||||
failures.push(String::from("standard path failed: dirs::data_local_dir() returned no path"));
|
||||
failures.push(format!("{standard_name} failed: dirs::data_local_dir() returned no path"));
|
||||
}
|
||||
|
||||
match ensure_writable(&persistent_fallback) {
|
||||
Ok(()) => {
|
||||
let warning = format!(
|
||||
"The standard Flatpak startup log directory was unavailable; using persistent fallback '{}'. {}",
|
||||
persistent_fallback.display(),
|
||||
failures.join("; "),
|
||||
);
|
||||
|
||||
return Ok((persistent_fallback, Some(warning)));
|
||||
},
|
||||
|
||||
Err(error) => failures.push(format!("persistent fallback failed: {error}")),
|
||||
for (fallback_name, fallback_directory) in fallback_candidates {
|
||||
match ensure_writable(&fallback_directory) {
|
||||
Ok(()) => {
|
||||
let warning = format!(
|
||||
"The standard startup log directory was unavailable; using {fallback_name} '{}'. {}",
|
||||
fallback_directory.display(),
|
||||
failures.join("; "),
|
||||
);
|
||||
|
||||
return Ok((fallback_directory, Some(warning)));
|
||||
}
|
||||
|
||||
Err(error) => failures.push(format!("{fallback_name} failed: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
match ensure_writable(&temporary_fallback) {
|
||||
Ok(()) => {
|
||||
let warning = format!(
|
||||
"The standard and persistent Flatpak startup log directories were unavailable; using temporary fallback '{}'. {}",
|
||||
temporary_fallback.display(),
|
||||
failures.join("; "),
|
||||
);
|
||||
|
||||
Ok((temporary_fallback, Some(warning)))
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
failures.push(format!("temporary fallback failed: {error}"));
|
||||
Err(failures.join("; "))
|
||||
},
|
||||
}
|
||||
Err(failures.join("; "))
|
||||
}
|
||||
|
||||
fn ensure_log_directory_is_writable(directory: &Path) -> Result<(), String> {
|
||||
@ -242,8 +266,10 @@ pub fn switch_to_file_logging(logger_path: PathBuf) -> Result<(), Box<dyn Error>
|
||||
.basename("events")
|
||||
.suppress_timestamp()
|
||||
.suffix("log");
|
||||
let _ = LOG_APP_PATH.set(convert_log_path_to_string(&log_path));
|
||||
let app_log_path = convert_log_path_to_string(&log_path);
|
||||
let _ = LOG_APP_PATH.set(app_log_path.clone());
|
||||
LOGGER.get().expect("No LOGGER was set").handle.reset_flw(&FileLogWriter::builder(log_path))?;
|
||||
log::info!(Source = "Tauri"; "Usage log file path: {app_log_path}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -338,9 +364,18 @@ fn file_logger_format(
|
||||
}
|
||||
|
||||
pub async fn get_log_paths(_token: APIToken) -> Json<LogPathsResponse> {
|
||||
let log_startup_path = LOG_STARTUP_PATH.get().expect("No startup log path was set").clone();
|
||||
let log_app_path = LOG_APP_PATH.get().expect("No app log path was set").clone();
|
||||
log::info!(
|
||||
Source = "Tauri";
|
||||
"Returning log file paths: startup='{}', usage='{}'.",
|
||||
log_startup_path,
|
||||
log_app_path,
|
||||
);
|
||||
|
||||
Json(LogPathsResponse {
|
||||
log_startup_path: LOG_STARTUP_PATH.get().expect("No startup log path was set").clone(),
|
||||
log_app_path: LOG_APP_PATH.get().expect("No app log path was set").clone(),
|
||||
log_startup_path,
|
||||
log_app_path,
|
||||
})
|
||||
}
|
||||
|
||||
@ -497,23 +532,51 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_flatpak_path_selection_keeps_existing_fallback_order() {
|
||||
fn non_flatpak_path_selection_prefers_local_app_data_path() {
|
||||
let local_data = PathBuf::from("/local-data");
|
||||
let expected = local_data.join(BUNDLE_IDENTIFIER).join("data");
|
||||
let home = PathBuf::from("/home/user");
|
||||
let working = PathBuf::from("/working");
|
||||
let temporary = PathBuf::from("/tmp");
|
||||
|
||||
assert_eq!(
|
||||
get_non_flatpak_startup_log_path(Some(home.clone()), Some(working.clone()), temporary.clone()),
|
||||
home,
|
||||
);
|
||||
assert_eq!(
|
||||
get_non_flatpak_startup_log_path(None, Some(working.clone()), temporary.clone()),
|
||||
working,
|
||||
);
|
||||
assert_eq!(
|
||||
get_non_flatpak_startup_log_path(None, None, temporary.clone()),
|
||||
let (selected, warning) = get_non_flatpak_startup_log_path(
|
||||
BUNDLE_IDENTIFIER,
|
||||
Some(local_data),
|
||||
Some(home),
|
||||
Some(working),
|
||||
temporary,
|
||||
);
|
||||
|_| Ok(()),
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(selected, expected);
|
||||
assert!(warning.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_flatpak_path_selection_falls_back_to_home_when_local_app_data_is_unwritable() {
|
||||
let local_data = PathBuf::from("/local-data");
|
||||
let standard = local_data.join(BUNDLE_IDENTIFIER).join("data");
|
||||
let home = PathBuf::from("/home/user");
|
||||
let working = PathBuf::from("/working");
|
||||
let temporary = PathBuf::from("/tmp");
|
||||
|
||||
let (selected, warning) = get_non_flatpak_startup_log_path(
|
||||
BUNDLE_IDENTIFIER,
|
||||
Some(local_data),
|
||||
Some(home.clone()),
|
||||
Some(working),
|
||||
temporary,
|
||||
|candidate| {
|
||||
if candidate == standard {
|
||||
Err(String::from("read-only"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(selected, home);
|
||||
assert!(warning.unwrap().contains("home directory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -533,7 +596,7 @@ mod tests {
|
||||
},
|
||||
).unwrap();
|
||||
let log_path = FileSpec::default()
|
||||
.directory(selected)
|
||||
.directory(selected.clone())
|
||||
.basename(".AI Studio Events")
|
||||
.suppress_timestamp()
|
||||
.suffix("log");
|
||||
@ -542,8 +605,8 @@ mod tests {
|
||||
store_startup_log_path(&storage, &log_path);
|
||||
|
||||
assert_eq!(
|
||||
storage.get().unwrap(),
|
||||
"/tmp/org.mindworkai.AIStudio/data/.AI Studio Events.log",
|
||||
PathBuf::from(storage.get().unwrap()),
|
||||
absolute(selected.join(".AI Studio Events.log")).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user