notepad initial commit
Some checks failed
Release / release (push) Failing after 55s

This commit is contained in:
Greg Gauthier 2026-02-14 14:15:52 +00:00
commit 01acd4d6e6
17 changed files with 1302 additions and 0 deletions

View File

@ -0,0 +1,107 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-gitea
steps:
- name: Prep For Local Builds
run: echo "${LOCIP} gitea.comnenos" >> /etc/hosts
- uses: actions/checkout@v4
- name: Install Build Dependencies
run: |
apt update
apt -y --no-install-recommends install \
wget \
file \
fuse \
libfuse2 \
python3 \
python3-pip
- name: Install .NET SDK
run: |
wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh
chmod +x dotnet-install.sh
./dotnet-install.sh --channel 8.0 --install-dir /usr/local/dotnet
ln -sf /usr/local/dotnet/dotnet /usr/bin/dotnet
- name: Install AppImageTool
run: |
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage \
-O /usr/local/bin/appimagetool
chmod +x /usr/local/bin/appimagetool
- name: Build Linux Packages
run: |
cd NotePad
python3 publish.py linux both
- name: Create Release Archive
run: |
VERSION=${GITHUB_REF#refs/tags/}
mkdir -p release
# Copy AppImage
cp publish/appimage/NotePad-0.1.0-x86_64.AppImage \
release/NotePad-${VERSION}-x86_64.AppImage
# Copy Tarball
cp publish/tarball/notepad-0.1.0-linux-x64.tar.gz \
release/notepad-${VERSION}-linux-x64.tar.gz
# Generate checksums
cd release
sha256sum NotePad-${VERSION}-x86_64.AppImage > NotePad-${VERSION}-x86_64.AppImage.sha256
sha256sum notepad-${VERSION}-linux-x64.tar.gz > notepad-${VERSION}-linux-x64.tar.gz.sha256
ls -lh # For debugging
- name: Create Release
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
VERSION=${GITHUB_REF#refs/tags/}
# Create release
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"${VERSION}\",
\"name\": \"NotePad ${VERSION}\",
\"body\": \"Release ${VERSION}\n\n## Linux Downloads\n- **AppImage**: Portable, single-file executable (no installation needed)\n- **Tarball**: Traditional installation with \`sudo ./install.sh\`\"
}" > release_response.json
RELEASE_ID=$(cat release_response.json | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
# Upload AppImage
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=NotePad-${VERSION}-x86_64.AppImage" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @release/NotePad-${VERSION}-x86_64.AppImage
# Upload AppImage checksum
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=NotePad-${VERSION}-x86_64.AppImage.sha256" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: text/plain" \
--data-binary @release/NotePad-${VERSION}-x86_64.AppImage.sha256
# Upload tarball
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=notepad-${VERSION}-linux-x64.tar.gz" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/gzip" \
--data-binary @release/notepad-${VERSION}-linux-x64.tar.gz
# Upload tarball checksum
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=notepad-${VERSION}-linux-x64.tar.gz.sha256" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: text/plain" \
--data-binary @release/notepad-${VERSION}-linux-x64.tar.gz.sha256

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.idea/
bin/
obj/
/packages/
publish/
riderModule.iml
/_ReSharper.Caches/
poetry.lock

18
NotePad.sln Normal file
View File

@ -0,0 +1,18 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NotePad", "NotePad\NotePad.csproj", "{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
EndGlobal

10
NotePad/App.axaml Normal file
View File

@ -0,0 +1,10 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NotePad.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

23
NotePad/App.axaml.cs Normal file
View File

@ -0,0 +1,23 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace NotePad;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}

74
NotePad/MainWindow.axaml Normal file
View File

@ -0,0 +1,74 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NotePad.MainWindow"
Title="Untitled - Notepad"
Width="600" Height="400">
<DockPanel>
<!-- Menu Bar -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New" Click="OnNewClick" InputGesture="Ctrl+N"/>
<MenuItem Header="_Open..." Click="OnOpenClick" InputGesture="Ctrl+O"/>
<MenuItem Header="_Save" Click="OnSaveClick" InputGesture="Ctrl+S"/>
<MenuItem Header="Save _As..." Click="OnSaveAsClick"/>
<Separator/>
<MenuItem Header="E_xit" Click="OnExitClick"/>
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Undo" Click="OnUndoClick" InputGesture="Ctrl+Z"/>
<Separator/>
<MenuItem Header="Cu_t" Click="OnCutClick" InputGesture="Ctrl+X"/>
<MenuItem Header="_Copy" Click="OnCopyClick" InputGesture="Ctrl+C"/>
<MenuItem Header="_Paste" Click="OnPasteClick" InputGesture="Ctrl+V"/>
<MenuItem Header="De_lete" Click="OnDeleteClick" InputGesture="Delete"/>
<Separator/>
<MenuItem Header="Select _All" Click="OnSelectAllClick" InputGesture="Ctrl+A"/>
<MenuItem Header="Time/_Date" Click="OnTimeDateClick" InputGesture="F5"/>
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Header="_Word Wrap" x:Name="WordWrapMenuItem" Click="OnWordWrapToggle">
<MenuItem.Icon>
<CheckBox x:Name="WordWrapCheckBox" IsChecked="True" IsHitTestVisible="False"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_About Notepad" Click="OnAboutClick"/>
</MenuItem>
</Menu>
<!-- Text Editor Area -->
<Border Background="White" BorderThickness="0">
<TextBox x:Name="EditorTextBox"
AcceptsReturn="True"
AcceptsTab="True"
TextWrapping="Wrap"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
BorderThickness="0"
Background="White"
Foreground="Black"
CaretBrush="Black"
FontFamily="Consolas,Courier New,monospace"
FontSize="12">
<TextBox.Styles>
<Style Selector="TextBox">
<Setter Property="Background" Value="White"/>
<Setter Property="Foreground" Value="Black"/>
</Style>
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="Transparent"/>
</Style>
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="Transparent"/>
</Style>
</TextBox.Styles>
</TextBox>
</Border>
</DockPanel>
</Window>

374
NotePad/MainWindow.axaml.cs Normal file
View File

@ -0,0 +1,374 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Media;
namespace NotePad
{
public partial class MainWindow : Window
{
private string? _currentFilePath;
private string? _currentFileName;
private bool _isModified;
private string? _originalText;
public MainWindow()
{
InitializeComponent();
UpdateTitle();
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
textBox.TextChanged += OnTextChanged;
Closing += OnWindowClosing;
}
private void UpdateTitle()
{
var modified = _isModified ? "*" : "";
Title = $"{modified}{_currentFileName ?? "Untitled"} - Notepad";
}
private void OnTextChanged(object? sender, EventArgs e)
{
var currentText = this.FindControl<TextBox>("EditorTextBox")!.Text ?? string.Empty;
_isModified = currentText != _originalText;
UpdateTitle();
}
private async void OnWindowClosing(object? sender, WindowClosingEventArgs e)
{
if (_isModified)
{
e.Cancel = true;
var result = await ShowSavePrompt();
if (result == SavePromptResult.Save)
{
await PerformSave();
if (!_isModified) // Only close if save succeeded
{
Closing -= OnWindowClosing;
Close();
}
}
else if (result == SavePromptResult.DontSave)
{
Closing -= OnWindowClosing;
Close();
}
// Cancel = do nothing
}
}
private async Task<SavePromptResult> ShowSavePrompt()
{
var dialog = new Window
{
Title = "Notepad",
Width = 400,
Height = 150,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
CanResize = false
};
var result = SavePromptResult.Cancel;
var panel = new StackPanel { Margin = new Thickness(20) };
var message = new TextBlock
{
Text = $"Do you want to save changes to {_currentFileName ?? "Untitled"}?",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 20)
};
var buttonPanel = new StackPanel
{
Orientation = Avalonia.Layout.Orientation.Horizontal,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
Spacing = 10
};
var yesButton = new Button { Content = "Yes", Width = 80 };
yesButton.Click += (s, e) => { result = SavePromptResult.Save; dialog.Close(); };
var noButton = new Button { Content = "No", Width = 80 };
noButton.Click += (s, e) => { result = SavePromptResult.DontSave; dialog.Close(); };
var cancelButton = new Button { Content = "Cancel", Width = 80 };
cancelButton.Click += (s, e) => { result = SavePromptResult.Cancel; dialog.Close(); };
buttonPanel.Children.Add(yesButton);
buttonPanel.Children.Add(noButton);
buttonPanel.Children.Add(cancelButton);
panel.Children.Add(message);
panel.Children.Add(buttonPanel);
dialog.Content = panel;
await dialog.ShowDialog(this);
return result;
}
private enum SavePromptResult
{
Save,
DontSave,
Cancel
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
// New File
private async void OnNewClick(object? sender, RoutedEventArgs e)
{
if (_isModified)
{
var result = await ShowSavePrompt();
if (result == SavePromptResult.Save)
{
await PerformSave();
if (_isModified) return; // Save was cancelled
}
else if (result == SavePromptResult.Cancel)
{
return;
}
}
this.FindControl<TextBox>("EditorTextBox")!.Text = string.Empty;
_currentFilePath = null;
_currentFileName = null;
_originalText = string.Empty;
_isModified = false;
UpdateTitle();
}
// Open File
private async void OnOpenClick(object? sender, RoutedEventArgs e)
{
if (_isModified)
{
var result = await ShowSavePrompt();
if (result == SavePromptResult.Save)
{
await PerformSave();
if (_isModified) return; // Save was cancelled
}
else if (result == SavePromptResult.Cancel)
{
return;
}
}
var topLevel = GetTopLevel(this);
if (topLevel == null) return;
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open",
AllowMultiple = false,
FileTypeFilter = new[] { FilePickerFileTypes.TextPlain, FilePickerFileTypes.All }
});
if (files?.Count > 0)
{
var file = files[0];
_currentFilePath = file.TryGetLocalPath();
_currentFileName = file.Name;
await using var stream = await file.OpenReadAsync();
using var reader = new StreamReader(stream);
var text = await reader.ReadToEndAsync();
this.FindControl<TextBox>("EditorTextBox")!.Text = text;
_originalText = text;
_isModified = false;
UpdateTitle();
}
}
// Save File (if path exists, else Save As)
private async void OnSaveClick(object? sender, RoutedEventArgs e)
{
await PerformSave();
}
private async Task PerformSave()
{
if (string.IsNullOrEmpty(_currentFilePath))
{
await SaveAs();
}
else
{
await SaveToPath(_currentFilePath);
}
}
// Save As
private async void OnSaveAsClick(object? sender, RoutedEventArgs e)
{
await SaveAs();
}
private async Task SaveAs()
{
var topLevel = GetTopLevel(this);
if (topLevel == null) return;
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Save As",
DefaultExtension = "txt",
FileTypeChoices = new[] { FilePickerFileTypes.TextPlain, FilePickerFileTypes.All },
SuggestedFileName = _currentFileName ?? "Untitled.txt"
});
if (file != null)
{
_currentFilePath = file.TryGetLocalPath();
_currentFileName = file.Name;
await SaveToPath(_currentFilePath!);
UpdateTitle();
}
}
private async Task SaveToPath(string path)
{
var text = this.FindControl<TextBox>("EditorTextBox")!.Text ?? string.Empty;
await File.WriteAllTextAsync(path, text);
_originalText = text;
_isModified = false;
UpdateTitle();
}
// Exit
private void OnExitClick(object? sender, RoutedEventArgs e)
{
Close();
}
// Word Wrap Toggle
private void OnWordWrapToggle(object? sender, RoutedEventArgs e)
{
var checkBox = this.FindControl<CheckBox>("WordWrapCheckBox")!;
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
checkBox.IsChecked = !checkBox.IsChecked;
textBox.TextWrapping = checkBox.IsChecked == true ? TextWrapping.Wrap : TextWrapping.NoWrap;
}
// Edit Menu
private void OnUndoClick(object? sender, RoutedEventArgs e)
{
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
textBox.Undo();
}
private void OnCutClick(object? sender, RoutedEventArgs e)
{
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
textBox.Cut();
}
private void OnCopyClick(object? sender, RoutedEventArgs e)
{
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
textBox.Copy();
}
private void OnPasteClick(object? sender, RoutedEventArgs e)
{
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
textBox.Paste();
}
private void OnDeleteClick(object? sender, RoutedEventArgs e)
{
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
var selectionStart = textBox.SelectionStart;
var selectionEnd = textBox.SelectionEnd;
if (selectionStart != selectionEnd)
{
var text = textBox.Text ?? string.Empty;
textBox.Text = text.Remove(selectionStart, selectionEnd - selectionStart);
textBox.SelectionStart = selectionStart;
textBox.SelectionEnd = selectionStart;
}
}
private void OnSelectAllClick(object? sender, RoutedEventArgs e)
{
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
textBox.SelectAll();
}
private void OnTimeDateClick(object? sender, RoutedEventArgs e)
{
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
var timeDate = DateTime.Now.ToString("h:mm tt M/d/yyyy");
var position = textBox.SelectionStart;
var text = textBox.Text ?? string.Empty;
textBox.Text = text.Insert(position, timeDate);
textBox.SelectionStart = position + timeDate.Length;
}
// Help Menu
private async void OnAboutClick(object? sender, RoutedEventArgs e)
{
var dialog = new Window
{
Title = "About Notepad",
Width = 350,
Height = 200,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
CanResize = false
};
var panel = new StackPanel { Margin = new Thickness(20) };
var title = new TextBlock
{
Text = "Notepad",
FontSize = 16,
FontWeight = FontWeight.Bold,
Margin = new Thickness(0, 0, 0, 10)
};
var info = new TextBlock
{
Text = "A simple text editor built with Avalonia",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 20)
};
var okButton = new Button
{
Content = "OK",
Width = 80,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center
};
okButton.Click += (s, e) => dialog.Close();
panel.Children.Add(title);
panel.Children.Add(info);
panel.Children.Add(okButton);
dialog.Content = panel;
await dialog.ShowDialog(this);
}
}
}

