Minimalistyczny launcher aplikacji Windows stworzony w WPF z architekturą MVVM. StartupControler umożliwia szybkie uruchamianie skonfigurowanych aplikacji z custom argumentami, elevation do uprawnień administratora, zamykanie procesów przez konfigurowalne nazwy, oraz pełne wsparcie dla motywów Light/Dark/System z persistent konfigurацją. System oferuje integrację z Windows Task Scheduler dla auto-startu z najwyższymi uprawnieniami, automatyczną detekcję najnowszej wersji Discord przez app-* folders, oraz wbudowany mechanizm wygasania licencji.

Główne funkcje
Launcher Aplikacji z Elevation
AppLauncherService obsługuje uruchamianie aplikacji z custom argumentami i opcjonalną elevation do admina przez Verb = "runas". Wspiera dynamiczne ścieżki z wildcardem app-* dla instalacji z wersjami, automatycznie wykrywając najnowszy folder. Specjalna integracja z DiscordResolver dla Discord.exe z automatic detection najnowszego app-* directory w %LOCALAPPDATA%/Discord.
Zarządzanie Procesami
ProcessService umożliwia zamykanie procesów przez konfigurowalne listy nazw procesów per aplikacja. Graceful shutdown attempt przez CloseMainWindow() z 3-sekundowym timeoutem, fallback do force kill przez Process.Kill(entireProcessTree: true) przy przekroczeniu. Case-insensitive deduplication nazw procesów, zwraca count zamkniętych procesów dla user feedback.
System Motywów Light/Dark/System
ThemeService implementuje dynamic theme switching przez ResourceDictionary merging z oddzielnych XAML files (Light.xaml, Dark.xaml). SystemDefault resolver wykrywa aktualny motyw systemu przez SystemThemeDetector. Theme persistence przez ConfigService z zapisem do config.json. Pełny clear poprzednich merged dictionaries przy zmianie dla clean state.
Edytor Konfiguracji z UI
AppsEditorWindow to modal dialog z master-detail UI: ListView z listą apps po lewej, detail panel z bindingami po prawej. Browse buttons dla ExePath i IconPath przez OpenFileDialog z filtrami. KillProcessNamesString property z parsing comma/newline separated text do List<string>. Clone pattern dla Safe editing bez mutacji live config do momentu Save.
Auto-Start przez Task Scheduler
StartupService oferuje dwa tryby auto-start: Task Scheduler z admin privileges (recommended) lub Registry-based dla non-admin. Task variant tworzy LogonTrigger z TaskRunLevel.Highest, LogonType.InteractiveToken, battery-friendly settings. Registry variant dodaje entry do HKCU\Software\Microsoft\Windows\CurrentVersion\Run z quoted ExePath.
License Management
LicenseService z UTC-based expiration check przeciwko hardcoded ExpirationUtc = 2026-12-31 23:59:59. EnforceWithUi() pokazuje Polish MessageBox "Licencja wygasła [date]. Aplikacja zostanie zamknięta. Skontaktuj sie z ogurem" i wywołuje Application.Current.Shutdown(). Alternative EnforceOrThrow() rzuca LicenseExpiredException dla programmatic handling.
Architektura
Aplikacja zorganizowana jako single-project WPF solution z wyraźnym separation of concerns przez foldery: Models dla domain objects, Services dla business logic, ViewModels dla MVVM orchestration, Views dla WPF windows.
Models - AppEntry reprezentuje single application z properties: Name, ExePath (wspiera env vars i app-* wildcards), Arguments, IconPath, RequiresAdmin bool, Enabled bool, List<string> KillProcessNames. AppConfig to root configuration object z List<AppEntry> Apps i GeneralConfig zawierającą Theme string. Serialization do JSON przez System.Text.Json.
Services - ConfigService zarządza JSON persistence w %APPDATA%/StartupControlelr/config.json, auto-copy z appsettings.json template przy pierwszym run. AppLauncherService z ResolveExe dla dynamic paths i Discord resolution. ProcessService dla graceful/force kill logic. StartupService dla Task Scheduler integration. ThemeService dla ResourceDictionary management. LicenseService dla expiration enforcement.
ViewModels - MainViewModel z CommunityToolkit.Mvvm ObservableObject, ObservableCollection
Views - MainWindow z frameless design, custom title bar z Minimize/Maximize/Close buttons, DragMove na MouseLeftButtonDown, ListView z app entries, context menu z Run/Run as Admin/Kill options, theme menu bound do ChangeThemeCmd. AppsEditorWindow z master-detail layout, Browse buttons dla file dialogs, PropertyChanged listener dla auto-close on DialogResult.
Komunikacja przez dependency injection z Microsoft.Extensions.DependencyInjection. Program.cs konfiguruje Generic Host z services jako singletons, ViewModels jako singletons (MainViewModel) lub construction-time DI (MainWindow). Theme application on startup z saved config.
Serwisy
ConfigService - persistence layer dla application configuration. Constructor tworzy %APPDATA%/StartupControlelr directory, sprawdza czy config.json exists, jeśli nie to kopiuje z appsettings.json template z AppContext.BaseDirectory lub tworzy empty. Load() deserializuje JSON z JsonSerializerOptions(JsonSerializerDefaults.Web) z WriteIndented=true, AllowTrailingCommas=true, ReadCommentHandling=Skip. Save() serializuje Config do JSON i zapisuje atomicznie przez WriteAllText.
AppLauncherService - launching logic z integration DiscordResolver w constructor. ResolveExe(AppEntry) expands environment variables przez Environment.ExpandEnvironmentVariables(), special case dla paths zawierających "Discord\app-" wywołuje DiscordResolver.ResolveDiscordExe(), generic app-* wildcard resolution przez scanning parent directory dla app-* folders ordered descending, picking newest, combining z filename. Launch(AppEntry, bool asAdmin) tworzy ProcessStartInfo z resolved exe path, arguments, UseShellExecute=true, Verb="runas" jeśli asAdmin, wywołuje Process.Start().
ProcessService - process termination service. Kill(AppEntry, TimeSpan? grace) iteruje przez app.KillProcessNames z Distinct case-insensitive, dla każdej nazwy wywołuje Process.GetProcessesByName(), dla każdego procesu próbuje CloseMainWindow() z WaitForExit na timeout (default 3s), jeśli timeout lub close failed wywołuje Kill(entireProcessTree: true). Zwraca count killed processes. Try-catch per process dla resilience.
StartupService - Windows auto-start management. IsEnabled() sprawdza czy TaskName="StartupControlelr_AutoStart" exists w TaskService. EnableWithAdminAtLogon() tworzy TaskDefinition z Description, Principal.RunLevel=Highest, LogonType=InteractiveToken, Settings z StartWhenAvailable=true, DisallowStartIfOnBatteries=false, StopIfGoingOnBatteries=false. Dodaje LogonTrigger. Action to ExecAction z ExePath resolved z Process.GetCurrentProcess().MainModule.FileName, WorkingDirectory z GetDirectoryName. Registers w RootFolder. Disable() deletes task. Alternative EnableUserRunNoAdmin() i DisableUserRunNoAdmin() dla Registry-based approach w HKCU Run key.
ThemeService - implements IThemeService interface. ApplyTheme(AppTheme) sprawdza czy theme==SystemDefault, jeśli tak resolve przez SystemThemeDetector.GetSystemTheme(). Map theme enum do relative path "Themes/Light.xaml" lub "Themes/Dark.xaml". Tworzy new ResourceDictionary z Source=Uri. Clears Application.Current.Resources.MergedDictionaries, adds new theme dictionary. Updates CurrentTheme property. Clear() method usuwa wszystkie merged dictionaries i resetuje do SystemDefault.
LicenseService - license enforcement logic z hardcoded ExpirationUtc property = new DateTime(2026, 12, 31, 23, 59, 59, DateTimeKind.Utc). IsExpiredUtc(DateTime? nowUtc) porównuje now (default DateTime.UtcNow) z ExpirationUtc. EnforceWithUi() sprawdza expiration, loguje warning z NLog, converts ExpirationUtc do local time, pokazuje MessageBox z Polish text "Licencja wygasła {expLocal:d}. Aplikacja zostanie zamknięta. Skontaktuj sie z ogurem", wywołuje Application.Current.Shutdown(), zwraca false. EnforceOrThrow() variant rzuca LicenseExpiredException z ExpirationUtc.
DiscordResolver - helper service dla Discord path resolution. ResolveDiscordExe() expands "%LOCALAPPDATA%\Discord" environment variable, sprawdza czy directory exists, gets subdirectories matching "app-*" pattern ordered descending (newest first), iteruje i sprawdza czy Discord.exe exists w każdym directory, zwraca pierwszy match lub null.
ViewModels i Commands
MainViewModel - centralny orchestrator aplikacji inheriting z CommunityToolkit.Mvvm ObservableObject. Properties: ObservableCollection<AppEntry> Apps populated z Config.Apps filtered gdzie Enabled==true, Status partial observable property z attribute [ObservableProperty], CurrentTheme z manual SetProperty, BuildInfo string z GetBuildDate() returning assembly LastWriteTime formatted "yyyy-MM-dd HH:mm". Commands: RunCmd jako RelayCommand
Launch(AppEntry, bool) method wywołuje _launcher.Launch() w try-catch, updates Status z app.Name i (admin) suffix, loguje success z NLog. Catch updates Status z error message, loguje exception. Kill(AppEntry) wywołuje _process.Kill(), updates Status z count i app.Name, loguje count. OpenEditor() tworzy new AppsEditorViewModel z _config dependency, tworzy AppsEditorWindow z DataContext=vm, wywołuje ShowDialog(), jeśli result==true wywołuje ReloadApps() i updates Status "Zapisano konfigurację".
ChangeTheme(string? themeName) parsuje string do AppTheme enum przez Enum.TryParse, jeśli success wywołuje _themeService.ApplyTheme(), updates CurrentTheme property, updates Config.General.Theme, saves config, updates Status "Zmieniono motyw na {theme}", loguje info. Else updates Status "Nieznany motyw" i loguje warning.
ReloadApps() helper clears Apps ObservableCollection, re-populates z Config.Apps gdzie Enabled==true, wywołuje CollectionViewSource.GetDefaultView(Apps)?.Refresh() dla UI sync.
AppsEditorViewModel - editor dialog ViewModel z CommunityToolkit.Mvvm partial class. Properties: ObservableCollection<AppEntry> Apps populated z Config.Apps clones, Selected observable property, DialogResult nullable bool observable property dla window close signaling. KillProcessNamesString computed property z getter joining Selected.KillProcessNames przez Environment.NewLine, setter parsing input przez Split na ',', ';', '\r', '\n' separators z RemoveEmptyEntries, Trim, Length>0 filter, Distinct case-insensitive, ToList(), calls OnPropertyChanged.
Commands: AddCmd tworzy new AppEntry z defaults "Nowa aplikacja" Enabled=true, adds do Apps, sets Selected=item. RemoveCmd z CanExecute Selected!=null removes Selected, updates Selected do previous index lub null. SaveCmd clones all Apps do Config.Apps przez Select(Clone), calls _config.Save(), sets DialogResult=true. CancelCmd sets DialogResult=false. Clone(AppEntry) static helper tworzy new AppEntry z copied properties i ToList() dla KillProcessNames.
PropertyChanged subscription w constructor wywołuje RemoveCmd.NotifyCanExecuteChanged() przy Selected property change dla CanExecute update.
Windows Implementation
MainWindow - główne okno aplikacji bez WindowStyle, AllowsTransparency=true, custom title bar z border containing Minimize/Maximize/Close buttons. Title bar buttons: Minimize_Click sets WindowState=Minimized, Maximize_Click toggles Normal/Maximized, Close_Click wywołuje Close(). Window_MouseLeftButtonDown z check ChangedButton==Left wywołuje DragMove() dla window dragging.
Content zawiera theme menu z RadioButton per AppTheme bound do CurrentTheme property z ChangeThemeCmd command. ListView z ItemsSource=Apps, DisplayMemberPath="Name". Context menu z MenuItem per action: Run bound do RunCmd, Run as Admin do RunAdminCmd, Kill do KillProcessNames. Status bar z TextBlock bound do Status property. Build info TextBlock bound do BuildInfo.
AppsEditorWindow - modal dialog window z ShowDialog() call z MainViewModel. Loaded event handler z OnLoaded subscribuje PropertyChanged z DataContext, sprawdza PropertyName==DialogResult, jeśli AppsEditorViewModel.DialogResult.HasValue sets window DialogResult i calls Close() dla automatic modal dismiss.
Layout: Grid z ColumnDefinitions 200,*. Left column: ListView ItemsSource=Apps, SelectedItem=Selected. Right column: detail panel z TextBox per property bindings: Name, ExePath z Browse button calling BrowseExePath_Click, Arguments, IconPath z Browse button calling BrowseIconPath_Click. CheckBox dla RequiresAdmin i Enabled. TextBox Mode=AcceptsReturn dla KillProcessNamesString. Buttons panel: Add/Remove/Save/Cancel bound do respective commands.
BrowseExePath_Click tworzy OpenFileDialog z Title "Wybierz plik EXE lub skrót", Filter dla exe/lnk files, CheckFileExists=true. ShowDialog() result==true updates Selected.ExePath z dlg.FileName. BrowseIconPath_Click similar z Filter dla ico/exe/dll/lnk files.
Configuration i Persistence
Config Location - %APPDATA%/StartupControlelr/config.json dla user-specific storage. Directory auto-created przez ConfigService constructor z Directory.CreateDirectory(_appDir).
Template Fallback - przy pierwszym uruchomieniu jeśli config.json nie exists, ConfigService sprawdza AppContext.BaseDirectory/appsettings.json, jeśli exists kopiuje do _configPath. Else calls Save() dla empty default config. To umożliwia shipping aplikacji z pre-configured appsettings.json jako template.
JSON Serialization - System.Text.Json z JsonSerializerDefaults.Web dla camelCase naming, WriteIndented=true dla human-readable output, AllowTrailingCommas=true dla flexibility, ReadCommentHandling=Skip dla comments support w JSON. Deserialize do AppConfig z fallback do new() przy null.
Example Config Structure:
{
"general": {
"theme": "Dark"
},
"apps": [
{
"name": "Discord",
"exePath": "%LOCALAPPDATA%\\Discord\\app-*\\Discord.exe",
"arguments": "",
"iconPath": "%LOCALAPPDATA%\\Discord\\app-*\\Discord.exe",
"requiresAdmin": false,
"enabled": true,
"killProcessNames": ["Discord"]
},
{
"name": "Visual Studio Code",
"exePath": "%LOCALAPPDATA%\\Programs\\Microsoft VS Code\\Code.exe",
"arguments": "--new-window",
"iconPath": "",
"requiresAdmin": false,
"enabled": true,
"killProcessNames": ["Code"]
}
]
}
Theme Persistence - przy startup Program.cs resolve ThemeService i ConfigService, parsuje Config.General.Theme string do AppTheme enum, wywołuje ApplyTheme(). MainViewModel.ChangeTheme updates Config.General.Theme i saves.
App- Wildcard Resolution* - ExePath może zawierać app-* pattern dla applications z version-based folders. ResolveExe() wykrywa pattern, gets parent directory, scans dla matching subdirectories OrderByDescending (newest first), checks file existence, returns first match. Fallback do original path jeśli resolution fails.
Dependency Injection i Hosting
Program.cs Setup - Generic Host z CreateDefaultBuilder(args). ConfigureServices rejestruje wszystkie services jako singletons: ConfigService, DiscordResolver, AppLauncherService, ProcessService, StartupService, LicenseService, IThemeService. MainViewModel jako singleton, MainWindow jako transient z factory pattern tworzącym window i setting DataContext z resolved MainViewModel.
Service Resolution - po Build() host resolve ThemeService i ConfigService dla theme initialization. MainWindow resolved i passed do app.Run(). MainWindow constructor nie tworzy własnych dependencies (dead code visible z new operators), actual DI odbywa się przez Program.cs factory.
Lifetime Management - services jako singletons zapewniają single instance per application lifetime. MainWindow jako transient dla flexibility gdyby multiple windows były needed w przyszłości. ViewModels potential dla transient but currently singleton przez Program.cs.
Auto-Start Configuration
Task Scheduler Approach (Recommended) - EnableWithAdminAtLogon() tworzy scheduled task z LogonTrigger firing at user logon. Principal.RunLevel=Highest zapewnia admin elevation automatycznie bez UAC prompt przy każdym logon. LogonType=InteractiveToken dla user context. Settings: StartWhenAvailable=true retry jeśli computer was off podczas trigger time, DisallowStartIfOnBatteries=false i StopIfGoingOnBatteries=false dla laptops. ExecAction z full ExePath i WorkingDirectory.
Registry Approach (Fallback) - EnableUserRunNoAdmin() dodaje entry do HKCU\Software\Microsoft\Windows\CurrentVersion\Run z value "{ExePath}" quoted dla spaces handling. Runs z normal user privileges bez elevation. Simple approach ale no admin rights. DisableUserRunNoAdmin() deletes registry value jeśli exists.
Detection - IsEnabled() sprawdza czy scheduled task exists w TaskService.FindTask(). No check dla registry variant - potential enhancement.
Theme System
Theme Files Structure - Themes/Light.xaml i Themes/Dark.xaml jako separate ResourceDictionary XAML files. Każdy definiuje complete set color resources, brush resources, style overrides. Applied przez merging do Application.Current.Resources.MergedDictionaries.
Application Logic - ThemeService.ApplyTheme() clears wszystkie existing merged dictionaries przed adding new theme. To zapewnia clean state bez leftover resources z previous theme. CurrentTheme property tracked dla state management.
System Theme Detection - AppTheme.SystemDefault resolve przez SystemThemeDetector.GetSystemTheme() który query system registry dla current Windows theme preference. Automatic fallback do Light lub Dark based on OS setting.
Persistence - CurrentTheme saved do Config.General.Theme jako string. Loaded on startup w Program.cs before window creation. Updates przez MainViewModel.ChangeTheme command z save do config.
License Enforcement
Expiration Date - hardcoded w LicenseService jako new DateTime(2026, 12, 31, 23, 59, 59, DateTimeKind.Utc). UTC-based dla timezone-independent comparison. ExpirationUtc property publicly accessible dla display purposes.
Enforcement Flow - EnforceWithUi() sprawdza IsExpiredUtc() comparing DateTime.UtcNow > ExpirationUtc. Jeśli expired: converts ExpirationUtc do local time przez ToLocalTime(), loguje NLog warning z UTC i local timestamps, shows MessageBox z Polish text including formatted local expiration date, wywołuje Application.Current.Shutdown() dla graceful app termination.
Exception Variant - EnforceOrThrow() dla programmatic handling bez UI. Sprawdza expiration, loguje warning, rzuca LicenseExpiredException z ExpirationUtc property. Exception może być caught w higher layers dla custom handling.
Integration Point - currently nie wywołane automatycznie w Program.cs. Potential integration w startup before window creation lub w MainViewModel constructor. Dead code currently present w services.
Technologie
- Framework: .NET 8.0
- Język: C# 12 z nullable reference types
- UI: WPF z MVVM pattern, frameless window design
- MVVM: CommunityToolkit.Mvvm (ObservableObject, ObservableProperty, RelayCommand partial classes)
- DI/Hosting: Microsoft.Extensions (DependencyInjection, Hosting)
- Configuration: System.Text.Json z JsonSerializerDefaults.Web
- Logging: NLog structured logging z class-specific loggers
- Task Scheduler: TaskScheduler NuGet package dla scheduled task management
- Platform: Windows API (Registry, Task Scheduler, Process Management)
Wymagania
- OS: Windows 10/11
- Runtime: .NET 8.0 Desktop Runtime
- Uprawnienia: Admin rights zalecane dla Task Scheduler integration i run as admin features
- Dependencies: TaskScheduler NuGet, CommunityToolkit.Mvvm, NLog
Użycie
- Uruchom aplikację - pokazuje MainWindow z configured apps
- Apps list pokazuje tylko entries gdzie Enabled=true
- Kliknij app entry, użyj context menu lub buttons:
- Run - uruchamia z normal privileges
- Run as Admin - uruchamia elevated, UAC prompt
- Kill - zamyka wszystkie processes z KillProcessNames list
- Status bar shows operation feedback
- Theme menu w top-left: wybierz Light/Dark/System Default
- Click "Editor" button dla AppsEditorWindow
- W editorze:
- Add tworzy new entry z defaults
- Select entry z listy, edit properties po prawej
- Browse buttons dla ExePath i IconPath file selection
- KillProcessNames jako multiline text, comma/newline separated
- CheckBox dla RequiresAdmin i Enabled flags
- Save applies changes, Cancel discards
- Build info w bottom-right shows compilation timestamp
Known Issues i Limitations
Discord Path Hardcoded Logic - assumes standard %LOCALAPPDATA%/Discord installation path. Custom Discord installs w innych lokalizacjach require manual ExePath configuration. DiscordResolver może nie wykryć non-standard paths.
Process Kill Timeout - 3-second graceful shutdown timeout może być insufficient dla heavy applications z long cleanup procedures. No user-configurable timeout setting. Force kill may cause data loss jeśli application nie saved state.
Theme Switching Artifacts - clearing MergedDictionaries i adding new może cause brief visual glitches. Existing windows may need manual style refresh dla complete theme application. No animation during theme transition.
Single Instance - no enforcement of single application instance. Multiple copies can run simultaneously z potential conflicts w config file access. No file locking lub mutex implementation.
License Enforcement Not Active - LicenseService registered w DI ale EnforceWithUi() nie wywołane w startup flow. License expiration currently nie blokuje application usage. Dead code requiring integration.
Task Scheduler Permissions - EnableWithAdminAtLogon() requires elevated privileges do create scheduled task. No graceful fallback z error handling jeśli user declines UAC prompt. Silent failure possible.
Config Migration - no automatic migration logic dla config schema changes. Breaking changes w AppConfig structure require manual config.json edits lub deletion for recreation from template.
Roadmap Potential
Multi-Instance Detection - mutex-based single instance enforcement z activation of existing instance jeśli already running. Prevent config conflicts.
License Integration - actual enforcement of LicenseService.EnforceWithUi() w Program.cs startup before window creation. Grace period warnings before expiration.
Configurable Timeouts - user-adjustable process kill grace period w config. Per-app timeout overrides.
Theme Transition Animations - smooth fade animations during theme switches. Resource preloading dla glitch-free transitions.
Tray Icon Support - minimize to system tray, quick launch menu, persistent background running.
Process Monitoring - real-time display czy configured apps are running, memory/CPU usage stats, restart on crash functionality.
Import/Export - backup/restore functionality dla config.json, sharing app configurations between machines.
Licencja
License type not specified in source files. License expiration enforcement suggests proprietary/commercial usage model.
Wygasa: 31 grudnia 2026
Kontakt
- Website: github.com/ishkabar/StartupControler
- Email: kontakt@ogur.dev
- Discord: 7cd_
- LinkedIn: Dominik Karczewski