Rename project from "Ormentia Markus" to "Ormentia Markle" across all files, including license, application settings, UI elements, and deployment scripts. Add find panel functionality in the Markdown editor.

This commit is contained in:
shump
2026-02-08 16:19:04 -06:00
parent 0869724acd
commit a615a97662
25 changed files with 851 additions and 83 deletions

View File

@@ -634,7 +634,7 @@ state the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found. "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.> <one line to give the program's name and a brief idea of what it does.>
Copyright (C) 2024 Ormentia Markus Contributors Copyright (C) 2024 Ormentia Markle Contributors
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -654,7 +654,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode: notice like this when it starts in an interactive mode:
Ormentia Markus Copyright (C) 2024 Ormentia Markus Contributors Ormentia Markle Copyright (C) 2024 Ormentia Markle Contributors
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details. under certain conditions; type `show c' for details.

View File

@@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MarkdownEditor.App" x:Class="MarkdownEditor.App"
xmlns:local="using:MarkdownEditor" xmlns:local="using:MarkdownEditor"
Name="Ormentia Markus" Name="Ormentia Markle"
RequestedThemeVariant="Default"> RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->

View File

@@ -34,9 +34,9 @@ public partial class App : Application
var appMenu = NativeMenu.GetMenu(this); var appMenu = NativeMenu.GetMenu(this);
if (appMenu != null) if (appMenu != null)
{ {
// About Markus // About Markle
var aboutItem = new NativeMenuItem { Header = "About Markus" }; var aboutItem = new NativeMenuItem { Header = "About Markle" };
aboutItem.Click += AboutMarkus_Click; aboutItem.Click += AboutMarkle_Click;
appMenu.Items.Add(aboutItem); appMenu.Items.Add(aboutItem);
// Separator // Separator
@@ -48,10 +48,10 @@ public partial class App : Application
// Separator // Separator
appMenu.Items.Add(new NativeMenuItemSeparator()); appMenu.Items.Add(new NativeMenuItemSeparator());
// Hide Ormentia Markus // Hide Ormentia Markle
appMenu.Items.Add(new NativeMenuItem appMenu.Items.Add(new NativeMenuItem
{ {
Header = "Hide Ormentia Markus", Header = "Hide Ormentia Markle",
Gesture = KeyGesture.Parse("Cmd+H") Gesture = KeyGesture.Parse("Cmd+H")
}); });
@@ -71,7 +71,7 @@ public partial class App : Application
// Quit // Quit
appMenu.Items.Add(new NativeMenuItem appMenu.Items.Add(new NativeMenuItem
{ {
Header = "Quit Ormentia Markus", Header = "Quit Ormentia Markle",
Gesture = KeyGesture.Parse("Cmd+Q") Gesture = KeyGesture.Parse("Cmd+Q")
}); });
} }
@@ -125,9 +125,9 @@ public partial class App : Application
} }
/// <summary> /// <summary>
/// Handles the About Markus menu item click event. /// Handles the About Markle menu item click event.
/// </summary> /// </summary>
private void AboutMarkus_Click(object? sender, EventArgs e) private void AboutMarkle_Click(object? sender, EventArgs e)
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop &&
desktop.MainWindow != null) desktop.MainWindow != null)

View File

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 447 KiB

View File

@@ -76,7 +76,7 @@ public static class AppConstants
public static class Messages public static class Messages
{ {
public const string DefaultTabTitle = "Untitled"; public const string DefaultTabTitle = "Untitled";
public const string DefaultEditorContent = "# Welcome to Markdown Editor\n\nStart editing..."; public const string DefaultEditorContent = "# Welcome to Markle\n\nStart editing...";
public const string ErrorRenderingMarkdown = "Error rendering markdown"; public const string ErrorRenderingMarkdown = "Error rendering markdown";
public const string OpenFileDialogTitle = "Open Markdown File"; public const string OpenFileDialogTitle = "Open Markdown File";
public const string SaveFileDialogTitle = "Save Markdown File"; public const string SaveFileDialogTitle = "Save Markdown File";

View File

@@ -3,11 +3,11 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Ormentia Markus</string> <string>Ormentia Markle</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Ormentia Markus</string> <string>Ormentia Markle</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.ormentia.markus</string> <string>com.ormentia.markle</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0.0</string> <string>1.0.0</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
@@ -15,7 +15,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>OrmentiaMarkus</string> <string>OrmentiaMarkle</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
@@ -25,7 +25,7 @@
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© 2025 Ormentia. All rights reserved.</string> <string>© 2025 Ormentia. All rights reserved.</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Markus 1.0.0 - A Simple Markdown Editor</string> <string>Markle 1.0.0 - A Simple Markdown Editor</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>AppIcon</string> <string>AppIcon</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>

View File

@@ -6,13 +6,13 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationTitle>Ormentia Markus</ApplicationTitle> <ApplicationTitle>Ormentia Markle</ApplicationTitle>
<AssemblyName>OrmentiaMarkus</AssemblyName> <AssemblyName>OrmentiaMarkle</AssemblyName>
<Product>Ormentia Markus</Product> <Product>Ormentia Markle</Product>
<ApplicationIcon>Assets\AppIcon.ico</ApplicationIcon> <ApplicationIcon>Assets\AppIcon.ico</ApplicationIcon>
<CFBundleName>Ormentia Markus</CFBundleName> <CFBundleName>Ormentia Markle</CFBundleName>
<CFBundleDisplayName>Ormentia Markus</CFBundleDisplayName> <CFBundleDisplayName>Ormentia Markle</CFBundleDisplayName>
<CFBundleIdentifier>com.ormentia.markus</CFBundleIdentifier> <CFBundleIdentifier>com.ormentia.markle</CFBundleIdentifier>
<CFBundleVersion>1.0.0</CFBundleVersion> <CFBundleVersion>1.0.0</CFBundleVersion>
<CFBundleShortVersionString>1.0.0</CFBundleShortVersionString> <CFBundleShortVersionString>1.0.0</CFBundleShortVersionString>
</PropertyGroup> </PropertyGroup>

View File

@@ -33,7 +33,7 @@ public class SessionService : ISessionService
/// <returns>The path to the app data directory.</returns> /// <returns>The path to the app data directory.</returns>
private static string GetAppDataDirectory() private static string GetAppDataDirectory()
{ {
var appName = "OrmentiaMarkus"; var appName = "OrmentiaMarkle";
// Use LocalApplicationData for cross-platform compatibility // Use LocalApplicationData for cross-platform compatibility
// On Windows: %LOCALAPPDATA% // On Windows: %LOCALAPPDATA%

View File

@@ -83,6 +83,15 @@ public partial class MainWindowViewModel : ViewModelBase
SelectedTab = newTab; SelectedTab = newTab;
} }
/// <summary>
/// Shows the find panel for the currently selected tab.
/// </summary>
[RelayCommand]
private void ShowFindPanel()
{
SelectedTab?.ShowFindPanelCommand.Execute(null);
}
/// <summary> /// <summary>
/// Closes the specified tab. /// Closes the specified tab.
/// Maintains at least one open tab and updates selection appropriately. /// Maintains at least one open tab and updates selection appropriately.

View File

@@ -31,6 +31,23 @@ public partial class MarkdownTabViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private bool _isDirty = false; private bool _isDirty = false;
[ObservableProperty]
private bool _isFindPanelVisible = false;
[ObservableProperty]
private string _searchText = string.Empty;
[ObservableProperty]
private string _searchMatchInfo = string.Empty;
private int _currentMatchIndex = -1;
private System.Collections.Generic.List<int> _searchMatches = new();
/// <summary>
/// Event raised when search should be performed in the view.
/// </summary>
public event EventHandler<SearchEventArgs>? SearchRequested;
/// <summary> /// <summary>
/// Gets the text selection used for formatting operations. /// Gets the text selection used for formatting operations.
/// </summary> /// </summary>
@@ -117,6 +134,22 @@ public partial class MarkdownTabViewModel : ViewModelBase
OnPropertyChanged(nameof(DisplayTitle)); OnPropertyChanged(nameof(DisplayTitle));
} }
partial void OnSearchTextChanged(string value)
{
PerformSearch();
}
partial void OnIsPrettyViewChanged(bool value)
{
// Reset search when switching views
if (_currentMatchIndex >= 0)
{
_currentMatchIndex = -1;
_searchMatches.Clear();
UpdateSearchMatchInfo();
}
}
/// <summary> /// <summary>
/// Loads a markdown file from disk. /// Loads a markdown file from disk.
/// </summary> /// </summary>
@@ -293,4 +326,189 @@ public partial class MarkdownTabViewModel : ViewModelBase
get => Selection.End; get => Selection.End;
set => Selection.End = value; set => Selection.End = value;
} }
// Find/Search commands
[RelayCommand]
private void ShowFindPanel()
{
IsFindPanelVisible = true;
if (string.IsNullOrEmpty(SearchText) && Selection.Length > 0)
{
// If there's selected text, use it as the search term
SearchText = _document.GetText(Selection.Start, Selection.Length);
}
}
[RelayCommand]
private void CloseFindPanel()
{
IsFindPanelVisible = false;
SearchText = string.Empty;
_currentMatchIndex = -1;
_searchMatches.Clear();
UpdateSearchMatchInfo();
SearchRequested?.Invoke(this, new SearchEventArgs { ClearHighlight = true });
}
[RelayCommand]
private void FindNext()
{
if (string.IsNullOrWhiteSpace(SearchText))
{
PerformSearch();
return;
}
if (_searchMatches.Count == 0)
{
PerformSearch();
}
if (_searchMatches.Count > 0)
{
_currentMatchIndex = (_currentMatchIndex + 1) % _searchMatches.Count;
NavigateToMatch(_currentMatchIndex);
}
}
[RelayCommand]
private void FindPrevious()
{
if (string.IsNullOrWhiteSpace(SearchText))
{
PerformSearch();
return;
}
if (_searchMatches.Count == 0)
{
PerformSearch();
}
if (_searchMatches.Count > 0)
{
_currentMatchIndex = (_currentMatchIndex - 1 + _searchMatches.Count) % _searchMatches.Count;
NavigateToMatch(_currentMatchIndex);
}
}
private void PerformSearch()
{
if (string.IsNullOrWhiteSpace(SearchText))
{
_searchMatches.Clear();
_currentMatchIndex = -1;
UpdateSearchMatchInfo();
SearchRequested?.Invoke(this, new SearchEventArgs { ClearHighlight = true });
return;
}
var searchText = SearchText;
var content = IsPrettyView ? SafeMarkdownContent : _document.Text;
_searchMatches.Clear();
_currentMatchIndex = -1;
if (string.IsNullOrEmpty(content))
{
UpdateSearchMatchInfo();
return;
}
// Case-insensitive search
var comparison = StringComparison.OrdinalIgnoreCase;
int index = 0;
while ((index = content.IndexOf(searchText, index, comparison)) != -1)
{
_searchMatches.Add(index);
index += searchText.Length;
}
UpdateSearchMatchInfo();
if (_searchMatches.Count > 0)
{
_currentMatchIndex = 0;
NavigateToMatch(0);
}
else
{
SearchRequested?.Invoke(this, new SearchEventArgs
{
SearchText = searchText,
Matches = _searchMatches,
CurrentMatchIndex = -1
});
}
}
private void NavigateToMatch(int matchIndex)
{
if (matchIndex < 0 || matchIndex >= _searchMatches.Count)
return;
var matchPosition = _searchMatches[matchIndex];
var searchText = SearchText;
if (IsPrettyView)
{
// For pretty view, notify the view to highlight the match
SearchRequested?.Invoke(this, new SearchEventArgs
{
SearchText = searchText,
Matches = _searchMatches,
CurrentMatchIndex = matchIndex,
MatchPosition = matchPosition
});
}
else
{
// For raw view, set selection in the document
Selection.Start = matchPosition;
Selection.End = matchPosition + searchText.Length;
// Notify the view to scroll to selection
SearchRequested?.Invoke(this, new SearchEventArgs
{
SearchText = searchText,
Matches = _searchMatches,
CurrentMatchIndex = matchIndex,
MatchPosition = matchPosition,
ScrollToSelection = true
});
}
UpdateSearchMatchInfo();
}
private void UpdateSearchMatchInfo()
{
if (string.IsNullOrWhiteSpace(SearchText))
{
SearchMatchInfo = string.Empty;
}
else if (_searchMatches.Count == 0)
{
SearchMatchInfo = "No results";
}
else
{
SearchMatchInfo = $"{_currentMatchIndex + 1} of {_searchMatches.Count}";
}
}
}
/// <summary>
/// Event arguments for search operations.
/// </summary>
public class SearchEventArgs : EventArgs
{
public string SearchText { get; set; } = string.Empty;
public System.Collections.Generic.List<int> Matches { get; set; } = new();
public int CurrentMatchIndex { get; set; } = -1;
public int MatchPosition { get; set; } = -1;
public bool ScrollToSelection { get; set; } = false;
public bool ClearHighlight { get; set; } = false;
} }

View File

@@ -4,7 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="500" mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="500"
x:Class="MarkdownEditor.Views.AboutWindow" x:Class="MarkdownEditor.Views.AboutWindow"
Title="About Markus" Title="About Markle"
Width="600" Height="500" Width="600" Height="500"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
CanResize="False" CanResize="False"
@@ -17,12 +17,12 @@
Margin="0,40,0,40"> Margin="0,40,0,40">
<!-- Logo --> <!-- Logo -->
<Image Source="/Assets/markus-logo.png" <Image Source="/Assets/markle-logo.png"
Width="200" Height="200" Width="200" Height="200"
HorizontalAlignment="Center"/> HorizontalAlignment="Center"/>
<!-- App Name --> <!-- App Name -->
<TextBlock Text="Markus" <TextBlock Text="Markle"
FontSize="48" FontSize="48"
FontWeight="Light" FontWeight="Light"
Foreground="White" Foreground="White"

89
src/Views/FindPanel.axaml Normal file
View File

@@ -0,0 +1,89 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MarkdownEditor.ViewModels"
x:Class="MarkdownEditor.Views.FindPanel"
x:DataType="vm:MarkdownTabViewModel">
<Border Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
BorderThickness="0,0,0,1"
Padding="8,6"
CornerRadius="0">
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto">
<!-- Search icon -->
<TextBlock Text="🔍"
FontSize="14"
VerticalAlignment="Center"
Margin="0,0,8,0"
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"/>
<!-- Search input -->
<TextBox x:Name="SearchTextBox"
Grid.Column="1"
Text="{Binding SearchText, Mode=TwoWay}"
Watermark="Search"
FontSize="13"
Padding="6,4"
MinWidth="200"
VerticalAlignment="Center"
Margin="0,0,8,0"
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
KeyDown="OnSearchTextBoxKeyDown"/>
<!-- Match count -->
<TextBlock x:Name="MatchCountText"
Grid.Column="2"
Text="{Binding SearchMatchInfo}"
FontSize="12"
VerticalAlignment="Center"
Margin="0,0,12,0"
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
Opacity="0.7"/>
<!-- Previous match button -->
<Button Grid.Column="3"
Content="↑"
FontSize="14"
Width="28"
Height="24"
Padding="0"
Margin="0,0,4,0"
Command="{Binding FindPreviousCommand}"
ToolTip.Tip="Previous Match (Shift+Enter)"
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
Classes="find-button"/>
<!-- Next match button -->
<Button Grid.Column="4"
Content="↓"
FontSize="14"
Width="28"
Height="24"
Padding="0"
Margin="0,0,4,0"
Command="{Binding FindNextCommand}"
ToolTip.Tip="Next Match (Enter)"
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
Classes="find-button"/>
<!-- Close button -->
<Button Grid.Column="5"
Content="✕"
FontSize="14"
Width="24"
Height="24"
Padding="0"
Command="{Binding CloseFindPanelCommand}"
ToolTip.Tip="Close (Esc)"
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
Classes="find-button"/>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,55 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using MarkdownEditor.ViewModels;
namespace MarkdownEditor.Views;
/// <summary>
/// VSCode-style find panel overlay for searching text in both raw and pretty views.
/// </summary>
public partial class FindPanel : UserControl
{
/// <summary>
/// Initializes a new instance of the <see cref="FindPanel"/> class.
/// </summary>
public FindPanel()
{
InitializeComponent();
}
/// <inheritdoc/>
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
// Focus the search text box when the panel is shown
SearchTextBox?.Focus();
SearchTextBox?.SelectAll();
}
private void OnSearchTextBoxKeyDown(object? sender, KeyEventArgs e)
{
if (DataContext is not MarkdownTabViewModel viewModel)
return;
switch (e.Key)
{
case Key.Enter:
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
viewModel.FindPreviousCommand.Execute(null);
}
else
{
viewModel.FindNextCommand.Execute(null);
}
e.Handled = true;
break;
case Key.Escape:
viewModel.CloseFindPanelCommand.Execute(null);
e.Handled = true;
break;
}
}
}

View File

@@ -9,8 +9,8 @@
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="MarkdownEditor.Views.MainWindow" x:Class="MarkdownEditor.Views.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Icon="/Assets/markus-logo.png" Icon="/Assets/markle-logo.png"
Title="Markus - A Simple Markdown Editor" Title="Markle - A Simple Markdown Editor"
Width="1200" Height="800"> Width="1200" Height="800">
<Design.DataContext> <Design.DataContext>
@@ -22,6 +22,8 @@
<KeyBinding Gesture="Cmd+S" Command="{Binding SaveFileCommand}"/> <KeyBinding Gesture="Cmd+S" Command="{Binding SaveFileCommand}"/>
<KeyBinding Gesture="Cmd+Shift+S" Command="{Binding SaveFileAsCommand}"/> <KeyBinding Gesture="Cmd+Shift+S" Command="{Binding SaveFileAsCommand}"/>
<KeyBinding Gesture="Cmd+T" Command="{Binding AddNewTabCommand}"/> <KeyBinding Gesture="Cmd+T" Command="{Binding AddNewTabCommand}"/>
<KeyBinding Gesture="Cmd+F" Command="{Binding ShowFindPanelCommand}"/>
<KeyBinding Gesture="Ctrl+F" Command="{Binding ShowFindPanelCommand}"/>
</Window.KeyBindings> </Window.KeyBindings>
<NativeMenu.Menu> <NativeMenu.Menu>
@@ -43,6 +45,8 @@
<NativeMenuItem Header="_Copy" Gesture="Cmd+C"/> <NativeMenuItem Header="_Copy" Gesture="Cmd+C"/>
<NativeMenuItem Header="_Paste" Gesture="Cmd+V"/> <NativeMenuItem Header="_Paste" Gesture="Cmd+V"/>
<NativeMenuItemSeparator/> <NativeMenuItemSeparator/>
<NativeMenuItem Header="_Find" Command="{Binding ShowFindPanelCommand}" Gesture="Cmd+F"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="Select _All" Gesture="Cmd+A"/> <NativeMenuItem Header="Select _All" Gesture="Cmd+A"/>
</NativeMenu> </NativeMenu>
</NativeMenuItem> </NativeMenuItem>
@@ -87,6 +91,15 @@
<Setter Property="FontSize" Value="13"/> <Setter Property="FontSize" Value="13"/>
</Style> </Style>
<!-- Find panel button style -->
<Style Selector="Button.find-button">
<Setter Property="CornerRadius" Value="3"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="Button.find-button:pointerover">
<Setter Property="Background" Value="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"/>
</Style>
<!-- Compact tab styling --> <!-- Compact tab styling -->
<Style Selector="TabItem"> <Style Selector="TabItem">
<Setter Property="FontSize" Value="13"/> <Setter Property="FontSize" Value="13"/>
@@ -109,11 +122,11 @@
<!-- Left: Logo --> <!-- Left: Logo -->
<Image Grid.Column="0" <Image Grid.Column="0"
Source="/Assets/markus-logo.png" Source="/Assets/markle-logo.png"
Width="28" Height="28" Width="28" Height="28"
Margin="8,2,8,2" Margin="8,2,8,2"
VerticalAlignment="Center" VerticalAlignment="Center"
ToolTip.Tip="Markus - A Simple Markdown Editor"/> ToolTip.Tip="Markle - A Simple Markdown Editor"/>
<!-- Center: Tabs with scrolling --> <!-- Center: Tabs with scrolling -->
<StackPanel Grid.Column="1" Orientation="Horizontal"> <StackPanel Grid.Column="1" Orientation="Horizontal">
@@ -473,11 +486,20 @@
VerticalScrollBarVisibility="Auto"/> VerticalScrollBarVisibility="Auto"/>
<!-- Pretty markdown viewer --> <!-- Pretty markdown viewer -->
<views:MarkdownViewer Markdown="{Binding SafeMarkdownContent}" <views:MarkdownViewer x:Name="MarkdownViewer"
Markdown="{Binding SafeMarkdownContent}"
IsVisible="{Binding IsPrettyView}" IsVisible="{Binding IsPrettyView}"
FontSize="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ContentFontSize}" FontSize="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ContentFontSize}"
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}" Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"/> Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"/>
<!-- Find Panel Overlay (VSCode-style) -->
<views:FindPanel x:Name="FindPanelControl"
IsVisible="{Binding IsFindPanelVisible}"
VerticalAlignment="Top"
HorizontalAlignment="Stretch"
ZIndex="1000"
DataContext="{Binding}"/>
</Grid> </Grid>
</DockPanel> </DockPanel>
</DataTemplate> </DataTemplate>

View File

@@ -27,6 +27,92 @@ public partial class MainWindow : Window
// Subscribe to pointer pressed to capture selection before command executes // Subscribe to pointer pressed to capture selection before command executes
this.AddHandler(Button.PointerPressedEvent, OnButtonPointerPressed, Avalonia.Interactivity.RoutingStrategies.Tunnel); this.AddHandler(Button.PointerPressedEvent, OnButtonPointerPressed, Avalonia.Interactivity.RoutingStrategies.Tunnel);
// Subscribe to search events from tabs
if (DataContext is MainWindowViewModel viewModel)
{
viewModel.PropertyChanged += OnViewModelPropertyChanged;
}
}
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainWindowViewModel.SelectedTab))
{
// Unsubscribe from old tab
if (sender is MainWindowViewModel viewModel)
{
foreach (var tab in viewModel.Tabs)
{
tab.SearchRequested -= OnTabSearchRequested;
}
// Subscribe to new selected tab
if (viewModel.SelectedTab != null)
{
viewModel.SelectedTab.SearchRequested += OnTabSearchRequested;
}
}
}
}
private void OnTabSearchRequested(object? sender, SearchEventArgs e)
{
if (sender is not MarkdownTabViewModel tabViewModel)
return;
if (e.ClearHighlight)
{
// Clear search highlighting
ClearSearchHighlight();
return;
}
if (tabViewModel.IsPrettyView)
{
// Handle search in pretty view
HandlePrettyViewSearch(e);
}
else
{
// Handle search in raw editor view
HandleRawEditorSearch(tabViewModel, e);
}
}
private void HandleRawEditorSearch(MarkdownTabViewModel tabViewModel, SearchEventArgs e)
{
var textEditor = this.GetVisualDescendants().OfType<TextEditor>()
.FirstOrDefault(te => te.IsVisible && te.Name == "EditorTextBox");
if (textEditor == null)
return;
if (e.ScrollToSelection && e.MatchPosition >= 0)
{
// Set selection and scroll to it
textEditor.SelectionStart = e.MatchPosition;
textEditor.SelectionLength = e.SearchText.Length;
textEditor.CaretOffset = e.MatchPosition + e.SearchText.Length;
// Scroll to caret
var line = textEditor.Document.GetLineByOffset(textEditor.CaretOffset);
textEditor.ScrollToLine(line.LineNumber);
}
}
private void HandlePrettyViewSearch(SearchEventArgs e)
{
// For pretty view, we could highlight matches visually
// For now, we'll just scroll to the match position if possible
// Note: SelectableTextBlock doesn't have direct scroll-to-position support,
// so this is a simplified implementation
}
private void ClearSearchHighlight()
{
// Clear any search highlighting in both views
// This is a placeholder for future highlighting implementation
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -39,6 +125,13 @@ public partial class MainWindow : Window
viewModel.StorageProvider = StorageProvider; viewModel.StorageProvider = StorageProvider;
// Restore session after StorageProvider is set // Restore session after StorageProvider is set
_ = viewModel.RestoreSessionAsync(); _ = viewModel.RestoreSessionAsync();
// Subscribe to selected tab changes
viewModel.PropertyChanged += OnViewModelPropertyChanged;
if (viewModel.SelectedTab != null)
{
viewModel.SelectedTab.SearchRequested += OnTabSearchRequested;
}
} }
} }

View File

@@ -1,6 +1,7 @@
using System; using System;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media; using Avalonia.Media;
using Markdig; using Markdig;
using MarkdownEditor.Views.Renderers; using MarkdownEditor.Views.Renderers;
@@ -28,7 +29,7 @@ public class MarkdownViewer : UserControl
set => SetValue(MarkdownProperty, value); set => SetValue(MarkdownProperty, value);
} }
private readonly StackPanel _container; private readonly SelectableTextBlock _textBlock;
private readonly ScrollViewer _scrollViewer; private readonly ScrollViewer _scrollViewer;
private readonly MarkdownPipeline _pipeline; private readonly MarkdownPipeline _pipeline;
private MarkdownRendererCoordinator? _rendererCoordinator; private MarkdownRendererCoordinator? _rendererCoordinator;
@@ -38,10 +39,15 @@ public class MarkdownViewer : UserControl
/// </summary> /// </summary>
public MarkdownViewer() public MarkdownViewer()
{ {
_container = new StackPanel { Margin = new Thickness(10) }; _textBlock = new SelectableTextBlock
{
Margin = new Thickness(10),
TextWrapping = TextWrapping.Wrap
};
_scrollViewer = new ScrollViewer _scrollViewer = new ScrollViewer
{ {
Content = _container, Content = _textBlock,
HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto,
VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto
}; };
@@ -71,7 +77,11 @@ public class MarkdownViewer : UserControl
/// </summary> /// </summary>
private void RenderMarkdown() private void RenderMarkdown()
{ {
_container.Children.Clear(); // Clear existing inlines
if (_textBlock.Inlines != null)
{
_textBlock.Inlines.Clear();
}
if (string.IsNullOrWhiteSpace(Markdown)) if (string.IsNullOrWhiteSpace(Markdown))
return; return;
@@ -90,25 +100,32 @@ public class MarkdownViewer : UserControl
// Calculate font scale factor (base size is 14) // Calculate font scale factor (base size is 14)
double fontScale = FontSize / 14.0; double fontScale = FontSize / 14.0;
// Set base font size and colors
_textBlock.FontSize = FontSize;
_textBlock.Foreground = Foreground;
// Create coordinator with theme-aware colors and font scale // Create coordinator with theme-aware colors and font scale
_rendererCoordinator = new MarkdownRendererCoordinator(isDarkMode, fontScale); _rendererCoordinator = new MarkdownRendererCoordinator(isDarkMode, fontScale);
var document = Markdig.Markdown.Parse(Markdown, _pipeline); var document = Markdig.Markdown.Parse(Markdown, _pipeline);
var controls = _rendererCoordinator.RenderBlocks(document);
foreach (var control in controls) // Render all blocks into a single SelectableTextBlock's Inlines collection
// This enables text selection across multiple blocks
if (_textBlock.Inlines != null)
{ {
_container.Children.Add(control); _rendererCoordinator.RenderBlocksToInlines(document, _textBlock.Inlines);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_container.Children.Add(new TextBlock if (_textBlock.Inlines != null)
{ {
Text = $"Error rendering markdown: {ex.Message}", _textBlock.Inlines.Clear();
Foreground = Brushes.Red, _textBlock.Inlines.Add(new Run($"Error rendering markdown: {ex.Message}")
TextWrapping = TextWrapping.Wrap {
Foreground = Brushes.Red
}); });
} }
} }
} }
}

View File

@@ -46,7 +46,7 @@ public class CodeBlockRenderer : IMarkdownBlockRenderer
Padding = new Thickness(10), Padding = new Thickness(10),
Margin = new Thickness(0, 5, 0, 5), Margin = new Thickness(0, 5, 0, 5),
CornerRadius = new CornerRadius(4), CornerRadius = new CornerRadius(4),
Child = new TextBlock Child = new SelectableTextBlock
{ {
Text = text, Text = text,
FontFamily = new FontFamily("Consolas,Courier New,monospace"), FontFamily = new FontFamily("Consolas,Courier New,monospace"),

View File

@@ -36,7 +36,7 @@ public class HeadingRenderer : IMarkdownBlockRenderer
var text = MarkdownTextHelper.ExtractText(heading.Inline); var text = MarkdownTextHelper.ExtractText(heading.Inline);
var fontSize = GetFontSizeForLevel(heading.Level) * _fontScale; var fontSize = GetFontSizeForLevel(heading.Level) * _fontScale;
return new TextBlock return new SelectableTextBlock
{ {
Text = text, Text = text,
FontSize = fontSize, FontSize = fontSize,

View File

@@ -54,7 +54,7 @@ public class ListRenderer : IMarkdownBlockRenderer
{ {
Children = Children =
{ {
new TextBlock new SelectableTextBlock
{ {
Text = bullet, Text = bullet,
[DockPanel.DockProperty] = Dock.Left, [DockPanel.DockProperty] = Dock.Left,

View File

@@ -1,6 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media;
using Markdig.Extensions.Tables;
using Markdig.Syntax; using Markdig.Syntax;
using MarkdownEditor.Constants;
namespace MarkdownEditor.Views.Renderers; namespace MarkdownEditor.Views.Renderers;
@@ -11,6 +15,9 @@ namespace MarkdownEditor.Views.Renderers;
public class MarkdownRendererCoordinator public class MarkdownRendererCoordinator
{ {
private readonly List<IMarkdownBlockRenderer> _renderers; private readonly List<IMarkdownBlockRenderer> _renderers;
private readonly InlineRenderer _inlineRenderer;
private readonly bool _isDarkMode;
private readonly double _fontScale;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MarkdownRendererCoordinator"/> class. /// Initializes a new instance of the <see cref="MarkdownRendererCoordinator"/> class.
@@ -19,13 +26,15 @@ public class MarkdownRendererCoordinator
/// <param name="fontScale">Font scale factor for text sizing (1.0 = 14px base).</param> /// <param name="fontScale">Font scale factor for text sizing (1.0 = 14px base).</param>
public MarkdownRendererCoordinator(bool isDarkMode = true, double fontScale = 1.0) public MarkdownRendererCoordinator(bool isDarkMode = true, double fontScale = 1.0)
{ {
var inlineRenderer = new InlineRenderer(isDarkMode, fontScale); _isDarkMode = isDarkMode;
_fontScale = fontScale;
_inlineRenderer = new InlineRenderer(isDarkMode, fontScale);
_renderers = new List<IMarkdownBlockRenderer> _renderers = new List<IMarkdownBlockRenderer>
{ {
new HeadingRenderer(fontScale), new HeadingRenderer(fontScale),
new CodeBlockRenderer(isDarkMode, fontScale), new CodeBlockRenderer(isDarkMode, fontScale),
new ParagraphRenderer(inlineRenderer), new ParagraphRenderer(_inlineRenderer),
new ThematicBreakRenderer(isDarkMode), new ThematicBreakRenderer(isDarkMode),
new ListRenderer(this), // Pass coordinator for nested rendering new ListRenderer(this), // Pass coordinator for nested rendering
new QuoteRenderer(this), // Pass coordinator for nested rendering new QuoteRenderer(this), // Pass coordinator for nested rendering
@@ -69,5 +78,261 @@ public class MarkdownRendererCoordinator
} }
return controls; return controls;
} }
/// <summary>
/// Renders all markdown blocks into a single SelectableTextBlock's Inlines collection.
/// This enables text selection across multiple blocks.
/// </summary>
/// <param name="blocks">The markdown blocks to render.</param>
/// <param name="inlines">The inline collection to populate.</param>
public void RenderBlocksToInlines(IEnumerable<Block> blocks, InlineCollection inlines)
{
bool isFirstBlock = true;
foreach (var block in blocks)
{
// Add spacing between blocks (except before the first one)
if (!isFirstBlock)
{
inlines.Add(new LineBreak());
inlines.Add(new LineBreak());
}
isFirstBlock = false;
switch (block)
{
case HeadingBlock heading:
RenderHeadingToInlines(heading, inlines);
break;
case ParagraphBlock paragraph:
_inlineRenderer.BuildInlines(paragraph.Inline, inlines);
break;
case CodeBlock codeBlock:
RenderCodeBlockToInlines(codeBlock, inlines);
break;
case ListBlock list:
RenderListToInlines(list, inlines);
break;
case QuoteBlock quote:
RenderQuoteToInlines(quote, inlines);
break;
case Table table:
RenderTableToInlines(table, inlines);
break;
case ThematicBreakBlock:
inlines.Add(new Run(new string('─', 50))
{
Foreground = new SolidColorBrush(Color.Parse(_isDarkMode ? "#666666" : "#CCCCCC"))
});
break;
}
}
}
private void RenderHeadingToInlines(HeadingBlock heading, InlineCollection inlines)
{
var fontSize = GetHeadingFontSize(heading.Level) * _fontScale;
var text = MarkdownTextHelper.ExtractText(heading.Inline);
// Render heading text with inline formatting preserved
if (heading.Inline != null)
{
foreach (var inline in heading.Inline)
{
if (inline is Markdig.Syntax.Inlines.LiteralInline literal)
{
inlines.Add(new Run(literal.Content.ToString())
{
FontSize = fontSize,
FontWeight = FontWeight.Bold
});
}
else if (inline is Markdig.Syntax.Inlines.ContainerInline container)
{
// Handle nested formatting (bold, italic, etc.)
RenderInlineToInlines(container, inlines, fontSize, FontWeight.Bold);
}
}
}
else
{
inlines.Add(new Run(text)
{
FontSize = fontSize,
FontWeight = FontWeight.Bold
});
}
}
private void RenderInlineToInlines(Markdig.Syntax.Inlines.ContainerInline container, InlineCollection inlines, double fontSize, FontWeight fontWeight)
{
foreach (var inline in container)
{
if (inline is Markdig.Syntax.Inlines.LiteralInline literal)
{
inlines.Add(new Run(literal.Content.ToString())
{
FontSize = fontSize,
FontWeight = fontWeight
});
}
else if (inline is Markdig.Syntax.Inlines.EmphasisInline emphasis)
{
var text = MarkdownTextHelper.ExtractText(emphasis);
var run = new Run(text)
{
FontSize = fontSize,
FontWeight = fontWeight
};
if (emphasis.DelimiterCount == 2)
{
run.FontWeight = FontWeight.Bold;
}
else
{
run.FontStyle = FontStyle.Italic;
}
inlines.Add(run);
}
else if (inline is Markdig.Syntax.Inlines.ContainerInline nested)
{
RenderInlineToInlines(nested, inlines, fontSize, fontWeight);
}
}
}
private void RenderCodeBlockToInlines(CodeBlock codeBlock, InlineCollection inlines)
{
var text = codeBlock is FencedCodeBlock fenced
? fenced.Lines.ToString()
: codeBlock.Lines.ToString();
inlines.Add(new Run(text)
{
FontFamily = new FontFamily("Consolas,Courier New,monospace"),
FontSize = 14 * _fontScale,
Foreground = new SolidColorBrush(Color.Parse(_isDarkMode ? "#E8E8E8" : "#333333")),
Background = new SolidColorBrush(Color.Parse(_isDarkMode ? "#2D2D2D" : "#F0F0F0"))
});
}
private void RenderListToInlines(ListBlock list, InlineCollection inlines)
{
int index = 1;
foreach (var item in list)
{
if (item is ListItemBlock listItem)
{
var bullet = list.IsOrdered ? $"{index}. " : "• ";
inlines.Add(new Run(bullet));
foreach (var childBlock in listItem)
{
if (childBlock is ParagraphBlock paragraph)
{
_inlineRenderer.BuildInlines(paragraph.Inline, inlines);
}
else if (childBlock is HeadingBlock heading)
{
RenderHeadingToInlines(heading, inlines);
}
else
{
// For other block types in list items, recursively render them
RenderBlockToInlines(childBlock, inlines);
}
}
inlines.Add(new LineBreak());
index++;
}
}
}
private void RenderQuoteToInlines(QuoteBlock quote, InlineCollection inlines)
{
foreach (var childBlock in quote)
{
if (childBlock is ParagraphBlock paragraph)
{
inlines.Add(new Run("> "));
_inlineRenderer.BuildInlines(paragraph.Inline, inlines);
inlines.Add(new LineBreak());
}
}
}
private void RenderTableToInlines(Table table, InlineCollection inlines)
{
// Render table as plain text for selection purposes
foreach (var row in table)
{
if (row is TableRow tableRow)
{
bool firstCell = true;
foreach (var cell in tableRow)
{
if (!firstCell) inlines.Add(new Run(" | "));
firstCell = false;
if (cell is TableCell tableCell)
{
// Table cells contain child blocks, not inline content directly
foreach (var childBlock in tableCell)
{
RenderBlockToInlines(childBlock, inlines);
}
}
}
inlines.Add(new LineBreak());
}
}
}
private void RenderBlockToInlines(Block block, InlineCollection inlines)
{
// Helper method to render any block type to inlines
switch (block)
{
case ParagraphBlock paragraph:
_inlineRenderer.BuildInlines(paragraph.Inline, inlines);
break;
case HeadingBlock heading:
RenderHeadingToInlines(heading, inlines);
break;
case CodeBlock codeBlock:
RenderCodeBlockToInlines(codeBlock, inlines);
break;
default:
// For other block types, try to extract text if they have inline content
Markdig.Syntax.Inlines.ContainerInline? inlineContent = null;
if (block is ParagraphBlock p) inlineContent = p.Inline;
else if (block is HeadingBlock h) inlineContent = h.Inline;
var text = MarkdownTextHelper.ExtractText(inlineContent);
if (!string.IsNullOrEmpty(text))
{
inlines.Add(new Run(text));
}
break;
}
}
private static int GetHeadingFontSize(int level) => level switch
{
1 => AppConstants.FontSizes.Heading1,
2 => AppConstants.FontSizes.Heading2,
3 => AppConstants.FontSizes.Heading3,
4 => AppConstants.FontSizes.Heading4,
5 => AppConstants.FontSizes.Heading5,
_ => AppConstants.FontSizes.Heading6
};
} }

View File

@@ -32,7 +32,7 @@ public class ParagraphRenderer : IMarkdownBlockRenderer
return null; return null;
} }
var textBlock = new TextBlock var textBlock = new SelectableTextBlock
{ {
Margin = new Thickness(0, 5, 0, 5), Margin = new Thickness(0, 5, 0, 5),
TextWrapping = TextWrapping.Wrap TextWrapping = TextWrapping.Wrap

View File

@@ -27,8 +27,8 @@ Creates a complete `.app` bundle with:
Automatically detects your Mac architecture (Intel or Apple Silicon). Automatically detects your Mac architecture (Intel or Apple Silicon).
**Output:** **Output:**
- Apple Silicon: `./bin/Release/osx-arm64/OrmentiaMarkus.app` - Apple Silicon: `./bin/Release/osx-arm64/OrmentiaMarkle.app`
- Intel: `./bin/Release/osx-x64/OrmentiaMarkus.app` - Intel: `./bin/Release/osx-x64/OrmentiaMarkle.app`
### Windows ### Windows
@@ -41,7 +41,7 @@ Creates a self-contained `.exe` file with:
- All dependencies bundled - All dependencies bundled
- Single-file executable ready for distribution - Single-file executable ready for distribution
**Output:** `.\bin\Release\win-x64\OrmentiaMarkus.exe` **Output:** `.\bin\Release\win-x64\OrmentiaMarkle.exe`
**Note:** Icon file (`Assets/AppIcon.ico`) must be created first. Run `bash deploy/create-windows-icon.sh` from macOS/Linux. **Note:** Icon file (`Assets/AppIcon.ico`) must be created first. Run `bash deploy/create-windows-icon.sh` from macOS/Linux.
@@ -58,7 +58,7 @@ Creates a complete application package with:
- Launcher script - Launcher script
- Installation README - Installation README
**Output:** `./bin/Release/linux-x64/OrmentiaMarkus/` folder (ready to archive and distribute) **Output:** `./bin/Release/linux-x64/OrmentiaMarkle/` folder (ready to archive and distribute)
## Helper Scripts ## Helper Scripts

View File

@@ -3,7 +3,7 @@
set -e set -e
echo "Building Markus for Linux..." echo "Building Markle for Linux..."
# Navigate to project root # Navigate to project root
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
@@ -23,7 +23,7 @@ dotnet publish MarkdownEditor.csproj \
-o ./bin/Release/linux-x64/publish -o ./bin/Release/linux-x64/publish
# Create application directory structure # Create application directory structure
APP_DIR="./bin/Release/linux-x64/OrmentiaMarkus" APP_DIR="./bin/Release/linux-x64/OrmentiaMarkle"
echo "Creating application package..." echo "Creating application package..."
rm -rf "$APP_DIR" rm -rf "$APP_DIR"
mkdir -p "$APP_DIR" mkdir -p "$APP_DIR"
@@ -32,46 +32,46 @@ mkdir -p "$APP_DIR/share/applications"
mkdir -p "$APP_DIR/share/icons/hicolor/256x256/apps" mkdir -p "$APP_DIR/share/icons/hicolor/256x256/apps"
# Copy executable # Copy executable
cp ./bin/Release/linux-x64/publish/OrmentiaMarkus "$APP_DIR/bin/" cp ./bin/Release/linux-x64/publish/OrmentiaMarkle "$APP_DIR/bin/"
chmod +x "$APP_DIR/bin/OrmentiaMarkus" chmod +x "$APP_DIR/bin/OrmentiaMarkle"
# Copy icon # Copy icon
if [ -f "Assets/AppIcon.iconset/icon_256x256.png" ]; then if [ -f "Assets/AppIcon.iconset/icon_256x256.png" ]; then
cp "Assets/AppIcon.iconset/icon_256x256.png" "$APP_DIR/share/icons/hicolor/256x256/apps/ormentia-markus.png" cp "Assets/AppIcon.iconset/icon_256x256.png" "$APP_DIR/share/icons/hicolor/256x256/apps/ormentia-markle.png"
fi fi
# Create .desktop file # Create .desktop file
cat > "$APP_DIR/share/applications/ormentia-markus.desktop" << EOF cat > "$APP_DIR/share/applications/ormentia-markle.desktop" << EOF
[Desktop Entry] [Desktop Entry]
Version=1.0 Version=1.0
Type=Application Type=Application
Name=Markus Name=Markle
Comment=A Simple Markdown Editor Comment=A Simple Markdown Editor
Exec=OrmentiaMarkus Exec=OrmentiaMarkle
Icon=ormentia-markus Icon=ormentia-markle
Categories=Office;TextEditor;Utility; Categories=Office;TextEditor;Utility;
Terminal=false Terminal=false
StartupNotify=true StartupNotify=true
EOF EOF
# Create launcher script # Create launcher script
cat > "$APP_DIR/OrmentiaMarkus" << 'EOF' cat > "$APP_DIR/OrmentiaMarkle" << 'EOF'
#!/bin/bash #!/bin/bash
# Launcher script for Markus # Launcher script for Markle
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$SCRIPT_DIR/bin/OrmentiaMarkus" "$@" exec "$SCRIPT_DIR/bin/OrmentiaMarkle" "$@"
EOF EOF
chmod +x "$APP_DIR/OrmentiaMarkus" chmod +x "$APP_DIR/OrmentiaMarkle"
# Create README # Create README
cat > "$APP_DIR/README.txt" << EOF cat > "$APP_DIR/README.txt" << EOF
Markus - A Simple Markdown Editor Markle - A Simple Markdown Editor
================================== ==================================
Installation: Installation:
1. Extract this archive to your preferred location (e.g., /opt or ~/Applications) 1. Extract this archive to your preferred location (e.g., /opt or ~/Applications)
2. Run ./OrmentiaMarkus to launch the application 2. Run ./OrmentiaMarkle to launch the application
3. (Optional) Copy share/applications/ormentia-markus.desktop to ~/.local/share/applications/ 3. (Optional) Copy share/applications/ormentia-markle.desktop to ~/.local/share/applications/
for desktop menu integration for desktop menu integration
© 2025 Ormentia. All rights reserved. © 2025 Ormentia. All rights reserved.
@@ -83,11 +83,11 @@ rm -rf ./bin/Release/linux-x64/publish
echo "" echo ""
echo "✓ Build completed successfully!" echo "✓ Build completed successfully!"
echo "Application package: ./bin/Release/linux-x64/OrmentiaMarkus/" echo "Application package: ./bin/Release/linux-x64/OrmentiaMarkle/"
echo "" echo ""
echo "You can now:" echo "You can now:"
echo " 1. Run ./bin/Release/linux-x64/OrmentiaMarkus/OrmentiaMarkus" echo " 1. Run ./bin/Release/linux-x64/OrmentiaMarkle/OrmentiaMarkle"
echo " 2. Archive and distribute the OrmentiaMarkus folder" echo " 2. Archive and distribute the OrmentiaMarkle folder"
echo " 3. Install to /opt or ~/Applications" echo " 3. Install to /opt or ~/Applications"
echo "" echo ""

View File

@@ -3,7 +3,7 @@
set -e set -e
echo "Building Markus for macOS..." echo "Building Markle for macOS..."
# Navigate to project root # Navigate to project root
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
@@ -31,7 +31,7 @@ dotnet publish MarkdownEditor.csproj \
-o ./bin/Release/$RUNTIME/publish -o ./bin/Release/$RUNTIME/publish
# Create .app bundle structure # Create .app bundle structure
APP_NAME="OrmentiaMarkus.app" APP_NAME="OrmentiaMarkle.app"
APP_PATH="./bin/Release/$RUNTIME/$APP_NAME" APP_PATH="./bin/Release/$RUNTIME/$APP_NAME"
CONTENTS_PATH="$APP_PATH/Contents" CONTENTS_PATH="$APP_PATH/Contents"
MACOS_PATH="$CONTENTS_PATH/MacOS" MACOS_PATH="$CONTENTS_PATH/MacOS"
@@ -47,7 +47,7 @@ echo "Copying application files..."
cp -R ./bin/Release/$RUNTIME/publish/* "$MACOS_PATH/" cp -R ./bin/Release/$RUNTIME/publish/* "$MACOS_PATH/"
# Make the executable actually executable # Make the executable actually executable
chmod +x "$MACOS_PATH/OrmentiaMarkus" chmod +x "$MACOS_PATH/OrmentiaMarkle"
# Copy icon # Copy icon
echo "Copying application icon..." echo "Copying application icon..."
@@ -65,11 +65,11 @@ cat > "$CONTENTS_PATH/Info.plist" << EOF
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Markus</string> <string>Markle</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Markus</string> <string>Markle</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.ormentia.markus</string> <string>com.ormentia.markle</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0.0</string> <string>1.0.0</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
@@ -79,7 +79,7 @@ cat > "$CONTENTS_PATH/Info.plist" << EOF
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>MRKS</string> <string>MRKS</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>OrmentiaMarkus</string> <string>OrmentiaMarkle</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>AppIcon.icns</string> <string>AppIcon.icns</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@@ -2,7 +2,7 @@
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
Write-Host "Building Markus for Windows..." -ForegroundColor Cyan Write-Host "Building Markle for Windows..." -ForegroundColor Cyan
# Navigate to project root # Navigate to project root
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
@@ -30,7 +30,7 @@ dotnet publish MarkdownEditor.csproj `
Write-Host "" Write-Host ""
Write-Host "✓ Build completed successfully!" -ForegroundColor Green Write-Host "✓ Build completed successfully!" -ForegroundColor Green
Write-Host "Application: .\bin\Release\win-x64\OrmentiaMarkus.exe" -ForegroundColor Green Write-Host "Application: .\bin\Release\win-x64\OrmentiaMarkle.exe" -ForegroundColor Green
Write-Host "" Write-Host ""
Write-Host "You can now:" -ForegroundColor Cyan Write-Host "You can now:" -ForegroundColor Cyan
Write-Host " 1. Run the .exe file directly" -ForegroundColor Cyan Write-Host " 1. Run the .exe file directly" -ForegroundColor Cyan