From 01acd4d6e64c5a269cb4f027e1088d1b08073826 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 14 Feb 2026 14:15:52 +0000 Subject: [PATCH] notepad initial commit --- .gitea/workflows/release.yaml | 107 ++++++++++ .gitignore | 8 + NotePad.sln | 18 ++ NotePad/App.axaml | 10 + NotePad/App.axaml.cs | 23 +++ NotePad/MainWindow.axaml | 74 +++++++ NotePad/MainWindow.axaml.cs | 374 ++++++++++++++++++++++++++++++++++ NotePad/NotePad.csproj | 21 ++ NotePad/Program.cs | 21 ++ NotePad/app.manifest | 18 ++ NotePad/build-appimage.sh | 106 ++++++++++ NotePad/install.sh | 73 +++++++ NotePad/publish.py | 248 ++++++++++++++++++++++ NotePad/setup.iss | 54 +++++ NotePad/uninstall.sh | 53 +++++ publish.py | 85 ++++++++ pyproject.toml | 9 + 17 files changed, 1302 insertions(+) create mode 100644 .gitea/workflows/release.yaml create mode 100644 .gitignore create mode 100644 NotePad.sln create mode 100644 NotePad/App.axaml create mode 100644 NotePad/App.axaml.cs create mode 100644 NotePad/MainWindow.axaml create mode 100644 NotePad/MainWindow.axaml.cs create mode 100644 NotePad/NotePad.csproj create mode 100644 NotePad/Program.cs create mode 100644 NotePad/app.manifest create mode 100755 NotePad/build-appimage.sh create mode 100755 NotePad/install.sh create mode 100644 NotePad/publish.py create mode 100644 NotePad/setup.iss create mode 100755 NotePad/uninstall.sh create mode 100755 publish.py create mode 100644 pyproject.toml diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..20d1923 --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..401262a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea/ +bin/ +obj/ +/packages/ +publish/ +riderModule.iml +/_ReSharper.Caches/ +poetry.lock diff --git a/NotePad.sln b/NotePad.sln new file mode 100644 index 0000000..fd9c29e --- /dev/null +++ b/NotePad.sln @@ -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 diff --git a/NotePad/App.axaml b/NotePad/App.axaml new file mode 100644 index 0000000..8aa2a95 --- /dev/null +++ b/NotePad/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/NotePad/App.axaml.cs b/NotePad/App.axaml.cs new file mode 100644 index 0000000..90cd562 --- /dev/null +++ b/NotePad/App.axaml.cs @@ -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(); + } +} \ No newline at end of file diff --git a/NotePad/MainWindow.axaml b/NotePad/MainWindow.axaml new file mode 100644 index 0000000..66c6abe --- /dev/null +++ b/NotePad/MainWindow.axaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NotePad/MainWindow.axaml.cs b/NotePad/MainWindow.axaml.cs new file mode 100644 index 0000000..1c3da27 --- /dev/null +++ b/NotePad/MainWindow.axaml.cs @@ -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("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("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 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("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("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("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("WordWrapCheckBox")!; + var textBox = this.FindControl("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("EditorTextBox")!; + textBox.Undo(); + } + + private void OnCutClick(object? sender, RoutedEventArgs e) + { + var textBox = this.FindControl("EditorTextBox")!; + textBox.Cut(); + } + + private void OnCopyClick(object? sender, RoutedEventArgs e) + { + var textBox = this.FindControl("EditorTextBox")!; + textBox.Copy(); + } + + private void OnPasteClick(object? sender, RoutedEventArgs e) + { + var textBox = this.FindControl("EditorTextBox")!; + textBox.Paste(); + } + + private void OnDeleteClick(object? sender, RoutedEventArgs e) + { + var textBox = this.FindControl("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("EditorTextBox")!; + textBox.SelectAll(); + } + + private void OnTimeDateClick(object? sender, RoutedEventArgs e) + { + var textBox = this.FindControl("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); + } + } +} \ No newline at end of file diff --git a/NotePad/NotePad.csproj b/NotePad/NotePad.csproj new file mode 100644 index 0000000..68d7048 --- /dev/null +++ b/NotePad/NotePad.csproj @@ -0,0 +1,21 @@ + + + WinExe + net8.0 + enable + app.manifest + true + + + + + + + + + + None + All + + + diff --git a/NotePad/Program.cs b/NotePad/Program.cs new file mode 100644 index 0000000..61746f1 --- /dev/null +++ b/NotePad/Program.cs @@ -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() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} \ No newline at end of file diff --git a/NotePad/app.manifest b/NotePad/app.manifest new file mode 100644 index 0000000..6f49037 --- /dev/null +++ b/NotePad/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/NotePad/build-appimage.sh b/NotePad/build-appimage.sh new file mode 100755 index 0000000..4d8ad6b --- /dev/null +++ b/NotePad/build-appimage.sh @@ -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 + + + N + +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" diff --git a/NotePad/install.sh b/NotePad/install.sh new file mode 100755 index 0000000..302d9ae --- /dev/null +++ b/NotePad/install.sh @@ -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 "" diff --git a/NotePad/publish.py b/NotePad/publish.py new file mode 100644 index 0000000..b1f59b5 --- /dev/null +++ b/NotePad/publish.py @@ -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() \ No newline at end of file diff --git a/NotePad/setup.iss b/NotePad/setup.iss new file mode 100644 index 0000000..9e7f49e --- /dev/null +++ b/NotePad/setup.iss @@ -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 diff --git a/NotePad/uninstall.sh b/NotePad/uninstall.sh new file mode 100755 index 0000000..5b833f6 --- /dev/null +++ b/NotePad/uninstall.sh @@ -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 "" diff --git a/publish.py b/publish.py new file mode 100755 index 0000000..06802b9 --- /dev/null +++ b/publish.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a51399b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[tool.poetry] +name = "notepad" +version = "0.1.0" +description = "Python dependencies for NotePad project" +authors = ["Greg Gauthier "] +package-mode = false + +[tool.poetry.dependencies] +python = ">=3.12"