21
NotePad/NotePad.csproj Normal file
View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.12"/>
<PackageReference Include="Avalonia.Desktop" Version="11.3.12"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

21
NotePad/Program.cs Normal file
View File

@ -0,0 +1,21 @@
using Avalonia;
using System;
namespace NotePad;
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

18
NotePad/app.manifest Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="NotePad.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

106
NotePad/build-appimage.sh Executable file
View File

@ -0,0 +1,106 @@
#!/bin/bash
# Build AppImage for NotePad
# Requires appimagetool: https://github.com/AppImage/AppImageKit/releases
set -e
APP_NAME="NotePad"
VERSION="0.1.0"
ARCH="x86_64"
# Directories
PUBLISH_DIR="../publish/linux-x64"
APPDIR="NotePad.AppDir"
OUTPUT_DIR="../publish/appimage"
echo "============================================================"
echo "Building AppImage for $APP_NAME v$VERSION"
echo "============================================================"
# Check if publish directory exists
if [ ! -d "$PUBLISH_DIR" ]; then
echo "❌ ERROR: $PUBLISH_DIR not found. Run 'python3 publish.py linux' first."
exit 1
fi
# Clean and create AppDir structure
echo "Creating AppDir structure..."
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin"
mkdir -p "$APPDIR/usr/share/applications"
mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps"
# Copy application files
echo "Copying application files..."
cp -r "$PUBLISH_DIR"/* "$APPDIR/usr/bin/"
# Create desktop entry
echo "Creating desktop entry..."
cat > "$APPDIR/usr/share/applications/notepad.desktop" << EOF
[Desktop Entry]
Name=NotePad
Comment=Simple text editor
Exec=NotePad
Icon=notepad
Type=Application
Categories=Utility;TextEditor;
Terminal=false
EOF
# Create a simple icon (using text as placeholder - replace with actual icon)
echo "Creating icon placeholder..."
# This creates a simple SVG icon - you should replace with an actual icon
cat > "$APPDIR/usr/share/icons/hicolor/256x256/apps/notepad.svg" << EOF
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
<rect width="256" height="256" fill="#4a90e2"/>
<text x="128" y="140" font-size="120" fill="white" text-anchor="middle" font-family="sans-serif">N</text>
</svg>
EOF
# Create AppRun script
echo "Creating AppRun script..."
cat > "$APPDIR/AppRun" << 'EOF'
#!/bin/bash
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export PATH="${HERE}/usr/bin:${PATH}"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
exec "${HERE}/usr/bin/NotePad" "$@"
EOF
chmod +x "$APPDIR/AppRun"
# Copy desktop file and icon to root of AppDir
cp "$APPDIR/usr/share/applications/notepad.desktop" "$APPDIR/"
cp "$APPDIR/usr/share/icons/hicolor/256x256/apps/notepad.svg" "$APPDIR/notepad.svg"
# Check for appimagetool
if ! command -v appimagetool &> /dev/null; then
echo "⚠️ WARNING: appimagetool not found."
echo " Download from: https://github.com/AppImage/AppImageKit/releases"
echo " Or run: wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
echo " chmod +x appimagetool-x86_64.AppImage"
echo ""
echo " AppDir created at: $APPDIR"
echo " You can manually run: appimagetool $APPDIR"
exit 1
fi
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Build AppImage
echo "Building AppImage..."
ARCH=$ARCH appimagetool "$APPDIR" "$OUTPUT_DIR/$APP_NAME-$VERSION-$ARCH.AppImage"
# Clean up
echo "Cleaning up..."
rm -rf "$APPDIR"
echo ""
echo "============================================================"
echo "✅ AppImage created successfully!"
echo "📦 Output: $OUTPUT_DIR/$APP_NAME-$VERSION-$ARCH.AppImage"
echo "============================================================"
echo ""
echo "To run: chmod +x $OUTPUT_DIR/$APP_NAME-$VERSION-$ARCH.AppImage"
echo " ./$OUTPUT_DIR/$APP_NAME-$VERSION-$ARCH.AppImage"

73
NotePad/install.sh Executable file
View File

@ -0,0 +1,73 @@
#!/bin/bash
# Installation script for NotePad
# This script installs NotePad to /opt/notepad and creates a symlink in /usr/local/bin
set -e
APP_NAME="NotePad"
INSTALL_DIR="/opt/notepad"
BIN_LINK="/usr/local/bin/notepad"
DESKTOP_FILE="/usr/share/applications/notepad.desktop"
echo "============================================================"
echo "$APP_NAME Installer"
echo "============================================================"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run as root (use sudo)"
exit 1
fi
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Installing $APP_NAME..."
echo ""
# Create installation directory
echo "→ Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
# Copy files
echo "→ Copying application files..."
cp -r "$SCRIPT_DIR"/* "$INSTALL_DIR/"
# Make executable
chmod +x "$INSTALL_DIR/NotePad"
# Create symlink
echo "→ Creating symlink: $BIN_LINK"
ln -sf "$INSTALL_DIR/NotePad" "$BIN_LINK"
# Create desktop entry
echo "→ Creating desktop entry..."
cat > "$DESKTOP_FILE" << EOF
[Desktop Entry]
Name=NotePad
Comment=Simple text editor
Exec=notepad %F
Icon=text-editor
Type=Application
Categories=Utility;TextEditor;
Terminal=false
MimeType=text/plain;
EOF
# Update desktop database if available
if command -v update-desktop-database &> /dev/null; then
echo "→ Updating desktop database..."
update-desktop-database /usr/share/applications
fi
echo ""
echo "============================================================"
echo "✅ Installation complete!"
echo "============================================================"
echo ""
echo "You can now run $APP_NAME by typing: notepad"
echo "Or find it in your applications menu."
echo ""
echo "To uninstall, run: sudo $INSTALL_DIR/uninstall.sh"
echo ""

248
NotePad/publish.py Normal file
View File

@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""
Build script for NotePad application
Creates distributable binaries for Linux and Windows
"""
import subprocess
import sys
import os
import shutil
import tarfile
from pathlib import Path
def run_command(cmd, description):
"""Run a shell command and print status"""
print(f"\n{'=' * 60}")
print(f"{description}")
print(f"{'=' * 60}")
print(f"Command: {' '.join(cmd)}\n")
result = subprocess.run(cmd, capture_output=False, text=True)
if result.returncode != 0:
print(f"\n❌ ERROR: {description} failed!")
sys.exit(1)
print(f"\n{description} completed successfully!")
return result
def copy_to_publish_dir(runtime_id, platform):
"""Copy published files to centralized publish directory"""
source_dir = Path(f"bin/Release/net8.0/{runtime_id}/publish")
dest_dir = Path(f"../publish/{runtime_id}")
if dest_dir.exists():
shutil.rmtree(dest_dir)
shutil.copytree(source_dir, dest_dir)
print(f"\n📦 Copied {platform.upper()} build to: {dest_dir.absolute()}")
return dest_dir
def build_windows_installer():
"""Build Windows installer using Inno Setup"""
iscc_path = shutil.which("iscc")
if not iscc_path:
print("\n⚠️ WARNING: Inno Setup not found. Skipping installer creation.")
print(" Install from: https://jrsoftware.org/isinfo.php")
print(" Or install via winget: winget install JRSoftware.InnoSetup")
return
setup_script = Path("setup.iss")
if not setup_script.exists():
print(f"\n⚠️ WARNING: {setup_script} not found. Skipping installer creation.")
return
cmd = [iscc_path, str(setup_script)]
try:
run_command(cmd, "Creating Windows installer")
installer_dir = Path("../publish/installers")
if installer_dir.exists():
installers = list(installer_dir.glob("*.exe"))
if installers:
print(f"\n📦 Installer created: {installers[0].absolute()}")
except Exception as e:
print(f"\n⚠️ WARNING: Failed to create installer: {e}")
def build_linux_tarball():
"""Create tar.gz archive with install script"""
print(f"\n{'=' * 60}")
print("Creating Linux tar.gz archive")
print(f"{'=' * 60}\n")
source_dir = Path("../publish/linux-x64")
if not source_dir.exists():
print(f"⚠️ WARNING: {source_dir} not found. Skipping tar.gz creation.")
return
# Create tarball directory
tarball_dir = Path("../publish/tarball")
tarball_dir.mkdir(parents=True, exist_ok=True)
# Create temporary directory for tarball contents
temp_dir = tarball_dir / "notepad-temp"
if temp_dir.exists():
shutil.rmtree(temp_dir)
temp_dir.mkdir()
# Copy application files
shutil.copytree(source_dir, temp_dir / "notepad")
# Copy install scripts
for script in ["install.sh", "uninstall.sh"]:
script_path = Path(script)
if script_path.exists():
shutil.copy(script_path, temp_dir / "notepad" / script)
os.chmod(temp_dir / "notepad" / script, 0o755)
# Create tar.gz
tarball_path = tarball_dir / "notepad-0.1.0-linux-x64.tar.gz"
with tarfile.open(tarball_path, "w:gz") as tar:
tar.add(temp_dir / "notepad", arcname="notepad")
# Clean up temp directory
shutil.rmtree(temp_dir)
print(f"\n✅ Tar.gz archive created successfully!")
print(f"📦 Output: {tarball_path.absolute()}")
print(f"\nTo install:")
print(f" tar -xzf {tarball_path.name}")
print(f" cd notepad")
print(f" sudo ./install.sh")
def build_linux_appimage():
"""Build Linux AppImage"""
build_script = Path("build-appimage.sh")
if not build_script.exists():
print(f"\n⚠️ WARNING: {build_script} not found. Skipping AppImage creation.")
return
try:
result = subprocess.run(
["bash", str(build_script)],
capture_output=True,
text=True
)
print(result.stdout)
if result.returncode != 0:
print(result.stderr)
print("\n⚠️ WARNING: AppImage creation failed.")
else:
print("\n✅ AppImage created successfully!")
except Exception as e:
print(f"\n⚠️ WARNING: Failed to create AppImage: {e}")
def publish_platform(platform, linux_package='both'):
"""Publish for a specific platform"""
runtime_id = f"{platform}-x64"
# Find dotnet executable
dotnet_path = shutil.which("dotnet")
if not dotnet_path:
# Try common locations
home = Path.home()
common_paths = [
home / ".dotnet" / "dotnet",
Path("/usr/bin/dotnet"),
Path("/usr/local/bin/dotnet")
]
for path in common_paths:
if path.exists():
dotnet_path = str(path)
break
if not dotnet_path:
print("❌ ERROR: dotnet not found. Please install .NET SDK.")
sys.exit(1)
cmd = [
dotnet_path, "publish",
"-c", "Release",
"-r", runtime_id,
"--self-contained", "true",
"-p:PublishSingleFile=true",
"-p:IncludeNativeLibrariesForSelfExtract=true",
"-p:PublishTrimmed=false" # Avalonia doesn't work well with trimming
]
run_command(cmd, f"Building for {platform.upper()}")
# Copy to centralized publish directory
dest_dir = copy_to_publish_dir(runtime_id, platform)
# Show the output files
if dest_dir.exists():
files = list(dest_dir.glob("*"))
for file in files:
size_mb = file.stat().st_size / (1024 * 1024)
print(f" - {file.name} ({size_mb:.2f} MB)")
# Build installer for Windows
if platform == 'win':
build_windows_installer()
# Build Linux packages
if platform == 'linux':
if linux_package in ['appimage', 'both']:
build_linux_appimage()
if linux_package in ['tarball', 'both']:
build_linux_tarball()
def main():
"""Main entry point"""
# Parse arguments
target = 'both'
linux_package = 'both'
if len(sys.argv) > 1:
target = sys.argv[1].lower()
if target not in ['linux', 'windows', 'both', 'all']:
print("Usage: python3 publish.py [linux|windows|both] [appimage|tarball|both]")
print(" Platform: linux, windows, or both (default: both)")
print(" Linux package: appimage, tarball, or both (default: both)")
print("")
print("Examples:")
print(" python3 publish.py # Build everything")
print(" python3 publish.py linux # Build Linux with both packages")
print(" python3 publish.py linux appimage # Build Linux AppImage only")
print(" python3 publish.py linux tarball # Build Linux tarball only")
print(" python3 publish.py windows # Build Windows installer")
sys.exit(1)
if len(sys.argv) > 2:
linux_package = sys.argv[2].lower()
if linux_package not in ['appimage', 'tarball', 'both']:
print("Error: Linux package must be 'appimage', 'tarball', or 'both'")
sys.exit(1)
# Change to script directory
script_dir = Path(__file__).parent
os.chdir(script_dir)
print(f"🚀 NotePad Build Script")
print(f"Target platform(s): {target.upper()}")
if target in ['linux', 'both', 'all']:
print(f"Linux package(s): {linux_package.upper()}")
# Build for requested platform(s)
if target in ['linux', 'both', 'all']:
publish_platform('linux', linux_package)
if target in ['windows', 'both', 'all']:
publish_platform('win')
print(f"\n{'=' * 60}")
print("🎉 All builds completed successfully!")
print(f"{'=' * 60}\n")
if __name__ == "__main__":
main()

54
NotePad/setup.iss Normal file
View File

@ -0,0 +1,54 @@
; Inno Setup Script for NotePad
; Download Inno Setup from: https://jrsoftware.org/isinfo.php
#define MyAppName "NotePad"
#define MyAppVersion "0.1.0"
#define MyAppPublisher "Greg Gauthier"
#define MyAppExeName "NotePad.exe"
#define MyAppAssocName "Text File"
#define MyAppAssocExt ".txt"
#define MyAppAssocKey StringChange(MyAppAssocName, " ", "") + MyAppAssocExt
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
AppId={{A8F9C3E1-5B2D-4A7C-9E8F-1D3C5B7A9E2F}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
LicenseFile=
OutputDir=..\publish\installers
OutputBaseFilename=NotePad-Setup-{#MyAppVersion}
Compression=lzma
SolidCompression=yes
WizardStyle=modern
ArchitecturesInstallIn64BitMode=x64
ArchitecturesAllowed=x64
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "associate"; Description: "Associate {#MyAppAssocExt} files with {#MyAppName}"; GroupDescription: "File associations:"; Flags: unchecked
[Files]
Source: "..\publish\win-x64\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Registry]
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associate
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey; Tasks: associate
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Tasks: associate
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Tasks: associate
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".txt"; ValueData: ""; Tasks: associate

53
NotePad/uninstall.sh Executable file
View File

@ -0,0 +1,53 @@
#!/bin/bash
# Uninstallation script for NotePad
set -e
APP_NAME="NotePad"
INSTALL_DIR="/opt/notepad"
BIN_LINK="/usr/local/bin/notepad"
DESKTOP_FILE="/usr/share/applications/notepad.desktop"
echo "============================================================"
echo "$APP_NAME Uninstaller"
echo "============================================================"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run as root (use sudo)"
exit 1
fi
echo "Uninstalling $APP_NAME..."
echo ""
# Remove symlink
if [ -L "$BIN_LINK" ]; then
echo "→ Removing symlink: $BIN_LINK"
rm -f "$BIN_LINK"
fi
# Remove desktop entry
if [ -f "$DESKTOP_FILE" ]; then
echo "→ Removing desktop entry: $DESKTOP_FILE"
rm -f "$DESKTOP_FILE"
fi
# Update desktop database if available
if command -v update-desktop-database &> /dev/null; then
echo "→ Updating desktop database..."
update-desktop-database /usr/share/applications
fi
# Remove installation directory
if [ -d "$INSTALL_DIR" ]; then
echo "→ Removing installation directory: $INSTALL_DIR"
rm -rf "$INSTALL_DIR"
fi
echo ""
echo "============================================================"
echo "✅ Uninstallation complete!"
echo "============================================================"
echo ""

85
publish.py Executable file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Build script for NotePad application
Creates distributable binaries for Linux and Windows
"""
import subprocess
import sys
import os
import shutil
from pathlib import Path
def run_command(cmd, description):
"""Run a shell command and print status"""
print(f"\n{'='*60}")
print(f"{description}")
print(f"{'='*60}")
print(f"Command: {' '.join(cmd)}\n")
result = subprocess.run(cmd, capture_output=False, text=True)
if result.returncode != 0:
print(f"\n❌ ERROR: {description} failed!")
sys.exit(1)
print(f"\n{description} completed successfully!")
return result
def publish_platform(platform):
"""Publish for a specific platform"""
runtime_id = f"{platform}-x64"
cmd = [
"dotnet", "publish",
"-c", "Release",
"-r", runtime_id,
"--self-contained", "true",
"-p:PublishSingleFile=true",
"-p:IncludeNativeLibrariesForSelfExtract=true",
"-p:PublishTrimmed=false" # Avalonia doesn't work well with trimming
]
run_command(cmd, f"Building for {platform.upper()}")
# Show the output location
output_dir = Path(f"bin/Release/net8.0/{runtime_id}/publish")
if output_dir.exists():
files = list(output_dir.glob("*"))
print(f"\n📦 Output files in: {output_dir.absolute()}")
for file in files:
size_mb = file.stat().st_size / (1024 * 1024)
print(f" - {file.name} ({size_mb:.2f} MB)")
def main():
"""Main entry point"""
# Parse arguments
if len(sys.argv) > 1:
target = sys.argv[1].lower()
if target not in ['linux', 'windows', 'both', 'all']:
print("Usage: python3 publish.py [linux|windows|both]")
print(" Default: both")
sys.exit(1)
else:
target = 'both'
# Change to script directory
script_dir = Path(__file__).parent
os.chdir(script_dir)
print(f"🚀 NotePad Build Script")
print(f"Target platform(s): {target.upper()}")
# Build for requested platform(s)
if target in ['linux', 'both', 'all']:
publish_platform('linux')
if target in ['windows', 'both', 'all']:
publish_platform('win')
print(f"\n{'='*60}")
print("🎉 All builds completed successfully!")
print(f"{'='*60}\n")
if __name__ == "__main__":
main()

9
pyproject.toml Normal file
View File

@ -0,0 +1,9 @@
[tool.poetry]
name = "notepad"
version = "0.1.0"
description = "Python dependencies for NotePad project"
authors = ["Greg Gauthier <gmgauthier@protonmail.com>"]
package-mode = false
[tool.poetry.dependencies]
python = ">=3.12"