Repository: JocysCom/FocusLogger
Branch: main
Commit: bd497bb745a6
Files: 88
Total size: 826.0 KB
Directory structure:
gitextract_pkit4a3b/
├── .ai/
│ ├── ReadMe.md
│ ├── coding-guideline.instructions.md
│ ├── instructions.md
│ ├── repository-analysis.instructions.md
│ └── skills/
│ └── ai-self-improvement/
│ ├── SKILL.md
│ └── scripts/
│ └── Sync-AgentAssets.ps1
├── .claude/
│ ├── coding-guideline.instructions.md
│ ├── instructions.md
│ ├── repository-analysis.instructions.md
│ └── skills/
│ └── ai-self-improvement/
│ ├── SKILL.md
│ └── scripts/
│ └── Sync-AgentAssets.ps1
├── .editorconfig
├── .github/
│ ├── copilot-instructions.md
│ ├── instructions/
│ │ ├── coding-guideline.instructions.md
│ │ └── repository-analysis.instructions.md
│ └── skills/
│ └── ai-self-improvement/
│ ├── SKILL.md
│ └── scripts/
│ └── Sync-AgentAssets.ps1
├── .gitignore
├── .markdownlint.json
├── Documents/
│ ├── App_1_Sign.ps1
│ ├── App_2_Zip.ps1
│ ├── Take_Screenshot.ps1
│ └── Take_Screenshot.ps1.cs
├── FocusLogger/
│ ├── App.xaml
│ ├── App.xaml.cs
│ ├── AssemblyInfo.cs
│ ├── Common/
│ │ ├── DataItem.cs
│ │ ├── DataItemType.cs
│ │ └── NativeMethods.cs
│ ├── Controls/
│ │ ├── DataListControl.xaml
│ │ └── DataListControl.xaml.cs
│ ├── JocysCom/
│ │ ├── Collections/
│ │ │ └── CollectionsHelper.cs
│ │ ├── Common/
│ │ │ └── Helper.cs
│ │ ├── ComponentModel/
│ │ │ ├── BindingListInvoked.cs
│ │ │ ├── NotifyPropertyChanged.cs
│ │ │ ├── PropertyComparer.cs
│ │ │ └── SortableBindingList.cs
│ │ ├── Configuration/
│ │ │ ├── Arguments.cs
│ │ │ ├── AssemblyInfo.cs
│ │ │ ├── ISettingsData.cs
│ │ │ ├── ISettingsFileItem.cs
│ │ │ ├── ISettingsItem.cs
│ │ │ ├── ISettingsListFileItem.cs
│ │ │ ├── SettingsData.cs
│ │ │ ├── SettingsHelper.cs
│ │ │ ├── SettingsItem.cs
│ │ │ └── SettingsParser.cs
│ │ ├── Controls/
│ │ │ ├── ControlsHelper.WPF.UseWindowsForms.cs
│ │ │ ├── ControlsHelper.WPF.cs
│ │ │ ├── ControlsHelper.cs
│ │ │ ├── InfoControl.xaml
│ │ │ ├── InfoControl.xaml.cs
│ │ │ ├── InfoHelpProvider.cs
│ │ │ ├── InitHelper.cs
│ │ │ ├── ItemFormattingConverter.cs
│ │ │ ├── MessageBoxWindow.xaml
│ │ │ ├── MessageBoxWindow.xaml.cs
│ │ │ ├── TabIndexConverter.cs
│ │ │ ├── Themes/
│ │ │ │ ├── Convert_SVG_to_XAML.ps1
│ │ │ │ ├── Default.xaml
│ │ │ │ ├── Default_MultiReplace.ps1
│ │ │ │ ├── Icons.xaml
│ │ │ │ └── Icons.xaml.cs
│ │ │ └── ToolStripBorderlessRenderer.cs
│ │ ├── Data/
│ │ │ └── SqlHelper.Types.cs
│ │ ├── IO/
│ │ │ └── PathHelper.cs
│ │ ├── MakeLinks_Ref.ps1
│ │ ├── Runtime/
│ │ │ ├── RuntimeHelper.cs
│ │ │ └── Serializer.cs
│ │ └── Text/
│ │ └── Helper.cs
│ ├── JocysCom.FocusLogger.csproj
│ ├── MainWindow.xaml
│ ├── MainWindow.xaml.cs
│ └── Resources/
│ ├── AiAnalysisPrompt.md
│ └── Icons/
│ ├── Convert_SVG_to_XAML.ps1
│ ├── IconExperience.License.txt
│ ├── Icons_Default.xaml
│ └── Icons_Default.xaml.cs
├── FocusLogger.Tests/
│ ├── CsvExportTests.cs
│ ├── JocysCom.FocusLogger.Tests.csproj
│ └── UIAutomationTests.cs
├── JocysCom.FocusLogger.slnx
├── LICENSE
├── README.md
├── Resources/
│ └── ZipFiles.ps1
├── SECURITY.md
├── Settings.XamlStyler
└── Solution_Cleanup.ps1
================================================
FILE CONTENTS
================================================
================================================
FILE: .ai/ReadMe.md
================================================
## Files
- `developer-info.md` - Developer information about the solution, which will be used when creating the repository-analysis.instructions.md file.
- `instructions.md` - Main AI Agent System Message instructions used by AI Agents, like CLINE or CoPilot.
- `repository-analysis.instructions.md` - Autogenerated Repository Analysis file that is supplied to the AI Agent as a System message after instructions.md.
- `repository-analysis.prompt.md` - User Message prompt that instructs AI to create repository-analysis.instructions.md.
- `Update-AgentInstructions.ps1` - Sets all `*instruction.md` files as AI agent system message instruction files. Used by AI agents, like CLINE or Copilot.
## Custom instructions: GitHub Copilot
With GitHub Copilot, you can receive chat responses tailored to your team'ss workflow, preferred tools, and project specifics.
Instead of adding this contextual detail to each chat query, you can create a file that supplies this information automatically.
While this additional context won't appear in the chat, it is available to GitHub Copilot, allowing it to generate more accurate and relevant responses.
**How to Enable Custom Instructions**
Enable the feature via Tools > Options > GitHub > Copilot > and check (Preview) Enable custom instructions to be loaded from .github/copilot-instructions.md files and added to requests.
Add copilot-instruction.md in the root of your respository inside the .github file, create the file if it doesn't already exist.
GitHub Copilot Enable Custom Instructions
Learn more about creating custom instructions:
https://docs.github.com/en/enterprise-cloud@latest/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot
## Custom instructions: CLINE
All CLINE instructions are stored inside the .clinerules\ folder.
All \*.md files will be loaded as system messages.
## Custom instructions: OpenAI Codex
Instructions are stored in `%USERPROFILE%\.codex\instructions.md` file.
## User Instructions to create `repository-analysis.md`
Note: Switch AI agent to `Plan` mode before submitting this message. This will yield better results.
### LLM Selection Guideline
If your deployment environment allows, prefer a newer, higher-capacity model—such as Anthropic Claude Sonnet 4 or Google Gemini 2.5 Pro - instead of the current OpenAI GPT 4 lineup. These frontier models typically deliver stronger reasoning, broader context windows, and more consistent output quality on complex tasks.
### Install the Mermaid preview plug-in
1. Open Extensions (Ctrl + Shift + X).
2. Search: Markdown Preview Mermaid Support (publisher: Matt Bierner).
3. Click Install – or hit Ctrl + P.
### Open Preview
1. Open the menu on the tab header of the file.
2. Click "Open Preview" – or hit Ctrl + Shift + V.
### Re Open Preview
1. Open the menu on the tab header of the file.
2. Click "Reopen Editor With..."
3. Click "Markdown Preview (Built-In)"
================================================
FILE: .ai/coding-guideline.instructions.md
================================================
# Coding Guidelines
- If the qdrant-mcp-server is running, use it for all permanent memory operations (e.g. storing user information).
- After making changes, ALWAYS start a new server for testing.
- Kill all existing related servers from previous testing before starting a new server.
- Prefer the simplest viable solution; avoid over-engineering.
- Do not add broad try/catch or wrapper layers unless required by a failing test or explicit requirement; if you catch, rethrow to preserve the stack.
- Before writing new code, actively look for existing utilities or functions that can be reused instead of duplicated.
- New helper methods or classes must be justified with a clear, documented need for functionality that is unavailable elsewhere in the codebase.
- Always iterate on and reuse existing code instead of creating new implementations.
- Avoid adding layers of abstraction that do not deliver clear value.
- Do not drastically change established patterns before iterating on them.
- No duplication / SSOT: update or move existing code instead of adding parallel implementations. If you introduce a replacement, remove the old one **in the same change**.
- Write code that accounts for different environments (dev, test, and prod).
- Only modify what is explicitly requested or clearly necessary; do **not** create new files or modules unless explicitly requested.
- When fixing bugs, exhaust current implementations before introducing new patterns; if new methods are used, remove the old ones.
- Keep the codebase clean and organized.
- Avoid one-off scripts unless absolutely necessary.
- Use mocks only for tests, not for dev or prod.
- Never add stubbing or fake data in dev or prod environments.
- Never overwrite the .env file without explicit confirmation.
- Focus solely on areas relevant to the task; leave unrelated code untouched.
- Write thorough tests for all major functionality.
- Avoid major changes to the existing architecture unless explicitly instructed.
- Always consider the impact on other methods and areas of the code.
- Prefer to wrap long lines for better readability.
- Preserve existing formatting; limit formatting to lines you changed and match surrounding style. Also remove any unused imports/usings or dead code **introduced by your edits**.
- Stability primitives (repeatability > cleverness): when the repo provides an established way to perform an operation (repo-owned script, documented command snippet, standard PS1 under .ai/Scripts), treat it as the single source of truth. Use it verbatim instead of synthesizing “equivalent” commands. Only deviate when explicitly asked or when the primitive is proven broken in this environment (and then fix the primitive, not invent a parallel path).
- No code file that you **create or modify** may exceed **6000 tokens (~24 KB)** once your changes are applied.
- If your changes alone would push the file past this limit, either trim the change or ask for explicit permission to refactor; do **not** alter unrelated code solely to meet the limit.
- Existing oversized files are left untouched unless the user explicitly requests a refactor.
## Source Control Conventions
Naming conventions for issues, branches, and pull requests.
### Categories
| Category | Use for |
|----------|---------|
| `FEAT` | New features |
| `FIX` | Bug fixes |
| `TECH` | Infrastructure, dependencies, refactoring |
| `DOCS` | Documentation |
### Naming Patterns
| Item | Pattern | Example |
|------------|---------|---------|
| **Issue** | `{CATEGORY}: {Description}` | `FEAT: Download logs to CSV` |
| **Branch** | `{CATEGORY}-{issue#}-{lowercase-dashed-name}` | `FEAT-21-download-logs-to-csv` |
| **PR** | `PR: #{issue#}: {CATEGORY}: {Description}` | `PR: #21: FEAT: Download logs to CSV` |
- Branch names: lowercase, words separated by dashes, non-ASCII replaced with a single dash, derived from issue title but may be shortened.
- PR body must reference the issue with `Closes #{issue#}`.
- Merge via PR only — no direct pushes to `main` or `master`.
- Feature branches are always created from `main` or `master`. Never merge one feature branch into another — only merge `main` if you need to catch up.
- Always create branches from latest origin/main or origin/main, never merge sub-branches, confirm before risky git ops.
- Do not add Co-Authored-By {AI model}/{AI company} lines to commits.
- Never close issues before a release is published with the fix.
Use the following guidelines:
1. Doc Comment Enhancement for IntelliSense
- Replace or augment simple comments with relevant doc comment syntax that is supported by IntelliSense as needed.
- Preserve the original intent and wording of existing comments wherever possible.
2. Code Layout for Clarity
- Place the most important or user-editable sections at the top if logically appropriate.
- Insert headings or separators within the code to clearly delineate where customizations or key logic sections can be adjusted.
3. No Extraneous Code Comments
- Do not include "one-off" or user-directed commentary in the code.
- Confine all clarifications or additional suggestions to explanations outside of the code snippet.
4. Avoid Outdated or Deprecated Methods
- Refrain from introducing or relying on obsolete or deprecated methods and libraries.
- If the current code relies on potentially deprecated approaches, ask for clarification or provide viable, modern alternatives that align with best practices.
5. Testing and Validation
- Suggest running unit tests or simulations on the modified segments to confirm that the changes fix the issue without impacting overall functionality.
- Ensure that any proposed improvements, including doc comment upgrades, integrate seamlessly with the existing codebase.
- After all code modifications, navigate to the affected project directory and build C# then Angular to confirm the application compiles without errors:
cd {PROJECT} && dotnet build {PROJECT}.csproj
cd {PROJECT}/ClientApp && ng build
- Run relevant unit tests if code changes affect core logic.
- If the developer certificate is not trusted, then execute: dotnet dev-certs https --trust
- To launch project use: dotnet watch run --project {PROJECT}/{PROJECT}.csproj --launch-profile "{PROJECT} (NG Build)"
6. Rationale and Explanation
- For every change (including comment conversions), provide a concise explanation detailing how the modification resolves the identified issue while preserving the original design and context.
- Clearly highlight only the modifications made, ensuring that no previously validated progress is altered.
- NOTE: Summarize reasoning for the user, but do NOT expose full chain-of-thought. Keep internal deliberations internal; surface only the concise rationale needed to justify each change.
7. Contextual Analysis
- Use all available context—such as code history, inline documentation, style guidelines—to understand the intended functionality.
- When inspecting an existing file for understanding, prefer reading the whole file in a
single `read_file` call when it comfortably fits in context; switch to targeted slices
only when the file is too large, the tool truncates it, or a specific anchor line is
already known.
- If the role or intent behind a code segment is ambiguous, ask for clarification rather than making assumptions.
8. Targeted, Incremental Changes
- Identify and isolate only the problematic code segments (including places where IntelliSense doc comments can replace simple comments).
- Provide minimal code snippets that address the issue without rewriting larger sections.
- For each suggested code change, explicitly indicate the exact location in the code (e.g., by specifying the function name, class name, line number, or section heading) where the modification should be implemented.
9. Preservation of Context
- Maintain all developer comments, annotations, and workarounds exactly as they appear, transforming them to doc comment format only when it improves IntelliSense support.
- Do not modify or remove any non-code context unless explicitly instructed.
- Avoid introducing new, irrelevant comments in the code.
10. Launching {PROJECT} Correctly:
- Navigate to the {PROJECT} project folder.
- Run the following command to launch the project with live reload and proper debugging configuration:
dotnet watch run --launch-profile "{PROJECT} (NG Build)" --project {PROJECT}/{PROJECT}.csproj
- This command will start the {PROJECT} project on the designated debugging session URL.
- Ensure that any previous {PROJECT} instances are terminated before running this command.
================================================
FILE: .ai/instructions.md
================================================
## Role
Your role is to analyze and improve code by making only localized, targeted changes. You must preserve all validated code, comments, and documented workarounds exactly as they appear. Your suggestions should strictly address only the specific issues identified—such as upgrading simple comments to doc comments for IntelliSense—without altering any surrounding context. Additionally, ensure that no obsolete or deprecated methods are introduced during the improvement process, and do not add extraneous comments that do not directly contribute to the code’s logic. Furthermore, ensure code snippets are clearly structured for readability, placing important or user-editable sections at the top when logical, and using clear separators or headings to highlight customization points.
Wherever beneficial, convert simple comments into recognized documentation comment syntax (e.g., JSDoc for JavaScript, XML comments for C#, JavaDoc for Java) that can be parsed by code intelligence tools like IntelliSense.
Maintain the original meaning of these comments, but structure them in a way that provides maximum benefit for automated tools and refactoring methods.
Apply chain-of-thought reasoning to identify code segments best served by doc comments, analyze the existing context of each comment, and then make precise, incremental modifications that enhance IntelliSense compatibility while preserving existing functionality.
## Output
Wrap any and all code—including regular code snippets, inline code segments, outputs, pseudocode, or any text that represents code—in Markdown code blocks with a language identifier (e.g., ```typescript, ```powershell).
================================================
FILE: .ai/repository-analysis.instructions.md
================================================
# Repository Analysis
## 1. Repository Overview
This document provides a factual reference for the Jocys.com FocusLogger repository, aimed at developers and AI coding agents working on the codebase.
**FocusLogger** is a Windows desktop utility that monitors and logs which process or program takes window focus. It targets users (especially gamers and power users) who experience unexpected focus stealing — where a background process briefly grabs foreground focus, interrupting gameplay or work. The tool logs every focus change with timestamps, process details, window class names, and focus-state flags, allowing users to identify the culprit.
- **Repository:** https://github.com/JocysCom/FocusLogger
- **License:** GNU General Public License v3.0
- **Current version:** 1.2.6
- **Target platform:** Windows 10+ with .NET 8.0
- **Primary audiences:** Gamers, power users, IT support personnel diagnosing focus-stealing issues.
## 2. Top-Level Structure
This section maps every top-level directory and file to help navigate the repository quickly.
| Path | Purpose |
|------|---------|
| `FocusLogger/` | Main application project (WPF, .NET 8.0). Contains all app source code, shared library, and resources. |
| `FocusLogger.Tests/` | MSTest test project. Unit tests for CSV export and UI automation tests. |
| `Documents/` | Release engineering: signing scripts, zip packaging scripts, screenshot tooling, and pre-built release files. |
| `Resources/` | Solution-level shared scripts (currently `ZipFiles.ps1` for checksum-aware zip packaging). |
| `.ai/` | AI agent instructions, coding guidelines, repository analysis, and skills. |
| `JocysCom.FocusLogger.slnx` | Solution file (XML-based `.slnx` format) referencing the two projects. |
| `README.md` | Project overview, download link, system requirements, screenshot. |
| `LICENSE` | GPLv3 license text. |
| `SECURITY.md` | Security vulnerability reporting policy (support@jocys.com). |
| `Settings.XamlStyler` | XamlStyler configuration for consistent XAML formatting. |
| `Solution_Cleanup.ps1` | PowerShell script for cleaning build artifacts. |
## 3. Technology Stack & Key Dependencies
This section lists verified technologies and versions drawn from project files.
| Technology | Version / Detail | Evidence |
|------------|-----------------|----------|
| .NET | 8.0 (`net8.0-windows`) | `JocysCom.FocusLogger.csproj` TargetFramework |
| C# | Implicit (SDK default for .NET 8) | SDK-style project |
| WPF | `true` | csproj |
| Windows Forms interop | `true` | csproj — used for P/Invoke helpers and DPI awareness |
| MSTest | v3.x (`MSTest.TestFramework 3.*`, `MSTest.TestAdapter 3.*`) | Test csproj PackageReference |
| Microsoft.NET.Test.Sdk | 17.x | Test csproj PackageReference |
| Windows API (user32.dll) | P/Invoke | `NativeMethods.cs` |
| PowerShell | Scripts for build, sign, zip, cleanup | `Documents/`, `Resources/`, root |
| XamlStyler | Config present | `Settings.XamlStyler` |
**No NuGet package dependencies** in the main application project — all functionality comes from .NET SDK and the embedded `JocysCom.ClassLibrary`.
## 4. Architecture & Runtime Model
This section describes how the application is structured and how it operates at runtime.
FocusLogger is a **single-executable WPF desktop application** that polls Windows API functions to detect focus changes. It does not persist log data between sessions (in-memory only) but provides CSV export for offline analysis.
### Architectural layers
```mermaid
graph TD
subgraph UI["UI Layer (WPF)"]
App["App.xaml.cs Entry point, DPI aware"]
MW["MainWindow.xaml.cs Main window frame"]
DLC["DataListControl.xaml.cs Core logging UI + logic"]
end
subgraph Core["Core Logic"]
DI["DataItem.cs Log entry model"]
DIT["DataItemType.cs Entry type enum"]
NM["NativeMethods.cs P/Invoke declarations"]
CSV["CSV Export BuildCsvContent, CsvEscape"]
end
subgraph Shared["JocysCom.ClassLibrary (embedded)"]
Config["Configuration SettingsData, SettingsItem, AssemblyInfo"]
CompModel["ComponentModel SortableBindingList, BindingListInvoked"]
Controls["Controls ControlsHelper, ItemFormattingConverter, InfoControl, MessageBoxWindow"]
Other["Collections, IO, Text, Runtime, Data"]
end
subgraph WinAPI["Windows OS"]
User32["user32.dll"]
end
App --> MW --> DLC
DLC --> DI
DLC --> NM
DLC --> CSV
NM --> User32
DI --> Config
DLC --> CompModel
DLC --> Controls
MW --> Controls
```
### Key architectural decisions
- **Polling via timer:** A `System.Timers.Timer` with 1ms interval (non-auto-reset) continuously polls `GetActiveWindow()` and `GetForegroundWindow()`. Duplicate events are suppressed via `DataItem.IsSame()`.
- **Thread safety:** Timer fires on a thread-pool thread; UI updates are marshalled via `ControlsHelper.BeginInvoke()`. A `lock(AddLock)` synchronizes the polling logic.
- **Embedded shared library:** `JocysCom.ClassLibrary` files are included directly in `FocusLogger/JocysCom/` rather than as a compiled DLL or NuGet package.
- **No MVVM framework:** Code-behind pattern with data binding. `DataListControl.xaml.cs` contains both view-model-like logic and model interaction.
## 5. Project Inventory
This section lists each project in the solution with its key metadata.
### 5.1 JocysCom.FocusLogger (main application)
| Property | Value |
|----------|-------|
| Path | `FocusLogger/JocysCom.FocusLogger.csproj` |
| Output type | `WinExe` |
| Target framework | `net8.0-windows` |
| Assembly name | `JocysCom.FocusLogger` |
| Description | Find out which process or program is taking the window focus. In game, mouse and keyboard could temporarily stop responding if another program takes the focus. This tool could help diagnose which program is stealing the focus. |
| Version | 1.2.6 |
| NuGet dependencies | None |
| Embedded resources | `Resources/BuildDate.txt` (auto-generated), `Resources/AiAnalysisPrompt.md` |
**Source structure:**
| Directory | Contents |
|-----------|----------|
| `FocusLogger/` (root) | `App.xaml(.cs)`, `MainWindow.xaml(.cs)`, `AssemblyInfo.cs`, `App.ico` |
| `FocusLogger/Common/` | `DataItem.cs`, `DataItemType.cs`, `NativeMethods.cs` |
| `FocusLogger/Controls/` | `DataListControl.xaml(.cs)` — core logging control |
| `FocusLogger/JocysCom/` | Embedded `JocysCom.ClassLibrary` (~30 files across Collections, Common, ComponentModel, Configuration, Controls, Data, IO, Runtime, Text) |
| `FocusLogger/Resources/` | `AiAnalysisPrompt.md`, `BuildDate.txt`, `Icons/` (SVG sources, XAML icons, conversion scripts) |
| `FocusLogger/Properties/` | Publish profiles |
### 5.2 JocysCom.FocusLogger.Tests (test project)
| Property | Value |
|----------|-------|
| Path | `FocusLogger.Tests/JocysCom.FocusLogger.Tests.csproj` |
| Target framework | `net8.0-windows` |
| Test framework | MSTest v3.x |
| Project reference | `FocusLogger/JocysCom.FocusLogger.csproj` |
**Test files:**
| File | Purpose |
|------|---------|
| `CsvExportTests.cs` | Unit tests for `CsvEscape` and `BuildCsvContent` methods |
| `UIAutomationTests.cs` | UI automation tests using `System.Windows.Automation` — launches the built app and interacts with controls by AutomationId |
Run tests with: `dotnet test FocusLogger.Tests/JocysCom.FocusLogger.Tests.csproj`
## 6. Dependency & Data Flow
This section explains how the projects and components relate to each other and how data moves through the system.
### Project dependency graph
```mermaid
graph LR
Tests["FocusLogger.Tests (MSTest v3)"] -->|ProjectReference| App["JocysCom.FocusLogger (WPF App)"]
App -->|embedded source| Shared["JocysCom.ClassLibrary (in FocusLogger/JocysCom/)"]
App -->|P/Invoke| WinAPI["user32.dll"]
```
### Runtime data flow
1. `System.Timers.Timer` fires (1ms interval, non-auto-reset).
2. `DataListControl.UpdateInfo()` acquires `AddLock`.
3. Calls `NativeMethods.GetActiveWindow()` and `NativeMethods.GetForegroundWindow()`.
4. For each handle, `GetItemFromHandle()` creates a `DataItem` with timestamp, focus flags (mouse/keyboard/caret), window title, and window class name.
5. `IsSame()` checks if the event differs from the previous one; if not, it is skipped.
6. `UpdateFromProcess()` enriches the `DataItem` with process name and path (with error handling for restricted processes).
7. The item is inserted at position 0 of `SortableBindingList` via `ControlsHelper.BeginInvoke()` (UI thread dispatch).
8. The WPF `DataGrid` updates via data binding. `ItemFormattingConverter` translates boolean flags to icons.
### CSV export flow
1. User clicks "Save CSV" button.
2. `SaveFileDialog` prompts for file location.
3. `BuildCsvContent()` iterates all `DataItem` entries, writing CSV with headers: Date, PID, Process Name, Active, Mouse, Keyboard, Caret, Window Title, Window Class, Path.
4. File is written as UTF-8.
5. "Explore" button opens the saved file location in Explorer.
6. "AI Prompt Example" button shows the embedded `AiAnalysisPrompt.md` in a `MessageBoxWindow` for users to copy and paste into an AI assistant along with their CSV.
## 7. Build, Test, CI/CD & Operational Workflows
This section documents how the project is built, tested, and released based on repository evidence.
### Build
```bash
dotnet build JocysCom.FocusLogger.slnx
```
- **Pre-build event:** Generates `Resources/BuildDate.txt` with the current ISO 8601 timestamp via PowerShell.
- **Output:** Single `JocysCom.FocusLogger.exe` in `bin/{Configuration}/net8.0-windows/`.
- **Debug configuration:** Embedded PDB symbols (`DebugType: embedded`).
### Test
```bash
dotnet test FocusLogger.Tests/JocysCom.FocusLogger.Tests.csproj
```
- **Framework:** MSTest v3.x with Microsoft.NET.Test.Sdk 17.x.
- **Unit tests:** `CsvExportTests` — validates CSV escaping and content generation.
- **UI automation tests:** `UIAutomationTests` — launches the built application and interacts via `System.Windows.Automation`. Requires a prior build of the main project.
### Release / packaging scripts
| Script | Purpose |
|--------|---------|
| `Documents/App_1_Sign.ps1` | Code-signs the application executable. |
| `Documents/App_2_Zip.ps1` | Packages the signed executable into a release ZIP. |
| `Resources/ZipFiles.ps1` | Shared utility for checksum-aware ZIP creation (compares source/dest checksums before rebuilding). |
| `Documents/Take_Screenshot.ps1` | Captures application screenshot for documentation. |
| `Documents/Take_Screenshot.ps1.cs` | C# helper compiled by the screenshot script. |
| `Solution_Cleanup.ps1` | Cleans `bin/`, `obj/`, and other build artifacts. |
### Icon workflow
SVG icon sources are stored in `FocusLogger/Resources/Icons/Icons_Default/`. The script `Convert_SVG_to_XAML.ps1` converts them to XAML resource dictionaries (`Icons_Default.xaml`).
### CI/CD
No CI/CD workflow files were found under `.github/workflows/`. Builds and releases appear to be performed locally.
## 8. Documentation Map
This section identifies where documentation lives in the repository.
| Location | Audience | Content |
|----------|----------|---------|
| `README.md` | End users, contributors | Project overview, download link, system requirements, screenshot |
| `SECURITY.md` | Security researchers | Vulnerability reporting policy |
| `LICENSE` | All | GPLv3 full text |
| `.ai/ReadMe.md` | AI agents, developers | Explains the purpose of each file in the `.ai/` directory and how custom instructions work for Copilot, CLINE, and Codex |
| `.ai/instructions.md` | AI agents | Role definition and output formatting rules for AI-assisted edits |
| `.ai/coding-guideline.instructions.md` | AI agents | Detailed coding guidelines, source control conventions (branch/PR naming), testing workflow, and constraints for AI agents |
| `.ai/repository-analysis.instructions.md` | AI agents, developers | This file — comprehensive repository reference |
| `.ai/skills/` | AI agents | Skill definitions (e.g., `ai-self-improvement`) for agent-assisted workflows |
| `FocusLogger/Resources/AiAnalysisPrompt.md` | End users | Prompt template for users to paste into AI assistants alongside exported CSV logs |
| `Documents/Images/` | README, users | Application screenshot |
| `Settings.XamlStyler` | Developers | XamlStyler formatting configuration |
### Documentation taxonomy
```mermaid
graph TD
subgraph EndUsers["End-User Documentation"]
README["README.md Overview, download, requirements"]
SECURITY["SECURITY.md Vulnerability reporting"]
LICENSE["LICENSE GPLv3"]
AiPrompt["FocusLogger/Resources/ AiAnalysisPrompt.md Prompt template for CSV analysis"]
Screenshot["Documents/Images/ Application screenshot"]
end
subgraph AIAgents["AI Agent Instructions"]
AiReadMe[".ai/ReadMe.md Directory guide"]
AiInstr[".ai/instructions.md Role & output rules"]
AiCoding[".ai/coding-guideline .instructions.md Coding & SCM conventions"]
AiRepo[".ai/repository-analysis .instructions.md This file"]
AiSkills[".ai/skills/ Agent skill definitions"]
end
subgraph DevConfig["Developer Configuration"]
XamlStyler["Settings.XamlStyler XAML formatting"]
end
```
## 9. AI-Agent-Relevant Conventions and Constraints
This section captures rules and patterns that materially affect automated edits. The full set of coding and source-control conventions is defined in `.ai/coding-guideline.instructions.md`; the highlights below are the items most likely to cause mistakes if overlooked.
1. **Coding style:** Follow Microsoft C# conventions. PascalCase for public members, camelCase for locals. Some private fields use `_PascalCase` (e.g., `_Date`). Preserve existing naming patterns in each file. Prefer the simplest viable solution; avoid over-engineering.
2. **Doc comments:** Per `.ai/instructions.md`, convert simple comments to XML documentation comments where beneficial for IntelliSense. Do not alter surrounding code when doing so.
3. **Shared library files (`FocusLogger/JocysCom/`):** These are embedded from a shared `JocysCom.ClassLibrary`. Exercise caution when editing — changes here may diverge from the upstream library.
4. **XAML formatting:** The repository uses XamlStyler (see `Settings.XamlStyler`). XAML edits should conform to the configured style.
5. **No NuGet packages in the main app:** All dependencies are framework-provided or embedded source. Do not introduce NuGet package dependencies without explicit approval.
6. **Test project uses MSTest v3:** New tests should follow MSTest v3 patterns (`[TestClass]`, `[TestMethod]`, `Assert.*`).
7. **UI automation tests depend on a built executable:** `UIAutomationTests` locate the app at a relative path from the test output. Building the main project before running these tests is required.
8. **Pre-build event:** The csproj generates `Resources/BuildDate.txt` via PowerShell. This file should not be manually edited or committed.
9. **Solution format:** Uses `.slnx` (XML-based solution format), not the older `.sln` text format.
10. **No CI/CD pipelines:** All build and release steps are manual/local. Scripts in `Documents/` handle signing and packaging.
11. **File size limit:** No code file that is created or modified may exceed 6000 tokens (~24 KB).
12. **Source control conventions:** Issues use `{CATEGORY}: {Description}` (FEAT, FIX, TECH, DOCS). Branches use `{CATEGORY}-{issue#}-{lowercase-dashed-name}`. PRs use `PR: #{issue#}: {CATEGORY}: {Description}` and must reference the issue with `Closes #{issue#}`. Merge via PR only — no direct pushes to `main`.
13. **No Co-Authored-By lines:** Do not add `Co-Authored-By` AI model/company lines to commits.
14. **Issue closure:** Never close issues before a release is published with the fix.
15. **No duplication / SSOT:** Update or move existing code instead of adding parallel implementations. If introducing a replacement, remove the old one in the same change.
16. **Stability primitives:** When the repo provides an established script or command, use it verbatim. Only deviate when it is proven broken, and then fix the primitive rather than inventing a parallel path.
================================================
FILE: .ai/skills/ai-self-improvement/SKILL.md
================================================
---
name: ai-self-improvement
description: Update, create, improve, and synchronise this repository's AI agent instructions and related assets (including skills). Use when the user asks to create or edit a skill/SKILL.md, modify the agent's own instructions/processes, restructure instruction governance, migrate instruction content into skills, or run/adjust the sync pipeline that publishes `.ai/` sources into agent-specific folders. Load this skill before writing any SKILL.md, .instructions.md, or touching any skills/ folder (.ai/, .claude/, .roo/, .github/). It tells you the correct location (.ai/) and the sync step, so files end up in the right place.
---
# AI Self-Improvement (Instructions + Skills)
## Critical: `.ai/` is the Primary Source for ALL Agents
The `.ai/` folder is the **single source of truth** for all AI agent configurations in this repository. This applies to:
- **CLINE / Roo Code** — synced to `.roo/rules/` and `.roo/skills/`
- **GitHub Copilot** — synced to `.github/copilot-instructions.md`
- **OpenAI Codex / AGENTS.md** — synced to `AGENTS.md` at repo root
- **Claude Code** — synced to `.claude/*.instructions.md` and `.claude/skills/`
**IMPORTANT:** When asked to modify skills, instructions, or perform any AI self-improvement task, you MUST:
1. Locate the source file under `.ai/` (not the agent-specific output)
2. Make changes to the `.ai/` source
3. Run the sync script to propagate changes to all agents
## Path Mapping Reference
When you encounter a path in an agent-specific folder, map it to `.ai/`:
| Agent-Specific Path | Source Path (Edit Here) |
|---------------------|------------------------|
| `.roo/rules/*.md` | `.ai/*.instructions.md` |
| `.roo/skills//SKILL.md` | `.ai/skills//SKILL.md` |
| `.github/copilot-instructions.md` | `.ai/instructions.md` (generated) |
| `AGENTS.md` | `.ai/instructions.md` (generated) |
| `.claude/*.instructions.md` | `.ai/*.instructions.md` |
| `.claude/skills//SKILL.md` | `.ai/skills//SKILL.md` |
**Example:** If asked to update `.roo/skills/ai-self-improvement/SKILL.md`, you must edit `.ai/skills/ai-self-improvement/SKILL.md` instead.
## Editable instruction files (sources of truth)
You can update your own instruction files under `.ai/`:
- `.ai/instructions.md` — the main system instructions file
- `.ai/*instructions.md` — additional instruction files (auto-included)
- `.ai/*instructions-detail.md` — detailed instruction files (read only when needed)
- `.ai/skills//SKILL.md` — skill definition files
## Workflow
1. Treat `.ai/` as the **single source of truth** for agent instructions **and skills**.
2. When creating or migrating a skill, create/update it under `.ai/skills/`.
3. Make instruction changes in `.ai/instructions.md` and related `*.instructions.md` / `*.instructions-detail.md` files.
4. Do **not** edit generated outputs directly (they are produced by the sync script):
- `.roo/rules/`
- `.roo/skills/`
- `.github/copilot-instructions.md`
- `AGENTS.md`
- `.claude/`
5. **Test changes before syncing** — verify scripts execute correctly and changes work as expected.
6. After testing, run the sync script to apply to all agents.
## Testing Before Sync
Before running the sync script, always verify your changes work correctly:
- **For script changes**: Execute the modified script and verify output is correct
- **For instruction changes**: Review the markdown renders properly and instructions are clear
- **For skill changes**: Test any bundled tools or scripts included in the skill
**Example**: If you modify a PowerShell script in a skill, run it directly from `.ai/skills//scripts/` to confirm it works before syncing.
## Activation process
After editing instruction files (or master skills), run from repository root:
```powershell
.\.ai\skills\ai-self-improvement\scripts\Sync-AgentAssets.ps1 AUTO
```
This script synchronizes changes from `.ai/` to all agent-specific folders.
## Single source of truth
**Never embed template content in instructions — reference template files instead.**
Example:
- ✅ "Template maintained in `pr/checklist.template.md`"
- ❌ Pasting template content into instructions
## Bundled scripts
- Sync entrypoint (instructions + skills): `.ai/skills/ai-self-improvement/scripts/Sync-AgentAssets.ps1`
================================================
FILE: .ai/skills/ai-self-improvement/scripts/Sync-AgentAssets.ps1
================================================
# Script: Sync-AgentAssets.ps1
# Location: .ai/skills/ai-self-improvement/scripts/Sync-AgentAssets.ps1
# Description:
# Synchronises AI agent instruction files and skills from master sources under `.ai/`.
# - Instructions: copies `*.instructions.md` from `.ai/` into agent-specific outputs.
# - Skills: mirrors `.ai/skills/*` into agent skill folders (e.g. `.roo/skills/*`).
#
# Options for Mode:
# ALL - update all known agent outputs
# AUTO - update only agents that exist in this repository (default usage)
# Or a specific agent name: CLINE, ROO CODE, GitHub CoPilot, OpenAI Codex, Claude Code
param(
[Parameter(Position = 0)]
[string]$Mode,
[switch]$NoClear
)
# Combine remaining args so Windows PowerShell (-File) invocations like:
# Sync-AgentAssets.ps1 GitHub CoPilot
# work the same as:
# Sync-AgentAssets.ps1 "GitHub CoPilot"
if ($args.Count -gt 0) {
$ModeFromArgs = ($args -join ' ')
if (-not $Mode -or $Mode -eq '') {
$Mode = $ModeFromArgs
}
}
# Allow calling via the old filename (if invoked through a copied/renamed script).
# This only affects displayed script name in prompts/logs.
$scriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Path)
# Strict mode
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Ensure-Directory {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -Path $Path -PathType Container)) {
New-Item -ItemType Directory -Force -Path $Path | Out-Null
}
}
# Function to check if instruction files exist in a directory
function Test-HasInstructionFiles {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[string]$Filter = '*instructions.md'
)
if (Test-Path $Path -PathType Container) {
$files = @(Get-ChildItem $Path -Filter $Filter -File -ErrorAction SilentlyContinue)
return ($files.Length -gt 0)
}
return $false
}
# Function to pause at the end (unless -NoWait is specified)
function Invoke-Pause {
Write-Host "Pausing for 2 seconds..."
Start-Sleep -Seconds 2
}
function Copy-FileIfDifferent {
param(
[Parameter(Mandatory = $true)]
[string]$SourcePath,
[Parameter(Mandatory = $true)]
[string]$TargetPath
)
$targetDir = Split-Path -Path $TargetPath -Parent
Ensure-Directory -Path $targetDir
if (-not (Test-Path -Path $TargetPath -PathType Leaf)) {
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
$relative = $TargetPath.Substring($repoRoot.Length + 1)
Write-Host "Created: $relative"
return
}
$srcBytes = [System.IO.File]::ReadAllBytes($SourcePath)
$dstBytes = [System.IO.File]::ReadAllBytes($TargetPath)
if ($srcBytes.Length -eq $dstBytes.Length) {
$same = $true
for ($i = 0; $i -lt $srcBytes.Length; $i++) {
if ($srcBytes[$i] -ne $dstBytes[$i]) { $same = $false; break }
}
if ($same) {
$relative = $TargetPath.Substring($repoRoot.Length + 1)
Write-Host "Up-to-date: $relative"
return
}
}
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
$relative = $TargetPath.Substring($repoRoot.Length + 1)
Write-Host "Updated: $relative"
}
function Get-TextAuto {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
# .NET StreamReader detects BOM for UTF-8/UTF-16/UTF-32 automatically.
$sr = New-Object System.IO.StreamReader($Path, $true)
try {
return $sr.ReadToEnd()
}
finally {
$sr.Dispose()
}
}
function Write-Utf8NoBom {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Content
)
$dir = Split-Path -Path $Path -Parent
Ensure-Directory -Path $dir
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
}
function Assert-InstructionSync {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDirectory,
[Parameter(Mandatory = $true)]
[string]$TargetDirectory,
[Parameter(Mandatory = $true)]
[System.IO.FileSystemInfo[]]$SourceFiles
)
$srcDir = Join-Path $repoRoot $SourceDirectory
$dstDir = Join-Path $repoRoot $TargetDirectory
foreach ($sourceFile in $SourceFiles) {
$srcPath = Join-Path $srcDir $sourceFile.Name
$dstPath = Join-Path $dstDir $sourceFile.Name
if (-not (Test-Path $dstPath -PathType Leaf)) {
throw "Binary comparison failed. Destination file missing: $dstPath"
}
$srcBytes = [System.IO.File]::ReadAllBytes($srcPath)
$dstBytes = [System.IO.File]::ReadAllBytes($dstPath)
if ($srcBytes.Length -ne $dstBytes.Length) {
throw "Binary comparison failed. Source and target size mismatch in binary: Source: $srcPath Target: $dstPath"
}
for ($i = 0; $i -lt $srcBytes.Length; $i++) {
if ($srcBytes[$i] -ne $dstBytes[$i]) {
throw "Binary comparison failed. Source and target content mismatch in binary: Source: $srcPath Target: $dstPath"
}
}
}
}
# Function to update agents that use multiple separate instruction files
function Update-MultipleFileAgent {
param(
[Parameter(Mandatory = $true)]
[string]$AgentName,
[Parameter(Mandatory = $true)]
[string]$TargetDirectory,
[Parameter(Mandatory = $true)]
[System.IO.FileSystemInfo[]]$SourceFiles,
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
Write-Host "`r`n--- Updating $AgentName Instructions ---"
$targetDir = Join-Path $RepoRoot $TargetDirectory
foreach ($sourceFile in $SourceFiles) {
$targetFile = Join-Path $targetDir $sourceFile.Name
Copy-FileIfDifferent -SourcePath $sourceFile.FullName -TargetPath $targetFile
}
Assert-InstructionSync -SourceDirectory ".ai" -TargetDirectory $TargetDirectory -SourceFiles $SourceFiles
}
# Function to update agents that use a single combined instruction file
function Update-SingleFileAgent {
param(
[Parameter(Mandatory = $true)]
[string]$AgentName,
[Parameter(Mandatory = $true)]
[string]$TargetFilePath,
[Parameter(Mandatory = $true)]
[System.IO.FileSystemInfo[]]$SourceFiles,
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
Write-Host "`r`n--- Updating $AgentName Instructions ---"
$targetFile = Join-Path $RepoRoot $TargetFilePath
$relativeTarget = $targetFile.Substring($repoRoot.Length + 1)
$allInstructionsContent = New-Object System.Text.StringBuilder
$firstFile = $true
foreach ($sourceFile in $SourceFiles) {
$sourceContent = Get-TextAuto -Path $sourceFile.FullName
if ([string]::IsNullOrWhiteSpace($sourceContent)) {
Write-Warning "Skipping empty file: $($sourceFile.Name)"
continue
}
if (-not $firstFile) {
[void]$allInstructionsContent.AppendLine("")
}
[void]$allInstructionsContent.AppendLine("==== START OF INSTRUCTIONS FROM: $($sourceFile.Name) ====")
[void]$allInstructionsContent.AppendLine("")
[void]$allInstructionsContent.AppendLine("# Instructions from: $($sourceFile.Name)")
[void]$allInstructionsContent.AppendLine("")
[void]$allInstructionsContent.AppendLine($sourceContent.Trim())
[void]$allInstructionsContent.AppendLine("")
[void]$allInstructionsContent.AppendLine("==== END OF INSTRUCTIONS FROM: $($sourceFile.Name) ====")
$firstFile = $false
}
$finalContent = $allInstructionsContent.ToString()
$existing = if (Test-Path -Path $targetFile -PathType Leaf) { Get-TextAuto -Path $targetFile } else { $null }
if ($null -ne $existing -and $existing -eq $finalContent) {
Write-Host "Up-to-date: $relativeTarget"
return
}
Write-Utf8NoBom -Path $targetFile -Content $finalContent
Write-Host "Updated: $relativeTarget"
}
function Invoke-RoboCopyMirror {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDirectory,
[Parameter(Mandatory = $true)]
[string]$DestinationDirectory,
[Parameter(Mandatory = $true)]
[string]$Label
)
if (-not (Test-Path $SourceDirectory -PathType Container)) {
Write-Host "No skills folder found at: $SourceDirectory"
return
}
Ensure-Directory -Path $DestinationDirectory
Write-Host "`r`n--- Mirroring skills to $Label ---"
Write-Host "Source: $SourceDirectory"
Write-Host "Destination: $DestinationDirectory"
# /MIR = mirror (copy + delete removed)
# /FFT = tolerate 2s timestamp granularity
# /R:1 /W:1 = retry quickly
# /NFL/NDL = no file/dir listing (keep output compact)
# /NJH/NJS = no job header/summary
# /NP = no progress
# /XD = exclude version control/build dirs
$excludedDirs = @('.git', '.vs', 'bin', 'obj')
$args = @(
$SourceDirectory,
$DestinationDirectory,
'/MIR',
'/FFT',
'/R:1',
'/W:1',
'/NFL',
'/NDL',
'/NJH',
'/NJS',
'/NP'
)
foreach ($d in $excludedDirs) {
$args += '/XD'
$args += $d
}
$exe = 'robocopy'
# Do not echo the full robocopy command; it is noisy and can wrap in some terminals.
Write-Host "robocopy /MIR /NFL /NDL /NJH /NJS /NP ..."
& $exe @args | Out-Null
$exitCode = $LASTEXITCODE
# Robocopy uses bitmask exit codes.
# 0-7 are success with various flags; >= 8 indicates failure.
if ($exitCode -ge 8) {
throw "Robocopy failed with exit code $exitCode. Command: $cmd"
}
# IMPORTANT: robocopy returns 1+ for successful copies.
# Ensure PowerShell script does not propagate a non-zero exit code for success cases.
$global:LASTEXITCODE = 0
Write-Host "Mirrored skills to $Label (robocopy exit code $exitCode)."
}
function Sync-SkillsToRoo {
param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
$srcSkillsRoot = Join-Path $RepoRoot ".ai\skills"
$rooSkillsRoot = Join-Path $RepoRoot ".roo\skills"
Invoke-RoboCopyMirror -SourceDirectory $srcSkillsRoot -DestinationDirectory $rooSkillsRoot -Label "Roo (.roo\\skills)"
}
function Sync-SkillsToGitHub {
param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
$srcSkillsRoot = Join-Path $RepoRoot ".ai\skills"
$githubSkillsRoot = Join-Path $RepoRoot ".github\skills"
Invoke-RoboCopyMirror -SourceDirectory $srcSkillsRoot -DestinationDirectory $githubSkillsRoot -Label "GitHub (.github\\skills)"
}
function Sync-SkillsToClaude {
param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
$srcSkillsRoot = Join-Path $RepoRoot ".ai\skills"
$claudeSkillsRoot = Join-Path $RepoRoot ".claude\skills"
Invoke-RoboCopyMirror -SourceDirectory $srcSkillsRoot -DestinationDirectory $claudeSkillsRoot -Label "Claude Code (.claude\\skills)"
}
# --- Main Script ---
if (-not $NoClear) {
Clear-Host
}
# We are located under `.ai/skills//tools`. Find repo root by going up 4 levels.
$scriptDir = $PSScriptRoot
$repoRoot = (Join-Path -Path $scriptDir -ChildPath "..\..\..\.." | Resolve-Path).Path
# `.ai` folder path
$aiDir = Join-Path $repoRoot ".ai"
# Discover source files matching *instructions.md in the .ai folder
[System.IO.FileSystemInfo[]]$sourceInstructionFiles = Get-ChildItem -Path $aiDir -Filter "*instructions.md" -File | Sort-Object Name
if ($null -eq $sourceInstructionFiles -or $sourceInstructionFiles.Length -eq 0) {
Write-Warning "No '*instructions.md' files found in '$aiDir'. Nothing to process."
exit 0
}
Write-Host "Found the following source instruction files in '$aiDir':"
$sourceInstructionFiles | ForEach-Object { Write-Host "- $($_.Name)" }
# Mode parameter handling: if 'ALL' or 'AUTO', skip interactive prompt
if ($Mode -eq 'ALL') {
Write-Host "Selected: ALL (parameter mode)"
$updateCline = $true
$updateCopilot = $true
$updateRooCode = $true
$updateCodex = $true
$updateClaude = $true
}
elseif ($Mode -eq 'AUTO') {
Write-Host "Selected: AUTO (parameter mode)"
# Determine available agents based on instruction files
$updateCline = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.clinerules')
$updateRooCode = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.roo\rules')
$updateCopilot = Test-Path (Join-Path $repoRoot '.github\copilot-instructions.md') -PathType Leaf
$updateCodex = Test-Path (Join-Path $repoRoot 'AGENTS.md') -PathType Leaf
$updateClaude = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.claude')
Write-Host "Agents to update based on available instruction files:"
if ($updateCline) { Write-Host "- CLINE" }
if ($updateRooCode) { Write-Host "- ROO CODE" }
if ($updateCopilot) { Write-Host "- GitHub CoPilot" }
if ($updateCodex) { Write-Host "- OpenAI Codex" }
if ($updateClaude) { Write-Host "- Claude Code" }
}
elseif ($Mode -and $Mode -ne '') {
# Specific agent mode (e.g., CLINE, "ROO CODE", etc.)
$updateCline = ($Mode -eq 'CLINE')
$updateCopilot = ($Mode -eq 'GitHub CoPilot')
$updateRooCode = ($Mode -eq 'ROO CODE')
$updateCodex = ($Mode -eq 'OpenAI Codex')
$updateClaude = ($Mode -eq 'Claude Code')
Write-Host "Selected: $Mode (parameter mode)"
}
else {
# User prompt for agent selection
# Detect available agents for interactive menu
$hasCline = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.clinerules')
$hasRooCode = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.roo\rules')
$hasCopilot = Test-Path (Join-Path $repoRoot '.github\copilot-instructions.md') -PathType Leaf
$hasCodex = Test-Path (Join-Path $repoRoot 'AGENTS.md') -PathType Leaf
$hasClaude = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.claude')
Write-Host "`r`nDetected AI agents with instruction files:"
if ($hasCline) { Write-Host "- CLINE" }
if ($hasRooCode) { Write-Host "- ROO CODE" }
if ($hasCopilot) { Write-Host "- GitHub CoPilot" }
if ($hasCodex) { Write-Host "- OpenAI Codex" }
if ($hasClaude) { Write-Host "- Claude Code" }
Write-Host ""
Write-Host "=============================================================="
Write-Host "Select Agent Instruction Set to Update"
Write-Host "--------------------------------------------------------------"
Write-Host "1. AUTO - Update only agents with instruction files (default)"
Write-Host "2. ALL - Update instructions for all AI agents"
Write-Host "3. CLINE - Update instructions for CLINE"
Write-Host "4. ROO CODE - Update instructions for ROO CODE"
Write-Host "5. GitHub CoPilot - Update instructions for GitHub CoPilot"
Write-Host "6. OpenAI Codex - Update instructions for OpenAI Codex"
Write-Host "7. Claude Code - Update instructions for Claude Code"
Write-Host "0. Exit"
Write-Host "=============================================================="
$selection = Read-Host "Enter the number of your choice (0-7)"
# Initialize flags
$updateCline = $false
$updateCopilot = $false
$updateRooCode = $false
$updateCodex = $false
$updateClaude = $false
switch ($selection) {
'1' {
$updateCline = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.clinerules')
$updateRooCode = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.roo\rules')
$updateCopilot = Test-Path (Join-Path $repoRoot '.github\copilot-instructions.md') -PathType Leaf
$updateCodex = Test-Path (Join-Path $repoRoot 'AGENTS.md') -PathType Leaf
$updateClaude = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.claude')
Write-Host "Selected: AUTO"
}
'2' {
$updateCline = $true
$updateCopilot = $true
$updateRooCode = $true
$updateCodex = $true
$updateClaude = $true
Write-Host "Selected: ALL"
}
'3' { $updateCline = $true; Write-Host "Selected: CLINE" }
'4' { $updateRooCode = $true; Write-Host "Selected: ROO CODE" }
'5' { $updateCopilot = $true; Write-Host "Selected: GitHub CoPilot" }
'6' { $updateCodex = $true; Write-Host "Selected: OpenAI Codex" }
'7' { $updateClaude = $true; Write-Host "Selected: Claude Code" }
'0' { Write-Host "Operation cancelled by user."; exit 0 }
default { throw "Invalid selection. Exiting." }
}
}
# --- Multiple-File Agent Updates ---
if ($updateCline) {
Update-MultipleFileAgent -AgentName "CLINE" -TargetDirectory ".clinerules" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
if ($updateRooCode) {
Update-MultipleFileAgent -AgentName "ROO CODE" -TargetDirectory ".roo\rules" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
# --- Single-File Agent Updates ---
if ($updateCopilot) {
$copilotTarget = ".github\copilot-instructions.md"
$githubInstructionsDir = Join-Path $repoRoot ".github\instructions"
if (Test-Path $githubInstructionsDir -PathType Container) {
Write-Host "`r`n--- Updating GitHub CoPilot Instructions (folder-based) ---"
$mainName = "instructions.md"
$mainSource = $sourceInstructionFiles | Where-Object { $_.Name -ieq $mainName } | Select-Object -First 1
if ($null -eq $mainSource) {
throw "Expected source '$mainName' under .ai but none found."
}
Copy-FileIfDifferent -SourcePath $mainSource.FullName -TargetPath (Join-Path $repoRoot $copilotTarget)
foreach ($sf in $sourceInstructionFiles) {
if ($sf.Name -ieq $mainName) {
continue
}
$destination = Join-Path $githubInstructionsDir $sf.Name
Copy-FileIfDifferent -SourcePath $sf.FullName -TargetPath $destination
}
}
else {
Update-SingleFileAgent -AgentName "GitHub CoPilot" -TargetFilePath $copilotTarget -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
}
if ($updateCodex) {
Update-SingleFileAgent -AgentName "OpenAI Codex" -TargetFilePath "AGENTS.md" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
# --- Claude Code (multiple-file agent) ---
if ($updateClaude) {
Update-MultipleFileAgent -AgentName "Claude Code" -TargetDirectory ".claude" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
# --- Skills mirroring ---
if ($updateRooCode -or $Mode -eq 'ALL' -or $Mode -eq 'AUTO') {
Sync-SkillsToRoo -RepoRoot $repoRoot
}
# GitHub Copilot: mirror skills to `.github/skills` (Copilot tries to load from there).
if ($updateCopilot -or $Mode -eq 'ALL' -or $Mode -eq 'AUTO') {
Sync-SkillsToGitHub -RepoRoot $repoRoot
}
# Claude Code: mirror skills to `.claude/skills`.
if ($updateClaude -or $Mode -eq 'ALL' -or $Mode -eq 'AUTO') {
Sync-SkillsToClaude -RepoRoot $repoRoot
}
Write-Host "`r`nAll selected operations completed successfully."
# Only pause when launched by double-click (Explorer). In CI / terminal usage, do not pause.
if ($Host.Name -and $Host.Name -notlike '*ConsoleHost*') {
Invoke-Pause
}
================================================
FILE: .claude/coding-guideline.instructions.md
================================================
# Coding Guidelines
- If the qdrant-mcp-server is running, use it for all permanent memory operations (e.g. storing user information).
- After making changes, ALWAYS start a new server for testing.
- Kill all existing related servers from previous testing before starting a new server.
- Prefer the simplest viable solution; avoid over-engineering.
- Do not add broad try/catch or wrapper layers unless required by a failing test or explicit requirement; if you catch, rethrow to preserve the stack.
- Before writing new code, actively look for existing utilities or functions that can be reused instead of duplicated.
- New helper methods or classes must be justified with a clear, documented need for functionality that is unavailable elsewhere in the codebase.
- Always iterate on and reuse existing code instead of creating new implementations.
- Avoid adding layers of abstraction that do not deliver clear value.
- Do not drastically change established patterns before iterating on them.
- No duplication / SSOT: update or move existing code instead of adding parallel implementations. If you introduce a replacement, remove the old one **in the same change**.
- Write code that accounts for different environments (dev, test, and prod).
- Only modify what is explicitly requested or clearly necessary; do **not** create new files or modules unless explicitly requested.
- When fixing bugs, exhaust current implementations before introducing new patterns; if new methods are used, remove the old ones.
- Keep the codebase clean and organized.
- Avoid one-off scripts unless absolutely necessary.
- Use mocks only for tests, not for dev or prod.
- Never add stubbing or fake data in dev or prod environments.
- Never overwrite the .env file without explicit confirmation.
- Focus solely on areas relevant to the task; leave unrelated code untouched.
- Write thorough tests for all major functionality.
- Avoid major changes to the existing architecture unless explicitly instructed.
- Always consider the impact on other methods and areas of the code.
- Prefer to wrap long lines for better readability.
- Preserve existing formatting; limit formatting to lines you changed and match surrounding style. Also remove any unused imports/usings or dead code **introduced by your edits**.
- Stability primitives (repeatability > cleverness): when the repo provides an established way to perform an operation (repo-owned script, documented command snippet, standard PS1 under .ai/Scripts), treat it as the single source of truth. Use it verbatim instead of synthesizing “equivalent” commands. Only deviate when explicitly asked or when the primitive is proven broken in this environment (and then fix the primitive, not invent a parallel path).
- No code file that you **create or modify** may exceed **6000 tokens (~24 KB)** once your changes are applied.
- If your changes alone would push the file past this limit, either trim the change or ask for explicit permission to refactor; do **not** alter unrelated code solely to meet the limit.
- Existing oversized files are left untouched unless the user explicitly requests a refactor.
## Source Control Conventions
Naming conventions for issues, branches, and pull requests.
### Categories
| Category | Use for |
|----------|---------|
| `FEAT` | New features |
| `FIX` | Bug fixes |
| `TECH` | Infrastructure, dependencies, refactoring |
| `DOCS` | Documentation |
### Naming Patterns
| Item | Pattern | Example |
|------------|---------|---------|
| **Issue** | `{CATEGORY}: {Description}` | `FEAT: Download logs to CSV` |
| **Branch** | `{CATEGORY}-{issue#}-{lowercase-dashed-name}` | `FEAT-21-download-logs-to-csv` |
| **PR** | `PR: #{issue#}: {CATEGORY}: {Description}` | `PR: #21: FEAT: Download logs to CSV` |
- Branch names: lowercase, words separated by dashes, non-ASCII replaced with a single dash, derived from issue title but may be shortened.
- PR body must reference the issue with `Closes #{issue#}`.
- Merge via PR only — no direct pushes to `main` or `master`.
- Feature branches are always created from `main` or `master`. Never merge one feature branch into another — only merge `main` if you need to catch up.
- Always create branches from latest origin/main or origin/main, never merge sub-branches, confirm before risky git ops.
- Do not add Co-Authored-By {AI model}/{AI company} lines to commits.
- Never close issues before a release is published with the fix.
Use the following guidelines:
1. Doc Comment Enhancement for IntelliSense
- Replace or augment simple comments with relevant doc comment syntax that is supported by IntelliSense as needed.
- Preserve the original intent and wording of existing comments wherever possible.
2. Code Layout for Clarity
- Place the most important or user-editable sections at the top if logically appropriate.
- Insert headings or separators within the code to clearly delineate where customizations or key logic sections can be adjusted.
3. No Extraneous Code Comments
- Do not include "one-off" or user-directed commentary in the code.
- Confine all clarifications or additional suggestions to explanations outside of the code snippet.
4. Avoid Outdated or Deprecated Methods
- Refrain from introducing or relying on obsolete or deprecated methods and libraries.
- If the current code relies on potentially deprecated approaches, ask for clarification or provide viable, modern alternatives that align with best practices.
5. Testing and Validation
- Suggest running unit tests or simulations on the modified segments to confirm that the changes fix the issue without impacting overall functionality.
- Ensure that any proposed improvements, including doc comment upgrades, integrate seamlessly with the existing codebase.
- After all code modifications, navigate to the affected project directory and build C# then Angular to confirm the application compiles without errors:
cd {PROJECT} && dotnet build {PROJECT}.csproj
cd {PROJECT}/ClientApp && ng build
- Run relevant unit tests if code changes affect core logic.
- If the developer certificate is not trusted, then execute: dotnet dev-certs https --trust
- To launch project use: dotnet watch run --project {PROJECT}/{PROJECT}.csproj --launch-profile "{PROJECT} (NG Build)"
6. Rationale and Explanation
- For every change (including comment conversions), provide a concise explanation detailing how the modification resolves the identified issue while preserving the original design and context.
- Clearly highlight only the modifications made, ensuring that no previously validated progress is altered.
- NOTE: Summarize reasoning for the user, but do NOT expose full chain-of-thought. Keep internal deliberations internal; surface only the concise rationale needed to justify each change.
7. Contextual Analysis
- Use all available context—such as code history, inline documentation, style guidelines—to understand the intended functionality.
- When inspecting an existing file for understanding, prefer reading the whole file in a
single `read_file` call when it comfortably fits in context; switch to targeted slices
only when the file is too large, the tool truncates it, or a specific anchor line is
already known.
- If the role or intent behind a code segment is ambiguous, ask for clarification rather than making assumptions.
8. Targeted, Incremental Changes
- Identify and isolate only the problematic code segments (including places where IntelliSense doc comments can replace simple comments).
- Provide minimal code snippets that address the issue without rewriting larger sections.
- For each suggested code change, explicitly indicate the exact location in the code (e.g., by specifying the function name, class name, line number, or section heading) where the modification should be implemented.
9. Preservation of Context
- Maintain all developer comments, annotations, and workarounds exactly as they appear, transforming them to doc comment format only when it improves IntelliSense support.
- Do not modify or remove any non-code context unless explicitly instructed.
- Avoid introducing new, irrelevant comments in the code.
10. Launching {PROJECT} Correctly:
- Navigate to the {PROJECT} project folder.
- Run the following command to launch the project with live reload and proper debugging configuration:
dotnet watch run --launch-profile "{PROJECT} (NG Build)" --project {PROJECT}/{PROJECT}.csproj
- This command will start the {PROJECT} project on the designated debugging session URL.
- Ensure that any previous {PROJECT} instances are terminated before running this command.
================================================
FILE: .claude/instructions.md
================================================
## Role
Your role is to analyze and improve code by making only localized, targeted changes. You must preserve all validated code, comments, and documented workarounds exactly as they appear. Your suggestions should strictly address only the specific issues identified—such as upgrading simple comments to doc comments for IntelliSense—without altering any surrounding context. Additionally, ensure that no obsolete or deprecated methods are introduced during the improvement process, and do not add extraneous comments that do not directly contribute to the code’s logic. Furthermore, ensure code snippets are clearly structured for readability, placing important or user-editable sections at the top when logical, and using clear separators or headings to highlight customization points.
Wherever beneficial, convert simple comments into recognized documentation comment syntax (e.g., JSDoc for JavaScript, XML comments for C#, JavaDoc for Java) that can be parsed by code intelligence tools like IntelliSense.
Maintain the original meaning of these comments, but structure them in a way that provides maximum benefit for automated tools and refactoring methods.
Apply chain-of-thought reasoning to identify code segments best served by doc comments, analyze the existing context of each comment, and then make precise, incremental modifications that enhance IntelliSense compatibility while preserving existing functionality.
## Output
Wrap any and all code—including regular code snippets, inline code segments, outputs, pseudocode, or any text that represents code—in Markdown code blocks with a language identifier (e.g., ```typescript, ```powershell).
================================================
FILE: .claude/repository-analysis.instructions.md
================================================
# Repository Analysis
## 1. Repository Overview
This document provides a factual reference for the Jocys.com FocusLogger repository, aimed at developers and AI coding agents working on the codebase.
**FocusLogger** is a Windows desktop utility that monitors and logs which process or program takes window focus. It targets users (especially gamers and power users) who experience unexpected focus stealing — where a background process briefly grabs foreground focus, interrupting gameplay or work. The tool logs every focus change with timestamps, process details, window class names, and focus-state flags, allowing users to identify the culprit.
- **Repository:** https://github.com/JocysCom/FocusLogger
- **License:** GNU General Public License v3.0
- **Current version:** 1.2.6
- **Target platform:** Windows 10+ with .NET 8.0
- **Primary audiences:** Gamers, power users, IT support personnel diagnosing focus-stealing issues.
## 2. Top-Level Structure
This section maps every top-level directory and file to help navigate the repository quickly.
| Path | Purpose |
|------|---------|
| `FocusLogger/` | Main application project (WPF, .NET 8.0). Contains all app source code, shared library, and resources. |
| `FocusLogger.Tests/` | MSTest test project. Unit tests for CSV export and UI automation tests. |
| `Documents/` | Release engineering: signing scripts, zip packaging scripts, screenshot tooling, and pre-built release files. |
| `Resources/` | Solution-level shared scripts (currently `ZipFiles.ps1` for checksum-aware zip packaging). |
| `.ai/` | AI agent instructions, coding guidelines, repository analysis, and skills. |
| `JocysCom.FocusLogger.slnx` | Solution file (XML-based `.slnx` format) referencing the two projects. |
| `README.md` | Project overview, download link, system requirements, screenshot. |
| `LICENSE` | GPLv3 license text. |
| `SECURITY.md` | Security vulnerability reporting policy (support@jocys.com). |
| `Settings.XamlStyler` | XamlStyler configuration for consistent XAML formatting. |
| `Solution_Cleanup.ps1` | PowerShell script for cleaning build artifacts. |
## 3. Technology Stack & Key Dependencies
This section lists verified technologies and versions drawn from project files.
| Technology | Version / Detail | Evidence |
|------------|-----------------|----------|
| .NET | 8.0 (`net8.0-windows`) | `JocysCom.FocusLogger.csproj` TargetFramework |
| C# | Implicit (SDK default for .NET 8) | SDK-style project |
| WPF | `true` | csproj |
| Windows Forms interop | `true` | csproj — used for P/Invoke helpers and DPI awareness |
| MSTest | v3.x (`MSTest.TestFramework 3.*`, `MSTest.TestAdapter 3.*`) | Test csproj PackageReference |
| Microsoft.NET.Test.Sdk | 17.x | Test csproj PackageReference |
| Windows API (user32.dll) | P/Invoke | `NativeMethods.cs` |
| PowerShell | Scripts for build, sign, zip, cleanup | `Documents/`, `Resources/`, root |
| XamlStyler | Config present | `Settings.XamlStyler` |
**No NuGet package dependencies** in the main application project — all functionality comes from .NET SDK and the embedded `JocysCom.ClassLibrary`.
## 4. Architecture & Runtime Model
This section describes how the application is structured and how it operates at runtime.
FocusLogger is a **single-executable WPF desktop application** that polls Windows API functions to detect focus changes. It does not persist log data between sessions (in-memory only) but provides CSV export for offline analysis.
### Architectural layers
```mermaid
graph TD
subgraph UI["UI Layer (WPF)"]
App["App.xaml.cs Entry point, DPI aware"]
MW["MainWindow.xaml.cs Main window frame"]
DLC["DataListControl.xaml.cs Core logging UI + logic"]
end
subgraph Core["Core Logic"]
DI["DataItem.cs Log entry model"]
DIT["DataItemType.cs Entry type enum"]
NM["NativeMethods.cs P/Invoke declarations"]
CSV["CSV Export BuildCsvContent, CsvEscape"]
end
subgraph Shared["JocysCom.ClassLibrary (embedded)"]
Config["Configuration SettingsData, SettingsItem, AssemblyInfo"]
CompModel["ComponentModel SortableBindingList, BindingListInvoked"]
Controls["Controls ControlsHelper, ItemFormattingConverter, InfoControl, MessageBoxWindow"]
Other["Collections, IO, Text, Runtime, Data"]
end
subgraph WinAPI["Windows OS"]
User32["user32.dll"]
end
App --> MW --> DLC
DLC --> DI
DLC --> NM
DLC --> CSV
NM --> User32
DI --> Config
DLC --> CompModel
DLC --> Controls
MW --> Controls
```
### Key architectural decisions
- **Polling via timer:** A `System.Timers.Timer` with 1ms interval (non-auto-reset) continuously polls `GetActiveWindow()` and `GetForegroundWindow()`. Duplicate events are suppressed via `DataItem.IsSame()`.
- **Thread safety:** Timer fires on a thread-pool thread; UI updates are marshalled via `ControlsHelper.BeginInvoke()`. A `lock(AddLock)` synchronizes the polling logic.
- **Embedded shared library:** `JocysCom.ClassLibrary` files are included directly in `FocusLogger/JocysCom/` rather than as a compiled DLL or NuGet package.
- **No MVVM framework:** Code-behind pattern with data binding. `DataListControl.xaml.cs` contains both view-model-like logic and model interaction.
## 5. Project Inventory
This section lists each project in the solution with its key metadata.
### 5.1 JocysCom.FocusLogger (main application)
| Property | Value |
|----------|-------|
| Path | `FocusLogger/JocysCom.FocusLogger.csproj` |
| Output type | `WinExe` |
| Target framework | `net8.0-windows` |
| Assembly name | `JocysCom.FocusLogger` |
| Description | Find out which process or program is taking the window focus. In game, mouse and keyboard could temporarily stop responding if another program takes the focus. This tool could help diagnose which program is stealing the focus. |
| Version | 1.2.6 |
| NuGet dependencies | None |
| Embedded resources | `Resources/BuildDate.txt` (auto-generated), `Resources/AiAnalysisPrompt.md` |
**Source structure:**
| Directory | Contents |
|-----------|----------|
| `FocusLogger/` (root) | `App.xaml(.cs)`, `MainWindow.xaml(.cs)`, `AssemblyInfo.cs`, `App.ico` |
| `FocusLogger/Common/` | `DataItem.cs`, `DataItemType.cs`, `NativeMethods.cs` |
| `FocusLogger/Controls/` | `DataListControl.xaml(.cs)` — core logging control |
| `FocusLogger/JocysCom/` | Embedded `JocysCom.ClassLibrary` (~30 files across Collections, Common, ComponentModel, Configuration, Controls, Data, IO, Runtime, Text) |
| `FocusLogger/Resources/` | `AiAnalysisPrompt.md`, `BuildDate.txt`, `Icons/` (SVG sources, XAML icons, conversion scripts) |
| `FocusLogger/Properties/` | Publish profiles |
### 5.2 JocysCom.FocusLogger.Tests (test project)
| Property | Value |
|----------|-------|
| Path | `FocusLogger.Tests/JocysCom.FocusLogger.Tests.csproj` |
| Target framework | `net8.0-windows` |
| Test framework | MSTest v3.x |
| Project reference | `FocusLogger/JocysCom.FocusLogger.csproj` |
**Test files:**
| File | Purpose |
|------|---------|
| `CsvExportTests.cs` | Unit tests for `CsvEscape` and `BuildCsvContent` methods |
| `UIAutomationTests.cs` | UI automation tests using `System.Windows.Automation` — launches the built app and interacts with controls by AutomationId |
Run tests with: `dotnet test FocusLogger.Tests/JocysCom.FocusLogger.Tests.csproj`
## 6. Dependency & Data Flow
This section explains how the projects and components relate to each other and how data moves through the system.
### Project dependency graph
```mermaid
graph LR
Tests["FocusLogger.Tests (MSTest v3)"] -->|ProjectReference| App["JocysCom.FocusLogger (WPF App)"]
App -->|embedded source| Shared["JocysCom.ClassLibrary (in FocusLogger/JocysCom/)"]
App -->|P/Invoke| WinAPI["user32.dll"]
```
### Runtime data flow
1. `System.Timers.Timer` fires (1ms interval, non-auto-reset).
2. `DataListControl.UpdateInfo()` acquires `AddLock`.
3. Calls `NativeMethods.GetActiveWindow()` and `NativeMethods.GetForegroundWindow()`.
4. For each handle, `GetItemFromHandle()` creates a `DataItem` with timestamp, focus flags (mouse/keyboard/caret), window title, and window class name.
5. `IsSame()` checks if the event differs from the previous one; if not, it is skipped.
6. `UpdateFromProcess()` enriches the `DataItem` with process name and path (with error handling for restricted processes).
7. The item is inserted at position 0 of `SortableBindingList` via `ControlsHelper.BeginInvoke()` (UI thread dispatch).
8. The WPF `DataGrid` updates via data binding. `ItemFormattingConverter` translates boolean flags to icons.
### CSV export flow
1. User clicks "Save CSV" button.
2. `SaveFileDialog` prompts for file location.
3. `BuildCsvContent()` iterates all `DataItem` entries, writing CSV with headers: Date, PID, Process Name, Active, Mouse, Keyboard, Caret, Window Title, Window Class, Path.
4. File is written as UTF-8.
5. "Explore" button opens the saved file location in Explorer.
6. "AI Prompt Example" button shows the embedded `AiAnalysisPrompt.md` in a `MessageBoxWindow` for users to copy and paste into an AI assistant along with their CSV.
## 7. Build, Test, CI/CD & Operational Workflows
This section documents how the project is built, tested, and released based on repository evidence.
### Build
```bash
dotnet build JocysCom.FocusLogger.slnx
```
- **Pre-build event:** Generates `Resources/BuildDate.txt` with the current ISO 8601 timestamp via PowerShell.
- **Output:** Single `JocysCom.FocusLogger.exe` in `bin/{Configuration}/net8.0-windows/`.
- **Debug configuration:** Embedded PDB symbols (`DebugType: embedded`).
### Test
```bash
dotnet test FocusLogger.Tests/JocysCom.FocusLogger.Tests.csproj
```
- **Framework:** MSTest v3.x with Microsoft.NET.Test.Sdk 17.x.
- **Unit tests:** `CsvExportTests` — validates CSV escaping and content generation.
- **UI automation tests:** `UIAutomationTests` — launches the built application and interacts via `System.Windows.Automation`. Requires a prior build of the main project.
### Release / packaging scripts
| Script | Purpose |
|--------|---------|
| `Documents/App_1_Sign.ps1` | Code-signs the application executable. |
| `Documents/App_2_Zip.ps1` | Packages the signed executable into a release ZIP. |
| `Resources/ZipFiles.ps1` | Shared utility for checksum-aware ZIP creation (compares source/dest checksums before rebuilding). |
| `Documents/Take_Screenshot.ps1` | Captures application screenshot for documentation. |
| `Documents/Take_Screenshot.ps1.cs` | C# helper compiled by the screenshot script. |
| `Solution_Cleanup.ps1` | Cleans `bin/`, `obj/`, and other build artifacts. |
### Icon workflow
SVG icon sources are stored in `FocusLogger/Resources/Icons/Icons_Default/`. The script `Convert_SVG_to_XAML.ps1` converts them to XAML resource dictionaries (`Icons_Default.xaml`).
### CI/CD
No CI/CD workflow files were found under `.github/workflows/`. Builds and releases appear to be performed locally.
## 8. Documentation Map
This section identifies where documentation lives in the repository.
| Location | Audience | Content |
|----------|----------|---------|
| `README.md` | End users, contributors | Project overview, download link, system requirements, screenshot |
| `SECURITY.md` | Security researchers | Vulnerability reporting policy |
| `LICENSE` | All | GPLv3 full text |
| `.ai/ReadMe.md` | AI agents, developers | Explains the purpose of each file in the `.ai/` directory and how custom instructions work for Copilot, CLINE, and Codex |
| `.ai/instructions.md` | AI agents | Role definition and output formatting rules for AI-assisted edits |
| `.ai/coding-guideline.instructions.md` | AI agents | Detailed coding guidelines, source control conventions (branch/PR naming), testing workflow, and constraints for AI agents |
| `.ai/repository-analysis.instructions.md` | AI agents, developers | This file — comprehensive repository reference |
| `.ai/skills/` | AI agents | Skill definitions (e.g., `ai-self-improvement`) for agent-assisted workflows |
| `FocusLogger/Resources/AiAnalysisPrompt.md` | End users | Prompt template for users to paste into AI assistants alongside exported CSV logs |
| `Documents/Images/` | README, users | Application screenshot |
| `Settings.XamlStyler` | Developers | XamlStyler formatting configuration |
### Documentation taxonomy
```mermaid
graph TD
subgraph EndUsers["End-User Documentation"]
README["README.md Overview, download, requirements"]
SECURITY["SECURITY.md Vulnerability reporting"]
LICENSE["LICENSE GPLv3"]
AiPrompt["FocusLogger/Resources/ AiAnalysisPrompt.md Prompt template for CSV analysis"]
Screenshot["Documents/Images/ Application screenshot"]
end
subgraph AIAgents["AI Agent Instructions"]
AiReadMe[".ai/ReadMe.md Directory guide"]
AiInstr[".ai/instructions.md Role & output rules"]
AiCoding[".ai/coding-guideline .instructions.md Coding & SCM conventions"]
AiRepo[".ai/repository-analysis .instructions.md This file"]
AiSkills[".ai/skills/ Agent skill definitions"]
end
subgraph DevConfig["Developer Configuration"]
XamlStyler["Settings.XamlStyler XAML formatting"]
end
```
## 9. AI-Agent-Relevant Conventions and Constraints
This section captures rules and patterns that materially affect automated edits. The full set of coding and source-control conventions is defined in `.ai/coding-guideline.instructions.md`; the highlights below are the items most likely to cause mistakes if overlooked.
1. **Coding style:** Follow Microsoft C# conventions. PascalCase for public members, camelCase for locals. Some private fields use `_PascalCase` (e.g., `_Date`). Preserve existing naming patterns in each file. Prefer the simplest viable solution; avoid over-engineering.
2. **Doc comments:** Per `.ai/instructions.md`, convert simple comments to XML documentation comments where beneficial for IntelliSense. Do not alter surrounding code when doing so.
3. **Shared library files (`FocusLogger/JocysCom/`):** These are embedded from a shared `JocysCom.ClassLibrary`. Exercise caution when editing — changes here may diverge from the upstream library.
4. **XAML formatting:** The repository uses XamlStyler (see `Settings.XamlStyler`). XAML edits should conform to the configured style.
5. **No NuGet packages in the main app:** All dependencies are framework-provided or embedded source. Do not introduce NuGet package dependencies without explicit approval.
6. **Test project uses MSTest v3:** New tests should follow MSTest v3 patterns (`[TestClass]`, `[TestMethod]`, `Assert.*`).
7. **UI automation tests depend on a built executable:** `UIAutomationTests` locate the app at a relative path from the test output. Building the main project before running these tests is required.
8. **Pre-build event:** The csproj generates `Resources/BuildDate.txt` via PowerShell. This file should not be manually edited or committed.
9. **Solution format:** Uses `.slnx` (XML-based solution format), not the older `.sln` text format.
10. **No CI/CD pipelines:** All build and release steps are manual/local. Scripts in `Documents/` handle signing and packaging.
11. **File size limit:** No code file that is created or modified may exceed 6000 tokens (~24 KB).
12. **Source control conventions:** Issues use `{CATEGORY}: {Description}` (FEAT, FIX, TECH, DOCS). Branches use `{CATEGORY}-{issue#}-{lowercase-dashed-name}`. PRs use `PR: #{issue#}: {CATEGORY}: {Description}` and must reference the issue with `Closes #{issue#}`. Merge via PR only — no direct pushes to `main`.
13. **No Co-Authored-By lines:** Do not add `Co-Authored-By` AI model/company lines to commits.
14. **Issue closure:** Never close issues before a release is published with the fix.
15. **No duplication / SSOT:** Update or move existing code instead of adding parallel implementations. If introducing a replacement, remove the old one in the same change.
16. **Stability primitives:** When the repo provides an established script or command, use it verbatim. Only deviate when it is proven broken, and then fix the primitive rather than inventing a parallel path.
================================================
FILE: .claude/skills/ai-self-improvement/SKILL.md
================================================
---
name: ai-self-improvement
description: Update, create, improve, and synchronise this repository's AI agent instructions and related assets (including skills). Use when the user asks to create or edit a skill/SKILL.md, modify the agent's own instructions/processes, restructure instruction governance, migrate instruction content into skills, or run/adjust the sync pipeline that publishes `.ai/` sources into agent-specific folders. Load this skill before writing any SKILL.md, .instructions.md, or touching any skills/ folder (.ai/, .claude/, .roo/, .github/). It tells you the correct location (.ai/) and the sync step, so files end up in the right place.
---
# AI Self-Improvement (Instructions + Skills)
## Critical: `.ai/` is the Primary Source for ALL Agents
The `.ai/` folder is the **single source of truth** for all AI agent configurations in this repository. This applies to:
- **CLINE / Roo Code** — synced to `.roo/rules/` and `.roo/skills/`
- **GitHub Copilot** — synced to `.github/copilot-instructions.md`
- **OpenAI Codex / AGENTS.md** — synced to `AGENTS.md` at repo root
- **Claude Code** — synced to `.claude/*.instructions.md` and `.claude/skills/`
**IMPORTANT:** When asked to modify skills, instructions, or perform any AI self-improvement task, you MUST:
1. Locate the source file under `.ai/` (not the agent-specific output)
2. Make changes to the `.ai/` source
3. Run the sync script to propagate changes to all agents
## Path Mapping Reference
When you encounter a path in an agent-specific folder, map it to `.ai/`:
| Agent-Specific Path | Source Path (Edit Here) |
|---------------------|------------------------|
| `.roo/rules/*.md` | `.ai/*.instructions.md` |
| `.roo/skills//SKILL.md` | `.ai/skills//SKILL.md` |
| `.github/copilot-instructions.md` | `.ai/instructions.md` (generated) |
| `AGENTS.md` | `.ai/instructions.md` (generated) |
| `.claude/*.instructions.md` | `.ai/*.instructions.md` |
| `.claude/skills//SKILL.md` | `.ai/skills//SKILL.md` |
**Example:** If asked to update `.roo/skills/ai-self-improvement/SKILL.md`, you must edit `.ai/skills/ai-self-improvement/SKILL.md` instead.
## Editable instruction files (sources of truth)
You can update your own instruction files under `.ai/`:
- `.ai/instructions.md` — the main system instructions file
- `.ai/*instructions.md` — additional instruction files (auto-included)
- `.ai/*instructions-detail.md` — detailed instruction files (read only when needed)
- `.ai/skills//SKILL.md` — skill definition files
## Workflow
1. Treat `.ai/` as the **single source of truth** for agent instructions **and skills**.
2. When creating or migrating a skill, create/update it under `.ai/skills/`.
3. Make instruction changes in `.ai/instructions.md` and related `*.instructions.md` / `*.instructions-detail.md` files.
4. Do **not** edit generated outputs directly (they are produced by the sync script):
- `.roo/rules/`
- `.roo/skills/`
- `.github/copilot-instructions.md`
- `AGENTS.md`
- `.claude/`
5. **Test changes before syncing** — verify scripts execute correctly and changes work as expected.
6. After testing, run the sync script to apply to all agents.
## Testing Before Sync
Before running the sync script, always verify your changes work correctly:
- **For script changes**: Execute the modified script and verify output is correct
- **For instruction changes**: Review the markdown renders properly and instructions are clear
- **For skill changes**: Test any bundled tools or scripts included in the skill
**Example**: If you modify a PowerShell script in a skill, run it directly from `.ai/skills//scripts/` to confirm it works before syncing.
## Activation process
After editing instruction files (or master skills), run from repository root:
```powershell
.\.ai\skills\ai-self-improvement\scripts\Sync-AgentAssets.ps1 AUTO
```
This script synchronizes changes from `.ai/` to all agent-specific folders.
## Single source of truth
**Never embed template content in instructions — reference template files instead.**
Example:
- ✅ "Template maintained in `pr/checklist.template.md`"
- ❌ Pasting template content into instructions
## Bundled scripts
- Sync entrypoint (instructions + skills): `.ai/skills/ai-self-improvement/scripts/Sync-AgentAssets.ps1`
================================================
FILE: .claude/skills/ai-self-improvement/scripts/Sync-AgentAssets.ps1
================================================
# Script: Sync-AgentAssets.ps1
# Location: .ai/skills/ai-self-improvement/scripts/Sync-AgentAssets.ps1
# Description:
# Synchronises AI agent instruction files and skills from master sources under `.ai/`.
# - Instructions: copies `*.instructions.md` from `.ai/` into agent-specific outputs.
# - Skills: mirrors `.ai/skills/*` into agent skill folders (e.g. `.roo/skills/*`).
#
# Options for Mode:
# ALL - update all known agent outputs
# AUTO - update only agents that exist in this repository (default usage)
# Or a specific agent name: CLINE, ROO CODE, GitHub CoPilot, OpenAI Codex, Claude Code
param(
[Parameter(Position = 0)]
[string]$Mode,
[switch]$NoClear
)
# Combine remaining args so Windows PowerShell (-File) invocations like:
# Sync-AgentAssets.ps1 GitHub CoPilot
# work the same as:
# Sync-AgentAssets.ps1 "GitHub CoPilot"
if ($args.Count -gt 0) {
$ModeFromArgs = ($args -join ' ')
if (-not $Mode -or $Mode -eq '') {
$Mode = $ModeFromArgs
}
}
# Allow calling via the old filename (if invoked through a copied/renamed script).
# This only affects displayed script name in prompts/logs.
$scriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Path)
# Strict mode
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Ensure-Directory {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -Path $Path -PathType Container)) {
New-Item -ItemType Directory -Force -Path $Path | Out-Null
}
}
# Function to check if instruction files exist in a directory
function Test-HasInstructionFiles {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[string]$Filter = '*instructions.md'
)
if (Test-Path $Path -PathType Container) {
$files = @(Get-ChildItem $Path -Filter $Filter -File -ErrorAction SilentlyContinue)
return ($files.Length -gt 0)
}
return $false
}
# Function to pause at the end (unless -NoWait is specified)
function Invoke-Pause {
Write-Host "Pausing for 2 seconds..."
Start-Sleep -Seconds 2
}
function Copy-FileIfDifferent {
param(
[Parameter(Mandatory = $true)]
[string]$SourcePath,
[Parameter(Mandatory = $true)]
[string]$TargetPath
)
$targetDir = Split-Path -Path $TargetPath -Parent
Ensure-Directory -Path $targetDir
if (-not (Test-Path -Path $TargetPath -PathType Leaf)) {
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
$relative = $TargetPath.Substring($repoRoot.Length + 1)
Write-Host "Created: $relative"
return
}
$srcBytes = [System.IO.File]::ReadAllBytes($SourcePath)
$dstBytes = [System.IO.File]::ReadAllBytes($TargetPath)
if ($srcBytes.Length -eq $dstBytes.Length) {
$same = $true
for ($i = 0; $i -lt $srcBytes.Length; $i++) {
if ($srcBytes[$i] -ne $dstBytes[$i]) { $same = $false; break }
}
if ($same) {
$relative = $TargetPath.Substring($repoRoot.Length + 1)
Write-Host "Up-to-date: $relative"
return
}
}
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
$relative = $TargetPath.Substring($repoRoot.Length + 1)
Write-Host "Updated: $relative"
}
function Get-TextAuto {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
# .NET StreamReader detects BOM for UTF-8/UTF-16/UTF-32 automatically.
$sr = New-Object System.IO.StreamReader($Path, $true)
try {
return $sr.ReadToEnd()
}
finally {
$sr.Dispose()
}
}
function Write-Utf8NoBom {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Content
)
$dir = Split-Path -Path $Path -Parent
Ensure-Directory -Path $dir
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
}
function Assert-InstructionSync {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDirectory,
[Parameter(Mandatory = $true)]
[string]$TargetDirectory,
[Parameter(Mandatory = $true)]
[System.IO.FileSystemInfo[]]$SourceFiles
)
$srcDir = Join-Path $repoRoot $SourceDirectory
$dstDir = Join-Path $repoRoot $TargetDirectory
foreach ($sourceFile in $SourceFiles) {
$srcPath = Join-Path $srcDir $sourceFile.Name
$dstPath = Join-Path $dstDir $sourceFile.Name
if (-not (Test-Path $dstPath -PathType Leaf)) {
throw "Binary comparison failed. Destination file missing: $dstPath"
}
$srcBytes = [System.IO.File]::ReadAllBytes($srcPath)
$dstBytes = [System.IO.File]::ReadAllBytes($dstPath)
if ($srcBytes.Length -ne $dstBytes.Length) {
throw "Binary comparison failed. Source and target size mismatch in binary: Source: $srcPath Target: $dstPath"
}
for ($i = 0; $i -lt $srcBytes.Length; $i++) {
if ($srcBytes[$i] -ne $dstBytes[$i]) {
throw "Binary comparison failed. Source and target content mismatch in binary: Source: $srcPath Target: $dstPath"
}
}
}
}
# Function to update agents that use multiple separate instruction files
function Update-MultipleFileAgent {
param(
[Parameter(Mandatory = $true)]
[string]$AgentName,
[Parameter(Mandatory = $true)]
[string]$TargetDirectory,
[Parameter(Mandatory = $true)]
[System.IO.FileSystemInfo[]]$SourceFiles,
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
Write-Host "`r`n--- Updating $AgentName Instructions ---"
$targetDir = Join-Path $RepoRoot $TargetDirectory
foreach ($sourceFile in $SourceFiles) {
$targetFile = Join-Path $targetDir $sourceFile.Name
Copy-FileIfDifferent -SourcePath $sourceFile.FullName -TargetPath $targetFile
}
Assert-InstructionSync -SourceDirectory ".ai" -TargetDirectory $TargetDirectory -SourceFiles $SourceFiles
}
# Function to update agents that use a single combined instruction file
function Update-SingleFileAgent {
param(
[Parameter(Mandatory = $true)]
[string]$AgentName,
[Parameter(Mandatory = $true)]
[string]$TargetFilePath,
[Parameter(Mandatory = $true)]
[System.IO.FileSystemInfo[]]$SourceFiles,
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
Write-Host "`r`n--- Updating $AgentName Instructions ---"
$targetFile = Join-Path $RepoRoot $TargetFilePath
$relativeTarget = $targetFile.Substring($repoRoot.Length + 1)
$allInstructionsContent = New-Object System.Text.StringBuilder
$firstFile = $true
foreach ($sourceFile in $SourceFiles) {
$sourceContent = Get-TextAuto -Path $sourceFile.FullName
if ([string]::IsNullOrWhiteSpace($sourceContent)) {
Write-Warning "Skipping empty file: $($sourceFile.Name)"
continue
}
if (-not $firstFile) {
[void]$allInstructionsContent.AppendLine("")
}
[void]$allInstructionsContent.AppendLine("==== START OF INSTRUCTIONS FROM: $($sourceFile.Name) ====")
[void]$allInstructionsContent.AppendLine("")
[void]$allInstructionsContent.AppendLine("# Instructions from: $($sourceFile.Name)")
[void]$allInstructionsContent.AppendLine("")
[void]$allInstructionsContent.AppendLine($sourceContent.Trim())
[void]$allInstructionsContent.AppendLine("")
[void]$allInstructionsContent.AppendLine("==== END OF INSTRUCTIONS FROM: $($sourceFile.Name) ====")
$firstFile = $false
}
$finalContent = $allInstructionsContent.ToString()
$existing = if (Test-Path -Path $targetFile -PathType Leaf) { Get-TextAuto -Path $targetFile } else { $null }
if ($null -ne $existing -and $existing -eq $finalContent) {
Write-Host "Up-to-date: $relativeTarget"
return
}
Write-Utf8NoBom -Path $targetFile -Content $finalContent
Write-Host "Updated: $relativeTarget"
}
function Invoke-RoboCopyMirror {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDirectory,
[Parameter(Mandatory = $true)]
[string]$DestinationDirectory,
[Parameter(Mandatory = $true)]
[string]$Label
)
if (-not (Test-Path $SourceDirectory -PathType Container)) {
Write-Host "No skills folder found at: $SourceDirectory"
return
}
Ensure-Directory -Path $DestinationDirectory
Write-Host "`r`n--- Mirroring skills to $Label ---"
Write-Host "Source: $SourceDirectory"
Write-Host "Destination: $DestinationDirectory"
# /MIR = mirror (copy + delete removed)
# /FFT = tolerate 2s timestamp granularity
# /R:1 /W:1 = retry quickly
# /NFL/NDL = no file/dir listing (keep output compact)
# /NJH/NJS = no job header/summary
# /NP = no progress
# /XD = exclude version control/build dirs
$excludedDirs = @('.git', '.vs', 'bin', 'obj')
$args = @(
$SourceDirectory,
$DestinationDirectory,
'/MIR',
'/FFT',
'/R:1',
'/W:1',
'/NFL',
'/NDL',
'/NJH',
'/NJS',
'/NP'
)
foreach ($d in $excludedDirs) {
$args += '/XD'
$args += $d
}
$exe = 'robocopy'
# Do not echo the full robocopy command; it is noisy and can wrap in some terminals.
Write-Host "robocopy /MIR /NFL /NDL /NJH /NJS /NP ..."
& $exe @args | Out-Null
$exitCode = $LASTEXITCODE
# Robocopy uses bitmask exit codes.
# 0-7 are success with various flags; >= 8 indicates failure.
if ($exitCode -ge 8) {
throw "Robocopy failed with exit code $exitCode. Command: $cmd"
}
# IMPORTANT: robocopy returns 1+ for successful copies.
# Ensure PowerShell script does not propagate a non-zero exit code for success cases.
$global:LASTEXITCODE = 0
Write-Host "Mirrored skills to $Label (robocopy exit code $exitCode)."
}
function Sync-SkillsToRoo {
param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
$srcSkillsRoot = Join-Path $RepoRoot ".ai\skills"
$rooSkillsRoot = Join-Path $RepoRoot ".roo\skills"
Invoke-RoboCopyMirror -SourceDirectory $srcSkillsRoot -DestinationDirectory $rooSkillsRoot -Label "Roo (.roo\\skills)"
}
function Sync-SkillsToGitHub {
param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
$srcSkillsRoot = Join-Path $RepoRoot ".ai\skills"
$githubSkillsRoot = Join-Path $RepoRoot ".github\skills"
Invoke-RoboCopyMirror -SourceDirectory $srcSkillsRoot -DestinationDirectory $githubSkillsRoot -Label "GitHub (.github\\skills)"
}
function Sync-SkillsToClaude {
param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
$srcSkillsRoot = Join-Path $RepoRoot ".ai\skills"
$claudeSkillsRoot = Join-Path $RepoRoot ".claude\skills"
Invoke-RoboCopyMirror -SourceDirectory $srcSkillsRoot -DestinationDirectory $claudeSkillsRoot -Label "Claude Code (.claude\\skills)"
}
# --- Main Script ---
if (-not $NoClear) {
Clear-Host
}
# We are located under `.ai/skills//tools`. Find repo root by going up 4 levels.
$scriptDir = $PSScriptRoot
$repoRoot = (Join-Path -Path $scriptDir -ChildPath "..\..\..\.." | Resolve-Path).Path
# `.ai` folder path
$aiDir = Join-Path $repoRoot ".ai"
# Discover source files matching *instructions.md in the .ai folder
[System.IO.FileSystemInfo[]]$sourceInstructionFiles = Get-ChildItem -Path $aiDir -Filter "*instructions.md" -File | Sort-Object Name
if ($null -eq $sourceInstructionFiles -or $sourceInstructionFiles.Length -eq 0) {
Write-Warning "No '*instructions.md' files found in '$aiDir'. Nothing to process."
exit 0
}
Write-Host "Found the following source instruction files in '$aiDir':"
$sourceInstructionFiles | ForEach-Object { Write-Host "- $($_.Name)" }
# Mode parameter handling: if 'ALL' or 'AUTO', skip interactive prompt
if ($Mode -eq 'ALL') {
Write-Host "Selected: ALL (parameter mode)"
$updateCline = $true
$updateCopilot = $true
$updateRooCode = $true
$updateCodex = $true
$updateClaude = $true
}
elseif ($Mode -eq 'AUTO') {
Write-Host "Selected: AUTO (parameter mode)"
# Determine available agents based on instruction files
$updateCline = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.clinerules')
$updateRooCode = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.roo\rules')
$updateCopilot = Test-Path (Join-Path $repoRoot '.github\copilot-instructions.md') -PathType Leaf
$updateCodex = Test-Path (Join-Path $repoRoot 'AGENTS.md') -PathType Leaf
$updateClaude = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.claude')
Write-Host "Agents to update based on available instruction files:"
if ($updateCline) { Write-Host "- CLINE" }
if ($updateRooCode) { Write-Host "- ROO CODE" }
if ($updateCopilot) { Write-Host "- GitHub CoPilot" }
if ($updateCodex) { Write-Host "- OpenAI Codex" }
if ($updateClaude) { Write-Host "- Claude Code" }
}
elseif ($Mode -and $Mode -ne '') {
# Specific agent mode (e.g., CLINE, "ROO CODE", etc.)
$updateCline = ($Mode -eq 'CLINE')
$updateCopilot = ($Mode -eq 'GitHub CoPilot')
$updateRooCode = ($Mode -eq 'ROO CODE')
$updateCodex = ($Mode -eq 'OpenAI Codex')
$updateClaude = ($Mode -eq 'Claude Code')
Write-Host "Selected: $Mode (parameter mode)"
}
else {
# User prompt for agent selection
# Detect available agents for interactive menu
$hasCline = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.clinerules')
$hasRooCode = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.roo\rules')
$hasCopilot = Test-Path (Join-Path $repoRoot '.github\copilot-instructions.md') -PathType Leaf
$hasCodex = Test-Path (Join-Path $repoRoot 'AGENTS.md') -PathType Leaf
$hasClaude = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.claude')
Write-Host "`r`nDetected AI agents with instruction files:"
if ($hasCline) { Write-Host "- CLINE" }
if ($hasRooCode) { Write-Host "- ROO CODE" }
if ($hasCopilot) { Write-Host "- GitHub CoPilot" }
if ($hasCodex) { Write-Host "- OpenAI Codex" }
if ($hasClaude) { Write-Host "- Claude Code" }
Write-Host ""
Write-Host "=============================================================="
Write-Host "Select Agent Instruction Set to Update"
Write-Host "--------------------------------------------------------------"
Write-Host "1. AUTO - Update only agents with instruction files (default)"
Write-Host "2. ALL - Update instructions for all AI agents"
Write-Host "3. CLINE - Update instructions for CLINE"
Write-Host "4. ROO CODE - Update instructions for ROO CODE"
Write-Host "5. GitHub CoPilot - Update instructions for GitHub CoPilot"
Write-Host "6. OpenAI Codex - Update instructions for OpenAI Codex"
Write-Host "7. Claude Code - Update instructions for Claude Code"
Write-Host "0. Exit"
Write-Host "=============================================================="
$selection = Read-Host "Enter the number of your choice (0-7)"
# Initialize flags
$updateCline = $false
$updateCopilot = $false
$updateRooCode = $false
$updateCodex = $false
$updateClaude = $false
switch ($selection) {
'1' {
$updateCline = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.clinerules')
$updateRooCode = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.roo\rules')
$updateCopilot = Test-Path (Join-Path $repoRoot '.github\copilot-instructions.md') -PathType Leaf
$updateCodex = Test-Path (Join-Path $repoRoot 'AGENTS.md') -PathType Leaf
$updateClaude = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.claude')
Write-Host "Selected: AUTO"
}
'2' {
$updateCline = $true
$updateCopilot = $true
$updateRooCode = $true
$updateCodex = $true
$updateClaude = $true
Write-Host "Selected: ALL"
}
'3' { $updateCline = $true; Write-Host "Selected: CLINE" }
'4' { $updateRooCode = $true; Write-Host "Selected: ROO CODE" }
'5' { $updateCopilot = $true; Write-Host "Selected: GitHub CoPilot" }
'6' { $updateCodex = $true; Write-Host "Selected: OpenAI Codex" }
'7' { $updateClaude = $true; Write-Host "Selected: Claude Code" }
'0' { Write-Host "Operation cancelled by user."; exit 0 }
default { throw "Invalid selection. Exiting." }
}
}
# --- Multiple-File Agent Updates ---
if ($updateCline) {
Update-MultipleFileAgent -AgentName "CLINE" -TargetDirectory ".clinerules" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
if ($updateRooCode) {
Update-MultipleFileAgent -AgentName "ROO CODE" -TargetDirectory ".roo\rules" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
# --- Single-File Agent Updates ---
if ($updateCopilot) {
$copilotTarget = ".github\copilot-instructions.md"
$githubInstructionsDir = Join-Path $repoRoot ".github\instructions"
if (Test-Path $githubInstructionsDir -PathType Container) {
Write-Host "`r`n--- Updating GitHub CoPilot Instructions (folder-based) ---"
$mainName = "instructions.md"
$mainSource = $sourceInstructionFiles | Where-Object { $_.Name -ieq $mainName } | Select-Object -First 1
if ($null -eq $mainSource) {
throw "Expected source '$mainName' under .ai but none found."
}
Copy-FileIfDifferent -SourcePath $mainSource.FullName -TargetPath (Join-Path $repoRoot $copilotTarget)
foreach ($sf in $sourceInstructionFiles) {
if ($sf.Name -ieq $mainName) {
continue
}
$destination = Join-Path $githubInstructionsDir $sf.Name
Copy-FileIfDifferent -SourcePath $sf.FullName -TargetPath $destination
}
}
else {
Update-SingleFileAgent -AgentName "GitHub CoPilot" -TargetFilePath $copilotTarget -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
}
if ($updateCodex) {
Update-SingleFileAgent -AgentName "OpenAI Codex" -TargetFilePath "AGENTS.md" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
# --- Claude Code (multiple-file agent) ---
if ($updateClaude) {
Update-MultipleFileAgent -AgentName "Claude Code" -TargetDirectory ".claude" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
# --- Skills mirroring ---
if ($updateRooCode -or $Mode -eq 'ALL' -or $Mode -eq 'AUTO') {
Sync-SkillsToRoo -RepoRoot $repoRoot
}
# GitHub Copilot: mirror skills to `.github/skills` (Copilot tries to load from there).
if ($updateCopilot -or $Mode -eq 'ALL' -or $Mode -eq 'AUTO') {
Sync-SkillsToGitHub -RepoRoot $repoRoot
}
# Claude Code: mirror skills to `.claude/skills`.
if ($updateClaude -or $Mode -eq 'ALL' -or $Mode -eq 'AUTO') {
Sync-SkillsToClaude -RepoRoot $repoRoot
}
Write-Host "`r`nAll selected operations completed successfully."
# Only pause when launched by double-click (Explorer). In CI / terminal usage, do not pause.
if ($Host.Name -and $Host.Name -notlike '*ConsoleHost*') {
Invoke-Pause
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
# Line endings: Let Git handle normalization via .gitattributes and core.autocrlf
# - Repository stores LF (normalized by Git)
# - Working tree uses OS-native line endings (CRLF on Windows, LF on Unix)
# - Uncomment below to force editors to use LF (useful for LF-everywhere workflows):
# end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Spaces at the end can be meaningful in Markdown (hard line breaks)
[*.md]
trim_trailing_whitespace = false
# Windows PowerShell 5.1 can misinterpret UTF-8 without BOM when non-ASCII exists.
# If you are PowerShell 7+ only, you may prefer charset = utf-8 here.
[*.ps1]
charset = utf-8-bom
================================================
FILE: .github/copilot-instructions.md
================================================
## Role
Your role is to analyze and improve code by making only localized, targeted changes. You must preserve all validated code, comments, and documented workarounds exactly as they appear. Your suggestions should strictly address only the specific issues identified—such as upgrading simple comments to doc comments for IntelliSense—without altering any surrounding context. Additionally, ensure that no obsolete or deprecated methods are introduced during the improvement process, and do not add extraneous comments that do not directly contribute to the code’s logic. Furthermore, ensure code snippets are clearly structured for readability, placing important or user-editable sections at the top when logical, and using clear separators or headings to highlight customization points.
Wherever beneficial, convert simple comments into recognized documentation comment syntax (e.g., JSDoc for JavaScript, XML comments for C#, JavaDoc for Java) that can be parsed by code intelligence tools like IntelliSense.
Maintain the original meaning of these comments, but structure them in a way that provides maximum benefit for automated tools and refactoring methods.
Apply chain-of-thought reasoning to identify code segments best served by doc comments, analyze the existing context of each comment, and then make precise, incremental modifications that enhance IntelliSense compatibility while preserving existing functionality.
## Output
Wrap any and all code—including regular code snippets, inline code segments, outputs, pseudocode, or any text that represents code—in Markdown code blocks with a language identifier (e.g., ```typescript, ```powershell).
================================================
FILE: .github/instructions/coding-guideline.instructions.md
================================================
# Coding Guidelines
- If the qdrant-mcp-server is running, use it for all permanent memory operations (e.g. storing user information).
- After making changes, ALWAYS start a new server for testing.
- Kill all existing related servers from previous testing before starting a new server.
- Prefer the simplest viable solution; avoid over-engineering.
- Do not add broad try/catch or wrapper layers unless required by a failing test or explicit requirement; if you catch, rethrow to preserve the stack.
- Before writing new code, actively look for existing utilities or functions that can be reused instead of duplicated.
- New helper methods or classes must be justified with a clear, documented need for functionality that is unavailable elsewhere in the codebase.
- Always iterate on and reuse existing code instead of creating new implementations.
- Avoid adding layers of abstraction that do not deliver clear value.
- Do not drastically change established patterns before iterating on them.
- No duplication / SSOT: update or move existing code instead of adding parallel implementations. If you introduce a replacement, remove the old one **in the same change**.
- Write code that accounts for different environments (dev, test, and prod).
- Only modify what is explicitly requested or clearly necessary; do **not** create new files or modules unless explicitly requested.
- When fixing bugs, exhaust current implementations before introducing new patterns; if new methods are used, remove the old ones.
- Keep the codebase clean and organized.
- Avoid one-off scripts unless absolutely necessary.
- Use mocks only for tests, not for dev or prod.
- Never add stubbing or fake data in dev or prod environments.
- Never overwrite the .env file without explicit confirmation.
- Focus solely on areas relevant to the task; leave unrelated code untouched.
- Write thorough tests for all major functionality.
- Avoid major changes to the existing architecture unless explicitly instructed.
- Always consider the impact on other methods and areas of the code.
- Prefer to wrap long lines for better readability.
- Preserve existing formatting; limit formatting to lines you changed and match surrounding style. Also remove any unused imports/usings or dead code **introduced by your edits**.
- Stability primitives (repeatability > cleverness): when the repo provides an established way to perform an operation (repo-owned script, documented command snippet, standard PS1 under .ai/Scripts), treat it as the single source of truth. Use it verbatim instead of synthesizing “equivalent” commands. Only deviate when explicitly asked or when the primitive is proven broken in this environment (and then fix the primitive, not invent a parallel path).
- No code file that you **create or modify** may exceed **6000 tokens (~24 KB)** once your changes are applied.
- If your changes alone would push the file past this limit, either trim the change or ask for explicit permission to refactor; do **not** alter unrelated code solely to meet the limit.
- Existing oversized files are left untouched unless the user explicitly requests a refactor.
## Source Control Conventions
Naming conventions for issues, branches, and pull requests.
### Categories
| Category | Use for |
|----------|---------|
| `FEAT` | New features |
| `FIX` | Bug fixes |
| `TECH` | Infrastructure, dependencies, refactoring |
| `DOCS` | Documentation |
### Naming Patterns
| Item | Pattern | Example |
|------------|---------|---------|
| **Issue** | `{CATEGORY}: {Description}` | `FEAT: Download logs to CSV` |
| **Branch** | `{CATEGORY}-{issue#}-{lowercase-dashed-name}` | `FEAT-21-download-logs-to-csv` |
| **PR** | `PR: #{issue#}: {CATEGORY}: {Description}` | `PR: #21: FEAT: Download logs to CSV` |
- Branch names: lowercase, words separated by dashes, non-ASCII replaced with a single dash, derived from issue title but may be shortened.
- PR body must reference the issue with `Closes #{issue#}`.
- Merge via PR only — no direct pushes to `main` or `master`.
- Feature branches are always created from `main` or `master`. Never merge one feature branch into another — only merge `main` if you need to catch up.
- Always create branches from latest origin/main or origin/main, never merge sub-branches, confirm before risky git ops.
- Do not add Co-Authored-By {AI model}/{AI company} lines to commits.
- Never close issues before a release is published with the fix.
Use the following guidelines:
1. Doc Comment Enhancement for IntelliSense
- Replace or augment simple comments with relevant doc comment syntax that is supported by IntelliSense as needed.
- Preserve the original intent and wording of existing comments wherever possible.
2. Code Layout for Clarity
- Place the most important or user-editable sections at the top if logically appropriate.
- Insert headings or separators within the code to clearly delineate where customizations or key logic sections can be adjusted.
3. No Extraneous Code Comments
- Do not include "one-off" or user-directed commentary in the code.
- Confine all clarifications or additional suggestions to explanations outside of the code snippet.
4. Avoid Outdated or Deprecated Methods
- Refrain from introducing or relying on obsolete or deprecated methods and libraries.
- If the current code relies on potentially deprecated approaches, ask for clarification or provide viable, modern alternatives that align with best practices.
5. Testing and Validation
- Suggest running unit tests or simulations on the modified segments to confirm that the changes fix the issue without impacting overall functionality.
- Ensure that any proposed improvements, including doc comment upgrades, integrate seamlessly with the existing codebase.
- After all code modifications, navigate to the affected project directory and build C# then Angular to confirm the application compiles without errors:
cd {PROJECT} && dotnet build {PROJECT}.csproj
cd {PROJECT}/ClientApp && ng build
- Run relevant unit tests if code changes affect core logic.
- If the developer certificate is not trusted, then execute: dotnet dev-certs https --trust
- To launch project use: dotnet watch run --project {PROJECT}/{PROJECT}.csproj --launch-profile "{PROJECT} (NG Build)"
6. Rationale and Explanation
- For every change (including comment conversions), provide a concise explanation detailing how the modification resolves the identified issue while preserving the original design and context.
- Clearly highlight only the modifications made, ensuring that no previously validated progress is altered.
- NOTE: Summarize reasoning for the user, but do NOT expose full chain-of-thought. Keep internal deliberations internal; surface only the concise rationale needed to justify each change.
7. Contextual Analysis
- Use all available context—such as code history, inline documentation, style guidelines—to understand the intended functionality.
- When inspecting an existing file for understanding, prefer reading the whole file in a
single `read_file` call when it comfortably fits in context; switch to targeted slices
only when the file is too large, the tool truncates it, or a specific anchor line is
already known.
- If the role or intent behind a code segment is ambiguous, ask for clarification rather than making assumptions.
8. Targeted, Incremental Changes
- Identify and isolate only the problematic code segments (including places where IntelliSense doc comments can replace simple comments).
- Provide minimal code snippets that address the issue without rewriting larger sections.
- For each suggested code change, explicitly indicate the exact location in the code (e.g., by specifying the function name, class name, line number, or section heading) where the modification should be implemented.
9. Preservation of Context
- Maintain all developer comments, annotations, and workarounds exactly as they appear, transforming them to doc comment format only when it improves IntelliSense support.
- Do not modify or remove any non-code context unless explicitly instructed.
- Avoid introducing new, irrelevant comments in the code.
10. Launching {PROJECT} Correctly:
- Navigate to the {PROJECT} project folder.
- Run the following command to launch the project with live reload and proper debugging configuration:
dotnet watch run --launch-profile "{PROJECT} (NG Build)" --project {PROJECT}/{PROJECT}.csproj
- This command will start the {PROJECT} project on the designated debugging session URL.
- Ensure that any previous {PROJECT} instances are terminated before running this command.
================================================
FILE: .github/instructions/repository-analysis.instructions.md
================================================
# Repository Analysis
## 1. Repository Overview
This document provides a factual reference for the Jocys.com FocusLogger repository, aimed at developers and AI coding agents working on the codebase.
**FocusLogger** is a Windows desktop utility that monitors and logs which process or program takes window focus. It targets users (especially gamers and power users) who experience unexpected focus stealing — where a background process briefly grabs foreground focus, interrupting gameplay or work. The tool logs every focus change with timestamps, process details, window class names, and focus-state flags, allowing users to identify the culprit.
- **Repository:** https://github.com/JocysCom/FocusLogger
- **License:** GNU General Public License v3.0
- **Current version:** 1.2.6
- **Target platform:** Windows 10+ with .NET 8.0
- **Primary audiences:** Gamers, power users, IT support personnel diagnosing focus-stealing issues.
## 2. Top-Level Structure
This section maps every top-level directory and file to help navigate the repository quickly.
| Path | Purpose |
|------|---------|
| `FocusLogger/` | Main application project (WPF, .NET 8.0). Contains all app source code, shared library, and resources. |
| `FocusLogger.Tests/` | MSTest test project. Unit tests for CSV export and UI automation tests. |
| `Documents/` | Release engineering: signing scripts, zip packaging scripts, screenshot tooling, and pre-built release files. |
| `Resources/` | Solution-level shared scripts (currently `ZipFiles.ps1` for checksum-aware zip packaging). |
| `.ai/` | AI agent instructions, coding guidelines, repository analysis, and skills. |
| `JocysCom.FocusLogger.slnx` | Solution file (XML-based `.slnx` format) referencing the two projects. |
| `README.md` | Project overview, download link, system requirements, screenshot. |
| `LICENSE` | GPLv3 license text. |
| `SECURITY.md` | Security vulnerability reporting policy (support@jocys.com). |
| `Settings.XamlStyler` | XamlStyler configuration for consistent XAML formatting. |
| `Solution_Cleanup.ps1` | PowerShell script for cleaning build artifacts. |
## 3. Technology Stack & Key Dependencies
This section lists verified technologies and versions drawn from project files.
| Technology | Version / Detail | Evidence |
|------------|-----------------|----------|
| .NET | 8.0 (`net8.0-windows`) | `JocysCom.FocusLogger.csproj` TargetFramework |
| C# | Implicit (SDK default for .NET 8) | SDK-style project |
| WPF | `true` | csproj |
| Windows Forms interop | `true` | csproj — used for P/Invoke helpers and DPI awareness |
| MSTest | v3.x (`MSTest.TestFramework 3.*`, `MSTest.TestAdapter 3.*`) | Test csproj PackageReference |
| Microsoft.NET.Test.Sdk | 17.x | Test csproj PackageReference |
| Windows API (user32.dll) | P/Invoke | `NativeMethods.cs` |
| PowerShell | Scripts for build, sign, zip, cleanup | `Documents/`, `Resources/`, root |
| XamlStyler | Config present | `Settings.XamlStyler` |
**No NuGet package dependencies** in the main application project — all functionality comes from .NET SDK and the embedded `JocysCom.ClassLibrary`.
## 4. Architecture & Runtime Model
This section describes how the application is structured and how it operates at runtime.
FocusLogger is a **single-executable WPF desktop application** that polls Windows API functions to detect focus changes. It does not persist log data between sessions (in-memory only) but provides CSV export for offline analysis.
### Architectural layers
```mermaid
graph TD
subgraph UI["UI Layer (WPF)"]
App["App.xaml.cs Entry point, DPI aware"]
MW["MainWindow.xaml.cs Main window frame"]
DLC["DataListControl.xaml.cs Core logging UI + logic"]
end
subgraph Core["Core Logic"]
DI["DataItem.cs Log entry model"]
DIT["DataItemType.cs Entry type enum"]
NM["NativeMethods.cs P/Invoke declarations"]
CSV["CSV Export BuildCsvContent, CsvEscape"]
end
subgraph Shared["JocysCom.ClassLibrary (embedded)"]
Config["Configuration SettingsData, SettingsItem, AssemblyInfo"]
CompModel["ComponentModel SortableBindingList, BindingListInvoked"]
Controls["Controls ControlsHelper, ItemFormattingConverter, InfoControl, MessageBoxWindow"]
Other["Collections, IO, Text, Runtime, Data"]
end
subgraph WinAPI["Windows OS"]
User32["user32.dll"]
end
App --> MW --> DLC
DLC --> DI
DLC --> NM
DLC --> CSV
NM --> User32
DI --> Config
DLC --> CompModel
DLC --> Controls
MW --> Controls
```
### Key architectural decisions
- **Polling via timer:** A `System.Timers.Timer` with 1ms interval (non-auto-reset) continuously polls `GetActiveWindow()` and `GetForegroundWindow()`. Duplicate events are suppressed via `DataItem.IsSame()`.
- **Thread safety:** Timer fires on a thread-pool thread; UI updates are marshalled via `ControlsHelper.BeginInvoke()`. A `lock(AddLock)` synchronizes the polling logic.
- **Embedded shared library:** `JocysCom.ClassLibrary` files are included directly in `FocusLogger/JocysCom/` rather than as a compiled DLL or NuGet package.
- **No MVVM framework:** Code-behind pattern with data binding. `DataListControl.xaml.cs` contains both view-model-like logic and model interaction.
## 5. Project Inventory
This section lists each project in the solution with its key metadata.
### 5.1 JocysCom.FocusLogger (main application)
| Property | Value |
|----------|-------|
| Path | `FocusLogger/JocysCom.FocusLogger.csproj` |
| Output type | `WinExe` |
| Target framework | `net8.0-windows` |
| Assembly name | `JocysCom.FocusLogger` |
| Description | Find out which process or program is taking the window focus. In game, mouse and keyboard could temporarily stop responding if another program takes the focus. This tool could help diagnose which program is stealing the focus. |
| Version | 1.2.6 |
| NuGet dependencies | None |
| Embedded resources | `Resources/BuildDate.txt` (auto-generated), `Resources/AiAnalysisPrompt.md` |
**Source structure:**
| Directory | Contents |
|-----------|----------|
| `FocusLogger/` (root) | `App.xaml(.cs)`, `MainWindow.xaml(.cs)`, `AssemblyInfo.cs`, `App.ico` |
| `FocusLogger/Common/` | `DataItem.cs`, `DataItemType.cs`, `NativeMethods.cs` |
| `FocusLogger/Controls/` | `DataListControl.xaml(.cs)` — core logging control |
| `FocusLogger/JocysCom/` | Embedded `JocysCom.ClassLibrary` (~30 files across Collections, Common, ComponentModel, Configuration, Controls, Data, IO, Runtime, Text) |
| `FocusLogger/Resources/` | `AiAnalysisPrompt.md`, `BuildDate.txt`, `Icons/` (SVG sources, XAML icons, conversion scripts) |
| `FocusLogger/Properties/` | Publish profiles |
### 5.2 JocysCom.FocusLogger.Tests (test project)
| Property | Value |
|----------|-------|
| Path | `FocusLogger.Tests/JocysCom.FocusLogger.Tests.csproj` |
| Target framework | `net8.0-windows` |
| Test framework | MSTest v3.x |
| Project reference | `FocusLogger/JocysCom.FocusLogger.csproj` |
**Test files:**
| File | Purpose |
|------|---------|
| `CsvExportTests.cs` | Unit tests for `CsvEscape` and `BuildCsvContent` methods |
| `UIAutomationTests.cs` | UI automation tests using `System.Windows.Automation` — launches the built app and interacts with controls by AutomationId |
Run tests with: `dotnet test FocusLogger.Tests/JocysCom.FocusLogger.Tests.csproj`
## 6. Dependency & Data Flow
This section explains how the projects and components relate to each other and how data moves through the system.
### Project dependency graph
```mermaid
graph LR
Tests["FocusLogger.Tests (MSTest v3)"] -->|ProjectReference| App["JocysCom.FocusLogger (WPF App)"]
App -->|embedded source| Shared["JocysCom.ClassLibrary (in FocusLogger/JocysCom/)"]
App -->|P/Invoke| WinAPI["user32.dll"]
```
### Runtime data flow
1. `System.Timers.Timer` fires (1ms interval, non-auto-reset).
2. `DataListControl.UpdateInfo()` acquires `AddLock`.
3. Calls `NativeMethods.GetActiveWindow()` and `NativeMethods.GetForegroundWindow()`.
4. For each handle, `GetItemFromHandle()` creates a `DataItem` with timestamp, focus flags (mouse/keyboard/caret), window title, and window class name.
5. `IsSame()` checks if the event differs from the previous one; if not, it is skipped.
6. `UpdateFromProcess()` enriches the `DataItem` with process name and path (with error handling for restricted processes).
7. The item is inserted at position 0 of `SortableBindingList` via `ControlsHelper.BeginInvoke()` (UI thread dispatch).
8. The WPF `DataGrid` updates via data binding. `ItemFormattingConverter` translates boolean flags to icons.
### CSV export flow
1. User clicks "Save CSV" button.
2. `SaveFileDialog` prompts for file location.
3. `BuildCsvContent()` iterates all `DataItem` entries, writing CSV with headers: Date, PID, Process Name, Active, Mouse, Keyboard, Caret, Window Title, Window Class, Path.
4. File is written as UTF-8.
5. "Explore" button opens the saved file location in Explorer.
6. "AI Prompt Example" button shows the embedded `AiAnalysisPrompt.md` in a `MessageBoxWindow` for users to copy and paste into an AI assistant along with their CSV.
## 7. Build, Test, CI/CD & Operational Workflows
This section documents how the project is built, tested, and released based on repository evidence.
### Build
```bash
dotnet build JocysCom.FocusLogger.slnx
```
- **Pre-build event:** Generates `Resources/BuildDate.txt` with the current ISO 8601 timestamp via PowerShell.
- **Output:** Single `JocysCom.FocusLogger.exe` in `bin/{Configuration}/net8.0-windows/`.
- **Debug configuration:** Embedded PDB symbols (`DebugType: embedded`).
### Test
```bash
dotnet test FocusLogger.Tests/JocysCom.FocusLogger.Tests.csproj
```
- **Framework:** MSTest v3.x with Microsoft.NET.Test.Sdk 17.x.
- **Unit tests:** `CsvExportTests` — validates CSV escaping and content generation.
- **UI automation tests:** `UIAutomationTests` — launches the built application and interacts via `System.Windows.Automation`. Requires a prior build of the main project.
### Release / packaging scripts
| Script | Purpose |
|--------|---------|
| `Documents/App_1_Sign.ps1` | Code-signs the application executable. |
| `Documents/App_2_Zip.ps1` | Packages the signed executable into a release ZIP. |
| `Resources/ZipFiles.ps1` | Shared utility for checksum-aware ZIP creation (compares source/dest checksums before rebuilding). |
| `Documents/Take_Screenshot.ps1` | Captures application screenshot for documentation. |
| `Documents/Take_Screenshot.ps1.cs` | C# helper compiled by the screenshot script. |
| `Solution_Cleanup.ps1` | Cleans `bin/`, `obj/`, and other build artifacts. |
### Icon workflow
SVG icon sources are stored in `FocusLogger/Resources/Icons/Icons_Default/`. The script `Convert_SVG_to_XAML.ps1` converts them to XAML resource dictionaries (`Icons_Default.xaml`).
### CI/CD
No CI/CD workflow files were found under `.github/workflows/`. Builds and releases appear to be performed locally.
## 8. Documentation Map
This section identifies where documentation lives in the repository.
| Location | Audience | Content |
|----------|----------|---------|
| `README.md` | End users, contributors | Project overview, download link, system requirements, screenshot |
| `SECURITY.md` | Security researchers | Vulnerability reporting policy |
| `LICENSE` | All | GPLv3 full text |
| `.ai/ReadMe.md` | AI agents, developers | Explains the purpose of each file in the `.ai/` directory and how custom instructions work for Copilot, CLINE, and Codex |
| `.ai/instructions.md` | AI agents | Role definition and output formatting rules for AI-assisted edits |
| `.ai/coding-guideline.instructions.md` | AI agents | Detailed coding guidelines, source control conventions (branch/PR naming), testing workflow, and constraints for AI agents |
| `.ai/repository-analysis.instructions.md` | AI agents, developers | This file — comprehensive repository reference |
| `.ai/skills/` | AI agents | Skill definitions (e.g., `ai-self-improvement`) for agent-assisted workflows |
| `FocusLogger/Resources/AiAnalysisPrompt.md` | End users | Prompt template for users to paste into AI assistants alongside exported CSV logs |
| `Documents/Images/` | README, users | Application screenshot |
| `Settings.XamlStyler` | Developers | XamlStyler formatting configuration |
### Documentation taxonomy
```mermaid
graph TD
subgraph EndUsers["End-User Documentation"]
README["README.md Overview, download, requirements"]
SECURITY["SECURITY.md Vulnerability reporting"]
LICENSE["LICENSE GPLv3"]
AiPrompt["FocusLogger/Resources/ AiAnalysisPrompt.md Prompt template for CSV analysis"]
Screenshot["Documents/Images/ Application screenshot"]
end
subgraph AIAgents["AI Agent Instructions"]
AiReadMe[".ai/ReadMe.md Directory guide"]
AiInstr[".ai/instructions.md Role & output rules"]
AiCoding[".ai/coding-guideline .instructions.md Coding & SCM conventions"]
AiRepo[".ai/repository-analysis .instructions.md This file"]
AiSkills[".ai/skills/ Agent skill definitions"]
end
subgraph DevConfig["Developer Configuration"]
XamlStyler["Settings.XamlStyler XAML formatting"]
end
```
## 9. AI-Agent-Relevant Conventions and Constraints
This section captures rules and patterns that materially affect automated edits. The full set of coding and source-control conventions is defined in `.ai/coding-guideline.instructions.md`; the highlights below are the items most likely to cause mistakes if overlooked.
1. **Coding style:** Follow Microsoft C# conventions. PascalCase for public members, camelCase for locals. Some private fields use `_PascalCase` (e.g., `_Date`). Preserve existing naming patterns in each file. Prefer the simplest viable solution; avoid over-engineering.
2. **Doc comments:** Per `.ai/instructions.md`, convert simple comments to XML documentation comments where beneficial for IntelliSense. Do not alter surrounding code when doing so.
3. **Shared library files (`FocusLogger/JocysCom/`):** These are embedded from a shared `JocysCom.ClassLibrary`. Exercise caution when editing — changes here may diverge from the upstream library.
4. **XAML formatting:** The repository uses XamlStyler (see `Settings.XamlStyler`). XAML edits should conform to the configured style.
5. **No NuGet packages in the main app:** All dependencies are framework-provided or embedded source. Do not introduce NuGet package dependencies without explicit approval.
6. **Test project uses MSTest v3:** New tests should follow MSTest v3 patterns (`[TestClass]`, `[TestMethod]`, `Assert.*`).
7. **UI automation tests depend on a built executable:** `UIAutomationTests` locate the app at a relative path from the test output. Building the main project before running these tests is required.
8. **Pre-build event:** The csproj generates `Resources/BuildDate.txt` via PowerShell. This file should not be manually edited or committed.
9. **Solution format:** Uses `.slnx` (XML-based solution format), not the older `.sln` text format.
10. **No CI/CD pipelines:** All build and release steps are manual/local. Scripts in `Documents/` handle signing and packaging.
11. **File size limit:** No code file that is created or modified may exceed 6000 tokens (~24 KB).
12. **Source control conventions:** Issues use `{CATEGORY}: {Description}` (FEAT, FIX, TECH, DOCS). Branches use `{CATEGORY}-{issue#}-{lowercase-dashed-name}`. PRs use `PR: #{issue#}: {CATEGORY}: {Description}` and must reference the issue with `Closes #{issue#}`. Merge via PR only — no direct pushes to `main`.
13. **No Co-Authored-By lines:** Do not add `Co-Authored-By` AI model/company lines to commits.
14. **Issue closure:** Never close issues before a release is published with the fix.
15. **No duplication / SSOT:** Update or move existing code instead of adding parallel implementations. If introducing a replacement, remove the old one in the same change.
16. **Stability primitives:** When the repo provides an established script or command, use it verbatim. Only deviate when it is proven broken, and then fix the primitive rather than inventing a parallel path.
================================================
FILE: .github/skills/ai-self-improvement/SKILL.md
================================================
---
name: ai-self-improvement
description: Update, create, improve, and synchronise this repository's AI agent instructions and related assets (including skills). Use when the user asks to create or edit a skill/SKILL.md, modify the agent's own instructions/processes, restructure instruction governance, migrate instruction content into skills, or run/adjust the sync pipeline that publishes `.ai/` sources into agent-specific folders. Load this skill before writing any SKILL.md, .instructions.md, or touching any skills/ folder (.ai/, .claude/, .roo/, .github/). It tells you the correct location (.ai/) and the sync step, so files end up in the right place.
---
# AI Self-Improvement (Instructions + Skills)
## Critical: `.ai/` is the Primary Source for ALL Agents
The `.ai/` folder is the **single source of truth** for all AI agent configurations in this repository. This applies to:
- **CLINE / Roo Code** — synced to `.roo/rules/` and `.roo/skills/`
- **GitHub Copilot** — synced to `.github/copilot-instructions.md`
- **OpenAI Codex / AGENTS.md** — synced to `AGENTS.md` at repo root
- **Claude Code** — synced to `.claude/*.instructions.md` and `.claude/skills/`
**IMPORTANT:** When asked to modify skills, instructions, or perform any AI self-improvement task, you MUST:
1. Locate the source file under `.ai/` (not the agent-specific output)
2. Make changes to the `.ai/` source
3. Run the sync script to propagate changes to all agents
## Path Mapping Reference
When you encounter a path in an agent-specific folder, map it to `.ai/`:
| Agent-Specific Path | Source Path (Edit Here) |
|---------------------|------------------------|
| `.roo/rules/*.md` | `.ai/*.instructions.md` |
| `.roo/skills//SKILL.md` | `.ai/skills//SKILL.md` |
| `.github/copilot-instructions.md` | `.ai/instructions.md` (generated) |
| `AGENTS.md` | `.ai/instructions.md` (generated) |
| `.claude/*.instructions.md` | `.ai/*.instructions.md` |
| `.claude/skills//SKILL.md` | `.ai/skills//SKILL.md` |
**Example:** If asked to update `.roo/skills/ai-self-improvement/SKILL.md`, you must edit `.ai/skills/ai-self-improvement/SKILL.md` instead.
## Editable instruction files (sources of truth)
You can update your own instruction files under `.ai/`:
- `.ai/instructions.md` — the main system instructions file
- `.ai/*instructions.md` — additional instruction files (auto-included)
- `.ai/*instructions-detail.md` — detailed instruction files (read only when needed)
- `.ai/skills//SKILL.md` — skill definition files
## Workflow
1. Treat `.ai/` as the **single source of truth** for agent instructions **and skills**.
2. When creating or migrating a skill, create/update it under `.ai/skills/`.
3. Make instruction changes in `.ai/instructions.md` and related `*.instructions.md` / `*.instructions-detail.md` files.
4. Do **not** edit generated outputs directly (they are produced by the sync script):
- `.roo/rules/`
- `.roo/skills/`
- `.github/copilot-instructions.md`
- `AGENTS.md`
- `.claude/`
5. **Test changes before syncing** — verify scripts execute correctly and changes work as expected.
6. After testing, run the sync script to apply to all agents.
## Testing Before Sync
Before running the sync script, always verify your changes work correctly:
- **For script changes**: Execute the modified script and verify output is correct
- **For instruction changes**: Review the markdown renders properly and instructions are clear
- **For skill changes**: Test any bundled tools or scripts included in the skill
**Example**: If you modify a PowerShell script in a skill, run it directly from `.ai/skills//scripts/` to confirm it works before syncing.
## Activation process
After editing instruction files (or master skills), run from repository root:
```powershell
.\.ai\skills\ai-self-improvement\scripts\Sync-AgentAssets.ps1 AUTO
```
This script synchronizes changes from `.ai/` to all agent-specific folders.
## Single source of truth
**Never embed template content in instructions — reference template files instead.**
Example:
- ✅ "Template maintained in `pr/checklist.template.md`"
- ❌ Pasting template content into instructions
## Bundled scripts
- Sync entrypoint (instructions + skills): `.ai/skills/ai-self-improvement/scripts/Sync-AgentAssets.ps1`
================================================
FILE: .github/skills/ai-self-improvement/scripts/Sync-AgentAssets.ps1
================================================
# Script: Sync-AgentAssets.ps1
# Location: .ai/skills/ai-self-improvement/scripts/Sync-AgentAssets.ps1
# Description:
# Synchronises AI agent instruction files and skills from master sources under `.ai/`.
# - Instructions: copies `*.instructions.md` from `.ai/` into agent-specific outputs.
# - Skills: mirrors `.ai/skills/*` into agent skill folders (e.g. `.roo/skills/*`).
#
# Options for Mode:
# ALL - update all known agent outputs
# AUTO - update only agents that exist in this repository (default usage)
# Or a specific agent name: CLINE, ROO CODE, GitHub CoPilot, OpenAI Codex, Claude Code
param(
[Parameter(Position = 0)]
[string]$Mode,
[switch]$NoClear
)
# Combine remaining args so Windows PowerShell (-File) invocations like:
# Sync-AgentAssets.ps1 GitHub CoPilot
# work the same as:
# Sync-AgentAssets.ps1 "GitHub CoPilot"
if ($args.Count -gt 0) {
$ModeFromArgs = ($args -join ' ')
if (-not $Mode -or $Mode -eq '') {
$Mode = $ModeFromArgs
}
}
# Allow calling via the old filename (if invoked through a copied/renamed script).
# This only affects displayed script name in prompts/logs.
$scriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Path)
# Strict mode
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Ensure-Directory {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -Path $Path -PathType Container)) {
New-Item -ItemType Directory -Force -Path $Path | Out-Null
}
}
# Function to check if instruction files exist in a directory
function Test-HasInstructionFiles {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[string]$Filter = '*instructions.md'
)
if (Test-Path $Path -PathType Container) {
$files = @(Get-ChildItem $Path -Filter $Filter -File -ErrorAction SilentlyContinue)
return ($files.Length -gt 0)
}
return $false
}
# Function to pause at the end (unless -NoWait is specified)
function Invoke-Pause {
Write-Host "Pausing for 2 seconds..."
Start-Sleep -Seconds 2
}
function Copy-FileIfDifferent {
param(
[Parameter(Mandatory = $true)]
[string]$SourcePath,
[Parameter(Mandatory = $true)]
[string]$TargetPath
)
$targetDir = Split-Path -Path $TargetPath -Parent
Ensure-Directory -Path $targetDir
if (-not (Test-Path -Path $TargetPath -PathType Leaf)) {
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
$relative = $TargetPath.Substring($repoRoot.Length + 1)
Write-Host "Created: $relative"
return
}
$srcBytes = [System.IO.File]::ReadAllBytes($SourcePath)
$dstBytes = [System.IO.File]::ReadAllBytes($TargetPath)
if ($srcBytes.Length -eq $dstBytes.Length) {
$same = $true
for ($i = 0; $i -lt $srcBytes.Length; $i++) {
if ($srcBytes[$i] -ne $dstBytes[$i]) { $same = $false; break }
}
if ($same) {
$relative = $TargetPath.Substring($repoRoot.Length + 1)
Write-Host "Up-to-date: $relative"
return
}
}
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
$relative = $TargetPath.Substring($repoRoot.Length + 1)
Write-Host "Updated: $relative"
}
function Get-TextAuto {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
# .NET StreamReader detects BOM for UTF-8/UTF-16/UTF-32 automatically.
$sr = New-Object System.IO.StreamReader($Path, $true)
try {
return $sr.ReadToEnd()
}
finally {
$sr.Dispose()
}
}
function Write-Utf8NoBom {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Content
)
$dir = Split-Path -Path $Path -Parent
Ensure-Directory -Path $dir
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
}
function Assert-InstructionSync {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDirectory,
[Parameter(Mandatory = $true)]
[string]$TargetDirectory,
[Parameter(Mandatory = $true)]
[System.IO.FileSystemInfo[]]$SourceFiles
)
$srcDir = Join-Path $repoRoot $SourceDirectory
$dstDir = Join-Path $repoRoot $TargetDirectory
foreach ($sourceFile in $SourceFiles) {
$srcPath = Join-Path $srcDir $sourceFile.Name
$dstPath = Join-Path $dstDir $sourceFile.Name
if (-not (Test-Path $dstPath -PathType Leaf)) {
throw "Binary comparison failed. Destination file missing: $dstPath"
}
$srcBytes = [System.IO.File]::ReadAllBytes($srcPath)
$dstBytes = [System.IO.File]::ReadAllBytes($dstPath)
if ($srcBytes.Length -ne $dstBytes.Length) {
throw "Binary comparison failed. Source and target size mismatch in binary: Source: $srcPath Target: $dstPath"
}
for ($i = 0; $i -lt $srcBytes.Length; $i++) {
if ($srcBytes[$i] -ne $dstBytes[$i]) {
throw "Binary comparison failed. Source and target content mismatch in binary: Source: $srcPath Target: $dstPath"
}
}
}
}
# Function to update agents that use multiple separate instruction files
function Update-MultipleFileAgent {
param(
[Parameter(Mandatory = $true)]
[string]$AgentName,
[Parameter(Mandatory = $true)]
[string]$TargetDirectory,
[Parameter(Mandatory = $true)]
[System.IO.FileSystemInfo[]]$SourceFiles,
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
Write-Host "`r`n--- Updating $AgentName Instructions ---"
$targetDir = Join-Path $RepoRoot $TargetDirectory
foreach ($sourceFile in $SourceFiles) {
$targetFile = Join-Path $targetDir $sourceFile.Name
Copy-FileIfDifferent -SourcePath $sourceFile.FullName -TargetPath $targetFile
}
Assert-InstructionSync -SourceDirectory ".ai" -TargetDirectory $TargetDirectory -SourceFiles $SourceFiles
}
# Function to update agents that use a single combined instruction file
function Update-SingleFileAgent {
param(
[Parameter(Mandatory = $true)]
[string]$AgentName,
[Parameter(Mandatory = $true)]
[string]$TargetFilePath,
[Parameter(Mandatory = $true)]
[System.IO.FileSystemInfo[]]$SourceFiles,
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
Write-Host "`r`n--- Updating $AgentName Instructions ---"
$targetFile = Join-Path $RepoRoot $TargetFilePath
$relativeTarget = $targetFile.Substring($repoRoot.Length + 1)
$allInstructionsContent = New-Object System.Text.StringBuilder
$firstFile = $true
foreach ($sourceFile in $SourceFiles) {
$sourceContent = Get-TextAuto -Path $sourceFile.FullName
if ([string]::IsNullOrWhiteSpace($sourceContent)) {
Write-Warning "Skipping empty file: $($sourceFile.Name)"
continue
}
if (-not $firstFile) {
[void]$allInstructionsContent.AppendLine("")
}
[void]$allInstructionsContent.AppendLine("==== START OF INSTRUCTIONS FROM: $($sourceFile.Name) ====")
[void]$allInstructionsContent.AppendLine("")
[void]$allInstructionsContent.AppendLine("# Instructions from: $($sourceFile.Name)")
[void]$allInstructionsContent.AppendLine("")
[void]$allInstructionsContent.AppendLine($sourceContent.Trim())
[void]$allInstructionsContent.AppendLine("")
[void]$allInstructionsContent.AppendLine("==== END OF INSTRUCTIONS FROM: $($sourceFile.Name) ====")
$firstFile = $false
}
$finalContent = $allInstructionsContent.ToString()
$existing = if (Test-Path -Path $targetFile -PathType Leaf) { Get-TextAuto -Path $targetFile } else { $null }
if ($null -ne $existing -and $existing -eq $finalContent) {
Write-Host "Up-to-date: $relativeTarget"
return
}
Write-Utf8NoBom -Path $targetFile -Content $finalContent
Write-Host "Updated: $relativeTarget"
}
function Invoke-RoboCopyMirror {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDirectory,
[Parameter(Mandatory = $true)]
[string]$DestinationDirectory,
[Parameter(Mandatory = $true)]
[string]$Label
)
if (-not (Test-Path $SourceDirectory -PathType Container)) {
Write-Host "No skills folder found at: $SourceDirectory"
return
}
Ensure-Directory -Path $DestinationDirectory
Write-Host "`r`n--- Mirroring skills to $Label ---"
Write-Host "Source: $SourceDirectory"
Write-Host "Destination: $DestinationDirectory"
# /MIR = mirror (copy + delete removed)
# /FFT = tolerate 2s timestamp granularity
# /R:1 /W:1 = retry quickly
# /NFL/NDL = no file/dir listing (keep output compact)
# /NJH/NJS = no job header/summary
# /NP = no progress
# /XD = exclude version control/build dirs
$excludedDirs = @('.git', '.vs', 'bin', 'obj')
$args = @(
$SourceDirectory,
$DestinationDirectory,
'/MIR',
'/FFT',
'/R:1',
'/W:1',
'/NFL',
'/NDL',
'/NJH',
'/NJS',
'/NP'
)
foreach ($d in $excludedDirs) {
$args += '/XD'
$args += $d
}
$exe = 'robocopy'
# Do not echo the full robocopy command; it is noisy and can wrap in some terminals.
Write-Host "robocopy /MIR /NFL /NDL /NJH /NJS /NP ..."
& $exe @args | Out-Null
$exitCode = $LASTEXITCODE
# Robocopy uses bitmask exit codes.
# 0-7 are success with various flags; >= 8 indicates failure.
if ($exitCode -ge 8) {
throw "Robocopy failed with exit code $exitCode. Command: $cmd"
}
# IMPORTANT: robocopy returns 1+ for successful copies.
# Ensure PowerShell script does not propagate a non-zero exit code for success cases.
$global:LASTEXITCODE = 0
Write-Host "Mirrored skills to $Label (robocopy exit code $exitCode)."
}
function Sync-SkillsToRoo {
param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
$srcSkillsRoot = Join-Path $RepoRoot ".ai\skills"
$rooSkillsRoot = Join-Path $RepoRoot ".roo\skills"
Invoke-RoboCopyMirror -SourceDirectory $srcSkillsRoot -DestinationDirectory $rooSkillsRoot -Label "Roo (.roo\\skills)"
}
function Sync-SkillsToGitHub {
param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
$srcSkillsRoot = Join-Path $RepoRoot ".ai\skills"
$githubSkillsRoot = Join-Path $RepoRoot ".github\skills"
Invoke-RoboCopyMirror -SourceDirectory $srcSkillsRoot -DestinationDirectory $githubSkillsRoot -Label "GitHub (.github\\skills)"
}
function Sync-SkillsToClaude {
param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot
)
$srcSkillsRoot = Join-Path $RepoRoot ".ai\skills"
$claudeSkillsRoot = Join-Path $RepoRoot ".claude\skills"
Invoke-RoboCopyMirror -SourceDirectory $srcSkillsRoot -DestinationDirectory $claudeSkillsRoot -Label "Claude Code (.claude\\skills)"
}
# --- Main Script ---
if (-not $NoClear) {
Clear-Host
}
# We are located under `.ai/skills//tools`. Find repo root by going up 4 levels.
$scriptDir = $PSScriptRoot
$repoRoot = (Join-Path -Path $scriptDir -ChildPath "..\..\..\.." | Resolve-Path).Path
# `.ai` folder path
$aiDir = Join-Path $repoRoot ".ai"
# Discover source files matching *instructions.md in the .ai folder
[System.IO.FileSystemInfo[]]$sourceInstructionFiles = Get-ChildItem -Path $aiDir -Filter "*instructions.md" -File | Sort-Object Name
if ($null -eq $sourceInstructionFiles -or $sourceInstructionFiles.Length -eq 0) {
Write-Warning "No '*instructions.md' files found in '$aiDir'. Nothing to process."
exit 0
}
Write-Host "Found the following source instruction files in '$aiDir':"
$sourceInstructionFiles | ForEach-Object { Write-Host "- $($_.Name)" }
# Mode parameter handling: if 'ALL' or 'AUTO', skip interactive prompt
if ($Mode -eq 'ALL') {
Write-Host "Selected: ALL (parameter mode)"
$updateCline = $true
$updateCopilot = $true
$updateRooCode = $true
$updateCodex = $true
$updateClaude = $true
}
elseif ($Mode -eq 'AUTO') {
Write-Host "Selected: AUTO (parameter mode)"
# Determine available agents based on instruction files
$updateCline = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.clinerules')
$updateRooCode = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.roo\rules')
$updateCopilot = Test-Path (Join-Path $repoRoot '.github\copilot-instructions.md') -PathType Leaf
$updateCodex = Test-Path (Join-Path $repoRoot 'AGENTS.md') -PathType Leaf
$updateClaude = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.claude')
Write-Host "Agents to update based on available instruction files:"
if ($updateCline) { Write-Host "- CLINE" }
if ($updateRooCode) { Write-Host "- ROO CODE" }
if ($updateCopilot) { Write-Host "- GitHub CoPilot" }
if ($updateCodex) { Write-Host "- OpenAI Codex" }
if ($updateClaude) { Write-Host "- Claude Code" }
}
elseif ($Mode -and $Mode -ne '') {
# Specific agent mode (e.g., CLINE, "ROO CODE", etc.)
$updateCline = ($Mode -eq 'CLINE')
$updateCopilot = ($Mode -eq 'GitHub CoPilot')
$updateRooCode = ($Mode -eq 'ROO CODE')
$updateCodex = ($Mode -eq 'OpenAI Codex')
$updateClaude = ($Mode -eq 'Claude Code')
Write-Host "Selected: $Mode (parameter mode)"
}
else {
# User prompt for agent selection
# Detect available agents for interactive menu
$hasCline = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.clinerules')
$hasRooCode = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.roo\rules')
$hasCopilot = Test-Path (Join-Path $repoRoot '.github\copilot-instructions.md') -PathType Leaf
$hasCodex = Test-Path (Join-Path $repoRoot 'AGENTS.md') -PathType Leaf
$hasClaude = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.claude')
Write-Host "`r`nDetected AI agents with instruction files:"
if ($hasCline) { Write-Host "- CLINE" }
if ($hasRooCode) { Write-Host "- ROO CODE" }
if ($hasCopilot) { Write-Host "- GitHub CoPilot" }
if ($hasCodex) { Write-Host "- OpenAI Codex" }
if ($hasClaude) { Write-Host "- Claude Code" }
Write-Host ""
Write-Host "=============================================================="
Write-Host "Select Agent Instruction Set to Update"
Write-Host "--------------------------------------------------------------"
Write-Host "1. AUTO - Update only agents with instruction files (default)"
Write-Host "2. ALL - Update instructions for all AI agents"
Write-Host "3. CLINE - Update instructions for CLINE"
Write-Host "4. ROO CODE - Update instructions for ROO CODE"
Write-Host "5. GitHub CoPilot - Update instructions for GitHub CoPilot"
Write-Host "6. OpenAI Codex - Update instructions for OpenAI Codex"
Write-Host "7. Claude Code - Update instructions for Claude Code"
Write-Host "0. Exit"
Write-Host "=============================================================="
$selection = Read-Host "Enter the number of your choice (0-7)"
# Initialize flags
$updateCline = $false
$updateCopilot = $false
$updateRooCode = $false
$updateCodex = $false
$updateClaude = $false
switch ($selection) {
'1' {
$updateCline = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.clinerules')
$updateRooCode = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.roo\rules')
$updateCopilot = Test-Path (Join-Path $repoRoot '.github\copilot-instructions.md') -PathType Leaf
$updateCodex = Test-Path (Join-Path $repoRoot 'AGENTS.md') -PathType Leaf
$updateClaude = Test-HasInstructionFiles -Path (Join-Path $repoRoot '.claude')
Write-Host "Selected: AUTO"
}
'2' {
$updateCline = $true
$updateCopilot = $true
$updateRooCode = $true
$updateCodex = $true
$updateClaude = $true
Write-Host "Selected: ALL"
}
'3' { $updateCline = $true; Write-Host "Selected: CLINE" }
'4' { $updateRooCode = $true; Write-Host "Selected: ROO CODE" }
'5' { $updateCopilot = $true; Write-Host "Selected: GitHub CoPilot" }
'6' { $updateCodex = $true; Write-Host "Selected: OpenAI Codex" }
'7' { $updateClaude = $true; Write-Host "Selected: Claude Code" }
'0' { Write-Host "Operation cancelled by user."; exit 0 }
default { throw "Invalid selection. Exiting." }
}
}
# --- Multiple-File Agent Updates ---
if ($updateCline) {
Update-MultipleFileAgent -AgentName "CLINE" -TargetDirectory ".clinerules" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
if ($updateRooCode) {
Update-MultipleFileAgent -AgentName "ROO CODE" -TargetDirectory ".roo\rules" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
# --- Single-File Agent Updates ---
if ($updateCopilot) {
$copilotTarget = ".github\copilot-instructions.md"
$githubInstructionsDir = Join-Path $repoRoot ".github\instructions"
if (Test-Path $githubInstructionsDir -PathType Container) {
Write-Host "`r`n--- Updating GitHub CoPilot Instructions (folder-based) ---"
$mainName = "instructions.md"
$mainSource = $sourceInstructionFiles | Where-Object { $_.Name -ieq $mainName } | Select-Object -First 1
if ($null -eq $mainSource) {
throw "Expected source '$mainName' under .ai but none found."
}
Copy-FileIfDifferent -SourcePath $mainSource.FullName -TargetPath (Join-Path $repoRoot $copilotTarget)
foreach ($sf in $sourceInstructionFiles) {
if ($sf.Name -ieq $mainName) {
continue
}
$destination = Join-Path $githubInstructionsDir $sf.Name
Copy-FileIfDifferent -SourcePath $sf.FullName -TargetPath $destination
}
}
else {
Update-SingleFileAgent -AgentName "GitHub CoPilot" -TargetFilePath $copilotTarget -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
}
if ($updateCodex) {
Update-SingleFileAgent -AgentName "OpenAI Codex" -TargetFilePath "AGENTS.md" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
# --- Claude Code (multiple-file agent) ---
if ($updateClaude) {
Update-MultipleFileAgent -AgentName "Claude Code" -TargetDirectory ".claude" -SourceFiles $sourceInstructionFiles -RepoRoot $repoRoot
}
# --- Skills mirroring ---
if ($updateRooCode -or $Mode -eq 'ALL' -or $Mode -eq 'AUTO') {
Sync-SkillsToRoo -RepoRoot $repoRoot
}
# GitHub Copilot: mirror skills to `.github/skills` (Copilot tries to load from there).
if ($updateCopilot -or $Mode -eq 'ALL' -or $Mode -eq 'AUTO') {
Sync-SkillsToGitHub -RepoRoot $repoRoot
}
# Claude Code: mirror skills to `.claude/skills`.
if ($updateClaude -or $Mode -eq 'ALL' -or $Mode -eq 'AUTO') {
Sync-SkillsToClaude -RepoRoot $repoRoot
}
Write-Host "`r`nAll selected operations completed successfully."
# Only pause when launched by double-click (Explorer). In CI / terminal usage, do not pause.
if ($Host.Name -and $Host.Name -notlike '*ConsoleHost*') {
Invoke-Pause
}
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
/FocusLogger/Resources/BuildDate.txt
/FocusLogger/Documents/Files
/.claude/settings.local.json
Documents/Files
================================================
FILE: .markdownlint.json
================================================
// https://github.com/DavidAnson/markdownlint
{
"default": true,
"MD013": false, // There are lines that are longer than the configured line_length (default: 80 characters).
"MD033": false, // Raw HTML is used in a markdown document.
"MD041": false, // The first line in a document is not a top-level heading.
}
================================================
FILE: Documents/App_1_Sign.ps1
================================================
Import-Module "d:\_Backup\Configuration\SSL\Tools\app_signModule.ps1" -Force
[string[]]$appFiles = @(
"..\FocusLogger\bin\Release\publish\JocysCom.FocusLogger.exe"
)
[string]$appName = "Jocys.com Focus Logger"
[string]$appLink = "https://www.jocys.com"
ProcessFiles $appName $appLink $appFiles
pause
================================================
FILE: Documents/App_2_Zip.ps1
================================================
# Make sure the output directories exist
$filesDir = Join-Path $PSScriptRoot "Files"
$binDir = Join-Path $PSScriptRoot "..\Resources"
$file1="JocysCom.FocusLogger.exe"
if (-not [System.IO.File]::Exists([System.IO.Path]::Combine($filesDir, $file1))){
[System.IO.File]::Copy([System.IO.Path]::Combine($PSScriptRoot, "..\App\bin\Release\publish\", $file1), [System.IO.Path]::Combine($filesDir, $file1))
}
& "$binDir\ZipFiles.ps1" $filesDir "$filesDir\JocysCom.FocusLogger.zip" $file1 $true
================================================
FILE: Documents/Take_Screenshot.ps1
================================================
# Take_Screenshot.ps1
# Launches FocusLogger, Notepad, and Explorer, switches between them
# to generate focus log entries, then captures a screenshot using PrintWindow.
#
# Usage: Right-click > Run with PowerShell, or run from terminal:
# powershell -ExecutionPolicy Bypass -File Take_Screenshot.ps1
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# Load C# helper class (supports re-running in the same session).
$csFilePath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "Take_Screenshot.ps1.cs"
$csFileContent = Get-Content -Path $csFilePath -Raw
$fileHash = Get-FileHash -InputStream ([System.IO.MemoryStream]::new([System.Text.Encoding]::UTF8.GetBytes($csFileContent))) -Algorithm SHA256
$className = "TakeScreenshot"
if (-not $script:loadedClasses) { $script:loadedClasses = @{} }
if (-not $script:loadedClasses.ContainsKey($fileHash.Hash)) {
$className += (Get-Date -Format "yyyyMMddHHmmss")
$csCode = $csFileContent -replace "TakeScreenshot", $className
Add-Type -TypeDefinition $csCode -ReferencedAssemblies System.Windows.Forms, System.Drawing
$script:loadedClasses[$fileHash.Hash] = $className
} else {
$className = $script:loadedClasses[$fileHash.Hash]
}
$helper = [Type]$className
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$screenshotPath = Join-Path $scriptDir "Images\JocysCom.FocusLogger.png"
$tempFile = Join-Path $env:TEMP "My Document.txt"
# Find FocusLogger executable.
$appPath = Join-Path $scriptDir "..\FocusLogger\bin\Debug\net8.0-windows\JocysCom.FocusLogger.exe"
if (-not (Test-Path $appPath)) {
$appPath = Join-Path $scriptDir "..\FocusLogger\bin\Release\net8.0-windows\JocysCom.FocusLogger.exe"
}
if (-not (Test-Path $appPath)) {
Write-Error "FocusLogger not found. Build the project first."
exit 1
}
$appPath = Resolve-Path $appPath
$focusLogger = $null
$notepad = $null
$explorerProc = $null
try {
# 1. Start FocusLogger (centered).
Write-Host "1. Starting FocusLogger..."
$focusLogger = Start-Process -FilePath $appPath -PassThru
Start-Sleep -Seconds 2
$focusLogger.Refresh()
$flHwnd = $focusLogger.MainWindowHandle
$helper::CenterAndResize($flHwnd, 920, 480)
Start-Sleep -Milliseconds 500
# 2. Start Notepad with a document.
Write-Host "2. Starting Notepad..."
"Sample document for Focus Logger screenshot." | Out-File -FilePath $tempFile -Encoding UTF8
$notepadPidsBefore = @(Get-Process -Name "Notepad" -ErrorAction SilentlyContinue | ForEach-Object { $_.Id })
Start-Process -FilePath "notepad.exe" -ArgumentList "`"$tempFile`""
Start-Sleep -Seconds 2
$notepad = Get-Process -Name "Notepad" -ErrorAction SilentlyContinue |
Where-Object { $notepadPidsBefore -notcontains $_.Id -and $_.MainWindowHandle -ne [IntPtr]::Zero } |
Select-Object -First 1
if (-not $notepad) {
$notepad = Get-Process -Name "Notepad" -ErrorAction SilentlyContinue |
Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } |
Select-Object -First 1
}
$npHwnd = if ($notepad) { $notepad.MainWindowHandle } else { [IntPtr]::Zero }
if ($npHwnd -ne [IntPtr]::Zero) {
$helper::CenterAndResize($npHwnd, 1040, 680, -50)
$helper::SetForegroundWindow($npHwnd) | Out-Null
Start-Sleep -Milliseconds 500
}
# 3. Start Explorer.
Write-Host "3. Starting Explorer..."
$explorerHwndsBefore = @(Get-Process explorer -ErrorAction SilentlyContinue |
Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } |
ForEach-Object { $_.MainWindowHandle })
Start-Process "explorer.exe" -ArgumentList "C:\Windows"
Start-Sleep -Seconds 2
$explorerProc = Get-Process explorer -ErrorAction SilentlyContinue |
Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero -and $explorerHwndsBefore -notcontains $_.MainWindowHandle } |
Select-Object -First 1
if (-not $explorerProc) {
$explorerProc = Get-Process explorer -ErrorAction SilentlyContinue |
Where-Object { $_.MainWindowTitle -match "Windows" -and $_.MainWindowHandle -ne [IntPtr]::Zero } |
Select-Object -First 1
}
$exHwnd = if ($explorerProc) { $explorerProc.MainWindowHandle } else { [IntPtr]::Zero }
if ($exHwnd -ne [IntPtr]::Zero) {
$helper::CenterAndResize($exHwnd, 700, 400)
$helper::SetForegroundWindow($exHwnd) | Out-Null
Start-Sleep -Milliseconds 500
}
# 4. Switch focus between windows to generate log entries.
Write-Host "4. Switching focus..."
if ($npHwnd -ne [IntPtr]::Zero) { $helper::SetForegroundWindow($npHwnd) | Out-Null; Start-Sleep -Milliseconds 800 }
if ($exHwnd -ne [IntPtr]::Zero) { $helper::SetForegroundWindow($exHwnd) | Out-Null; Start-Sleep -Milliseconds 800 }
if ($npHwnd -ne [IntPtr]::Zero) { $helper::SetForegroundWindow($npHwnd) | Out-Null; Start-Sleep -Milliseconds 800 }
if ($exHwnd -ne [IntPtr]::Zero) { $helper::SetForegroundWindow($exHwnd) | Out-Null; Start-Sleep -Milliseconds 800 }
if ($npHwnd -ne [IntPtr]::Zero) { $helper::SetForegroundWindow($npHwnd) | Out-Null; Start-Sleep -Milliseconds 800 }
# 5. Move Notepad behind FocusLogger as white background, then capture.
Write-Host "5. Taking screenshot..."
if ($npHwnd -ne [IntPtr]::Zero) {
$helper::CenterAndResize($npHwnd, 1040, 680, -50)
$helper::SetForegroundWindow($npHwnd) | Out-Null
Start-Sleep -Milliseconds 300
}
$helper::SetForegroundWindow($flHwnd) | Out-Null
Start-Sleep -Seconds 1
$helper::CaptureWindow($flHwnd, $screenshotPath)
Write-Host " Screenshot saved to: $screenshotPath"
} finally {
Write-Host "6. Cleaning up..."
if ($notepad -and -not $notepad.HasExited) { $notepad.Kill(); $notepad.WaitForExit(3000) | Out-Null }
if ($focusLogger -and -not $focusLogger.HasExited) { $focusLogger.Kill(); $focusLogger.WaitForExit(3000) | Out-Null }
if ($explorerProc -and -not $explorerProc.HasExited) { $explorerProc.CloseMainWindow() | Out-Null }
Start-Sleep -Milliseconds 500
Remove-Item $tempFile -ErrorAction SilentlyContinue
Write-Host "Done."
}
================================================
FILE: Documents/Take_Screenshot.ps1.cs
================================================
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class TakeScreenshot
{
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags);
[DllImport("dwmapi.dll")]
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left, Top, Right, Bottom;
}
public static RECT GetExtendedFrameBounds(IntPtr hwnd)
{
RECT rect;
DwmGetWindowAttribute(hwnd, 9, out rect, Marshal.SizeOf(typeof(RECT)));
return rect;
}
public static void CenterAndResize(IntPtr hwnd, int w, int h, int yOffset = 0)
{
var screen = Screen.PrimaryScreen.WorkingArea;
int x = (screen.Width - w) / 2 + screen.Left;
int y = (screen.Height - h) / 2 + screen.Top + yOffset;
SetWindowPos(hwnd, IntPtr.Zero, x, y, w, h, 0x0040);
}
///
/// Captures a window using CopyFromScreen with DWM extended frame bounds
/// for accurate visible area (excludes invisible shadow border).
///
public static void CaptureWindow(IntPtr hwnd, string filePath)
{
var rect = GetExtendedFrameBounds(hwnd);
int w = rect.Right - rect.Left;
int h = rect.Bottom - rect.Top;
using (var bitmap = new Bitmap(w, h, PixelFormat.Format32bppArgb))
{
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.CopyFromScreen(rect.Left, rect.Top, 0, 0, new Size(w, h));
}
bitmap.Save(filePath, ImageFormat.Png);
}
}
}
================================================
FILE: FocusLogger/App.xaml
================================================
================================================
FILE: FocusLogger/App.xaml.cs
================================================
using System;
using System.Windows;
namespace JocysCom.FocusLogger
{
public partial class App : Application
{
public App()
{
SetDPIAware();
}
internal class NativeMethods
{
[System.Runtime.InteropServices.DllImport("user32.dll")]
internal static extern bool SetProcessDPIAware();
}
public static void SetDPIAware()
{
// DPI aware property must be set before application window is created.
if (Environment.OSVersion.Version.Major >= 6)
NativeMethods.SetProcessDPIAware();
}
}
}
================================================
FILE: FocusLogger/AssemblyInfo.cs
================================================
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
================================================
FILE: FocusLogger/Common/DataItem.cs
================================================
using JocysCom.ClassLibrary.Configuration;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace JocysCom.FocusLogger
{
public class DataItem : SettingsItem
{
public DateTime Date { get => _Date; set => SetProperty(ref _Date, value); }
DateTime _Date;
public int ProcessId { get => _ProcessId; set => SetProperty(ref _ProcessId, value); }
int _ProcessId;
public string ProcessName { get => _ProcessName; set => SetProperty(ref _ProcessName, value); }
string _ProcessName;
public string ProcessPath { get => _ProcessPath; set => SetProperty(ref _ProcessPath, value); }
string _ProcessPath;
public string WindowTitle { get => _WindowTitle; set => SetProperty(ref _WindowTitle, value); }
string _WindowTitle;
public string WindowClassName { get => _WindowClassName; set => SetProperty(ref _WindowClassName, value); }
string _WindowClassName;
public bool HasMouse { get => _HasMouse; set => SetProperty(ref _HasMouse, value); }
bool _HasMouse;
public bool HasKeyboard { get => _HasKeyboard; set => SetProperty(ref _HasKeyboard, value); }
bool _HasKeyboard;
public bool HasCaret { get => _HasCaret; set => SetProperty(ref _HasCaret, value); }
bool _HasCaret;
public bool IsActive { get => _IsActive; set => SetProperty(ref _IsActive, value); }
bool _IsActive;
public bool NonPath { get => _IsError; set => SetProperty(ref _IsError, value); }
bool _IsError;
public bool IsSame(DataItem item)
{
return
item.ProcessId == ProcessId &&
item.HasMouse == HasMouse &&
item.HasKeyboard == HasKeyboard &&
item.HasCaret == HasCaret &&
item.IsActive == IsActive;
}
public System.Windows.MessageBoxImage StatusCode { get => _StatusCode; set => SetProperty(ref _StatusCode, value); }
System.Windows.MessageBoxImage _StatusCode;
}
}
================================================
FILE: FocusLogger/Common/DataItemType.cs
================================================
namespace JocysCom.FocusLogger
{
public enum DataItemType
{
None = 0,
}
}
================================================
FILE: FocusLogger/Common/NativeMethods.cs
================================================
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace JocysCom.FocusLogger
{
internal class NativeMethods
{
// https://docs.microsoft.com/en-gb/windows/win32/api/winuser/
///
/// Get handle to the window with the keyboard focus.
///
///
/// The return value is the handle to the window with the keyboard focus.
/// If the calling thread's message queue does not have an associated window with the keyboard focus, the return value is NULL.
///
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
internal static extern IntPtr GetFocus();
///
/// Window which is active.
///
///
/// The return value is the handle to the active window attached to the calling thread's message queue.
/// Otherwise, the return value is NULL.
///
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
internal static extern IntPtr GetActiveWindow();
///
/// Get handle to the child window at the top of the Z order.
///
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
internal static extern IntPtr GetTopWindow(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
internal static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
internal static extern int GetWindowTextLengthW(IntPtr hWnd);
///
/// Copies the text of the specified window's title bar (if it has one) into a buffer.
///
/// A handle to the window or control containing the text.
/// The buffer that will receive the text.
/// The maximum number of characters to copy to the buffer, including the null character.
/// If the function succeeds, the return value is the length, in characters, of the copied string, not including the terminating null character.
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
///
/// A handle to the window that will receive the keyboard input.
///
///
/// The return value is a handle to the foreground window.
/// The foreground window can be NULL in certain circumstances, such as when a window is losing activation.
///
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
internal static extern IntPtr GetForegroundWindow();
///
/// Retrieves the identifier of the thread that created the specified window and,
/// optionally, the identifier of the process that created the window.
///
/// A handle to the window.
/// A pointer to a variable that receives the process identifier
/// Identifier of the thread that created the window.
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
///
/// Retrieves information about the active window or a specified GUI thread.
///
///
/// The identifier for the thread for which information is to be retrieved.
/// To retrieve this value, use the GetWindowThreadProcessId function.
/// If this parameter is NULL, the function returns information for the foreground thread.
///
///
/// A pointer to a GUITHREADINFO structure that receives information describing the thread.
///
/// If the function succeeds, the return value is nonzero.
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool GetGUIThreadInfo(int idThread, ref GUITHREADINFO pgui);
[StructLayout(LayoutKind.Sequential)]
internal struct RECT
{
public int iLeft;
public int iTop;
public int iRight;
public int iBottom;
}
[Flags]
internal enum GUI
{
/// The caret's blink state. This bit is set if the caret is visible.
GUI_CARETBLINKING = 0x00000001,
/// The thread's menu state. This bit is set if the thread is in menu mode.
GUI_INMENUMODE = 0x00000004,
/// The thread's move state. This bit is set if the thread is in a move or size loop.
GUI_INMOVESIZE = 0x00000002,
/// The thread's pop-up menu state. This bit is set if the thread has an active pop-up menu.
GUI_POPUPMENUMODE = 0x00000010,
/// The thread's system menu state. This bit is set if the thread is in a system menu mode.
GUI_SYSTEMMENUMODE = 0x00000008,
}
[StructLayout(LayoutKind.Sequential)]
internal struct GUITHREADINFO
{
/// The size of this structure, in bytes.
public int cbSize;
/// The thread state.
public GUI flags;
/// A handle to the active window within the thread.
public IntPtr hwndActive;
/// A handle to the window that has the keyboard focus.
public IntPtr hwndFocus;
/// A handle to the window that has captured the mouse.
public IntPtr hwndCapture;
/// A handle to the window that owns any active menus.
public IntPtr hwndMenuOwner;
/// A handle to the window in a move or size loop.
public IntPtr hwndMoveSize;
/// A handle to the window that is displaying the caret.
public IntPtr hwndCaret;
/// The caret's bounding rectangle, in client coordinates, relative to the window specified by the hwndCaret member.
public RECT rectCaret;
}
internal static GUITHREADINFO? GetInfo(IntPtr hWnd)
{
int lpdwProcessId;
int threadId = GetWindowThreadProcessId(hWnd, out lpdwProcessId);
var pgui = new GUITHREADINFO();
pgui.cbSize = Marshal.SizeOf(pgui);
if (GetGUIThreadInfo(threadId, ref pgui))
return pgui;
return null;
}
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
internal static string GetWindowClassName(IntPtr hWnd)
{
var sb = new StringBuilder(256);
var length = GetClassName(hWnd, sb, sb.Capacity);
return length > 0 ? sb.ToString() : "";
}
internal static string GetWindowText(IntPtr hWnd)
{
int textLength = GetWindowTextLengthW(hWnd);
var lpString = new StringBuilder(textLength + 1);
var length = GetWindowText(hWnd, lpString, lpString.Capacity);
return lpString.ToString();
}
}
}
================================================
FILE: FocusLogger/Controls/DataListControl.xaml
================================================
================================================
FILE: FocusLogger/Controls/DataListControl.xaml.cs
================================================
using JocysCom.ClassLibrary.ComponentModel;
using JocysCom.ClassLibrary.Controls;
using JocysCom.FocusLogger.Resources.Icons;
using Microsoft.Win32;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
namespace JocysCom.FocusLogger.Controls
{
///
/// Interaction logic for DataListControl.xaml
///
public partial class DataListControl : UserControl
{
public DataListControl()
{
InitializeComponent();
if (ControlsHelper.IsDesignMode(this))
return;
// Configure converter.
MainDataGrid.ItemsSource = DataItems;
var gridFormattingConverter = MainDataGrid.Resources.Values.OfType().First();
gridFormattingConverter.ConvertFunction = _MainDataGridFormattingConverter_Convert;
}
object _MainDataGridFormattingConverter_Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var sender = (FrameworkElement)values[0];
var template = (FrameworkElement)values[1];
var value = values[2];
var cell = (DataGridCell)(template ?? sender).Parent;
var item = (DataItem)cell.DataContext;
if (cell.Column == IsActiveImageColumn)
{
return item.IsActive ? Icons_Default.Current[Icons_Default.Icon_window] : null;
}
if (cell.Column == IsActiveColumn)
{
cell.Opacity = 0.5;
return item.IsActive ? "Active" : "";
}
// Mouse.
if (cell.Column == HasMouseColumn)
return item.HasMouse ? "Mouse" : "";
if (cell.Column == HasMouseImageColumn)
return item.HasMouse ? Icons_Default.Current[Icons_Default.Icon_mouse2] : null;
// Keyboard.
if (cell.Column == HasKeyboardImageColumn)
return item.HasKeyboard ? Icons_Default.Current[Icons_Default.Icon_keyboard] : null;
if (cell.Column == HasKeyboardColumn)
return item.HasKeyboard ? "Keyboard" : "";
// Caret.
if (cell.Column == HasCaretImageColumn)
return item.HasCaret ? Icons_Default.Current[Icons_Default.Icon_text_field] : null;
if (cell.Column == HasCaretColumn)
return item.HasCaret ? "Caret" : "";
if (cell.Column == DateColumn)
{
value = string.Format("{0:HH:mm:ss:fff}", item.Date);
cell.Opacity = 0.5;
}
if (cell.Column == ProcessPathColumn)
{
if (item.NonPath)
cell.Opacity = 0.3;
}
// Other.
return value;
}
public SortableBindingList DataItems { get; set; } = new SortableBindingList();
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
if (ControlsHelper.IsDesignMode(this))
return;
InitTimer();
}
private string _lastSavedCsvPath;
private void SaveCsvButton_Click(object sender, RoutedEventArgs e)
{
if (DataItems.Count == 0)
{
MessageBox.Show("No log entries to save.", "Save CSV", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var dialog = new SaveFileDialog
{
Filter = "CSV files (*.csv)|*.csv",
DefaultExt = ".csv",
FileName = $"FocusLog_{DateTime.Now:yyyy-MM-dd_HHmmss}.csv",
};
if (dialog.ShowDialog() != true)
return;
var csv = BuildCsvContent(DataItems);
File.WriteAllText(dialog.FileName, csv, Encoding.UTF8);
_lastSavedCsvPath = dialog.FileName;
}
private void ExploreCsvButton_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(_lastSavedCsvPath) || !File.Exists(_lastSavedCsvPath))
{
MessageBox.Show("No CSV file saved yet. Use 'Save CSV' first.", "Explore", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
Process.Start("explorer.exe", $"/select,\"{_lastSavedCsvPath}\"");
}
public static string BuildCsvContent(System.Collections.Generic.IEnumerable items)
{
var sb = new StringBuilder();
sb.AppendLine("Date,PID,Process Name,Active,Mouse,Keyboard,Caret,Window Title,Window Class,Path");
foreach (var item in items)
{
sb.AppendLine(string.Join(",",
CsvEscape(item.Date.ToString("yyyy-MM-dd HH:mm:ss.fff")),
item.ProcessId,
CsvEscape(item.ProcessName),
item.IsActive,
item.HasMouse,
item.HasKeyboard,
item.HasCaret,
CsvEscape(item.WindowTitle),
CsvEscape(item.WindowClassName),
CsvEscape(item.ProcessPath)
));
}
return sb.ToString();
}
public static string CsvEscape(string value)
{
if (string.IsNullOrEmpty(value))
return "";
if (value.Contains(",") || value.Contains("\"") || value.Contains("\n"))
return "\"" + value.Replace("\"", "\"\"") + "\"";
return value;
}
private void CopyAiPromptButton_Click(object sender, RoutedEventArgs e)
{
var prompt = ClassLibrary.Helper.FindResource("Resources/AiAnalysisPrompt.md").TrimEnd();
var box = new MessageBoxWindow();
box.SetSize(720, 400);
box.ShowPrompt(prompt, "AI Analysis Prompt - Copy and paste into your AI assistant", MessageBoxButton.OK, MessageBoxImage.Information, MessageBoxResult.OK);
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
DataItems.Clear();
}
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
{
if (ControlsHelper.IsDesignMode(this))
return;
}
object AddLock = new object();
public bool IsElevated
{
get
{
var id = System.Security.Principal.WindowsIdentity.GetCurrent();
return id.Owner != id.User;
}
}
public void UpdateFromProcess(DataItem item)
{
using (var process = Process.GetProcessById(item.ProcessId))
{
item.ProcessName = process.ProcessName;
if (item.ProcessId == 0)
{
item.ProcessPath = "System Idle Process";
item.NonPath = true;
}
if (item.ProcessId > 0)
{
try
{
item.ProcessPath = process.MainModule?.FileName;
}
catch (Exception ex)
{
const int E_FAIL = unchecked((int)0x80004005); // -2147467259
item.ProcessPath = $"Error: {ex.Message}";
item.NonPath = true;
// If Win32 Acccess is denied exception, then...
if (ex is Win32Exception && ex.HResult == E_FAIL)
item.ProcessPath += " Run as Administrator";
}
}
}
}
System.Timers.Timer _Timer;
void InitTimer()
{
_Timer = new System.Timers.Timer();
_Timer.Elapsed += _Timer_Elapsed;
_Timer.AutoReset = false;
_Timer.Interval = 1;
_Timer.Start();
}
private void _Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
if (MainWindow.IsClosing)
return;
UpdateInfo();
_Timer.Start();
}
DataItem oldActiveItem = new DataItem();
DataItem oldForegroundItem = new DataItem();
public void UpdateInfo()
{
// Active window - Window that appears in the foreground with a highlighted title bar.
// Foreground window - Window with which the user is currently working.
// The system assigns a slightly higher priority to the thread used to create the foreground window.
// Focus window - Window that is currently receiving keyboard input.
// The focus window is always the active window, a descendent of the active window, or NULL.
// Top-Level window - A window that has no parent window.
//
lock (AddLock)
{
// Get window which or child window of which receives keyboard input.
var activeHandle = NativeMethods.GetActiveWindow();
var activeItem = GetItemFromHandle(activeHandle, true);
// If active window changed then...
if (!activeItem.IsSame(oldActiveItem))
{
oldActiveItem = activeItem;
UpdateFromProcess(activeItem);
ControlsHelper.BeginInvoke(() => DataItems.Insert(0, activeItem));
}
// Get foreground window.
var foregroundHandle = NativeMethods.GetForegroundWindow();
var foregroundItem = GetItemFromHandle(foregroundHandle);
// If foreground window changed then...
if (!foregroundItem.IsSame(oldForegroundItem))
{
oldForegroundItem = foregroundItem;
UpdateFromProcess(foregroundItem);
ControlsHelper.BeginInvoke(() => DataItems.Insert(0, foregroundItem));
}
}
}
DataItem GetItemFromHandle(IntPtr hWnd, bool isActive = false)
{
var item = new DataItem();
item.Date = DateTime.Now;
item.IsActive = isActive;
var info = NativeMethods.GetInfo(hWnd);
if (info.HasValue)
{
item.HasMouse = info.Value.hwndCapture != IntPtr.Zero;
item.HasKeyboard = info.Value.hwndFocus != IntPtr.Zero;
item.HasCaret = info.Value.hwndCaret != IntPtr.Zero;
}
int processId;
if (isActive)
{
hWnd = NativeMethods.GetTopWindow(hWnd);
}
item.WindowTitle = NativeMethods.GetWindowText(hWnd);
item.WindowClassName = NativeMethods.GetWindowClassName(hWnd);
NativeMethods.GetWindowThreadProcessId(hWnd, out processId);
item.ProcessId = processId;
return item;
}
}
}
================================================
FILE: FocusLogger/JocysCom/Collections/CollectionsHelper.cs
================================================
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace JocysCom.ClassLibrary.Collections
{
public static partial class CollectionsHelper
{
///
/// Synchronizes target list to match source list:
/// removes items not in source, inserts missing items,
/// and reorders existing elements to mirror source order.
///
/// Type of elements in the lists.
/// The source list whose items and order to mirror; must not be null.
/// The target list to update; must support Insert and RemoveAt; must not be null.
/// Optional equality comparer; defaults to .
///
/// Uses a for fast source lookups; overall time complexity is O(n^2) due to list insert/remove operations.
/// Try to use quick sort algorithm by using uniqueSortName/index.
/// When target is , uses to avoid triggering remove and insert events.
///
public static void Synchronize(IList source, IList target, IEqualityComparer comparer = null)
{
comparer = comparer ?? EqualityComparer.Default;
// Create a dictionary for fast lookup in source list
var sourceSet = new Dictionary();
for (int i = 0; i < source.Count; i++)
sourceSet[source[i]] = i;
// Iterate over the target, remove items not in source
for (int i = target.Count - 1; i >= 0; i--)
if (!sourceSet.Keys.Contains(target[i], comparer))
target.RemoveAt(i);
// Iterate over source
for (int si = 0; si < source.Count; si++)
{
// If item is not present in target, insert it.
// Note: Only check items that have not been synchornized in the target.
if (!target.Skip(si).Contains(source[si], comparer))
{
target.Insert(si, source[si]);
continue;
}
// If item is present in target but not at the right position, move it.
int ti = -1;
// Note: Only check items that have not been synchornized in the target.
for (int i = si; i < target.Count; i++)
{
if (comparer.Equals(target[i], source[si]))
{
ti = i;
break;
}
}
if (ti != si)
{
var oc = target as ObservableCollection;
// If observable collection then
if (oc != null)
{
// Move item without triggering remove and insert events.
oc.Move(si, ti);
}
else
{
// Removes and inserts item and
// can disrupt data binding in WPF controls.
var item = target[ti];
target.RemoveAt(ti);
target.Insert(si, item);
}
}
}
// Remove items at the end of target that exceed source's length
while (target.Count > source.Count)
target.RemoveAt(target.Count - 1);
}
}
}
================================================
FILE: FocusLogger/JocysCom/Common/Helper.cs
================================================
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace JocysCom.ClassLibrary
{
public partial class Helper : IDisposable
{
#region Control Resources
///
/// Write application header title to CLI interface.
///
public static void WriteAppHeader()
{
var assembly = Assembly.GetExecutingAssembly();
WriteAppHeader(assembly);
}
public static void WriteAppHeader(Assembly assembly)
{
// Write title.
// Microsoft (R) SQL Server Database Publishing Wizard 1.1.1.0
// Copyright (C) Microsoft Corporation. All rights reserved.
var a = new Configuration.AssemblyInfo(assembly);
Console.WriteLine(a.Title + " " + a.Version.ToString());
Console.WriteLine(a.Copyright);
Console.WriteLine(a.Description);
}
///
/// Write help text from help.txt file.
///
public static void WriteAppHelp()
{
Console.Write(GetResource("Documents/Help.txt"));
}
///
/// Get resource from class *.resx file by full name.
///
public static T FindResource(string name, object o)
{
if (o is null)
throw new ArgumentNullException(nameof(o));
var resources = new System.ComponentModel.ComponentResourceManager(o.GetType());
return (T)(resources.GetObject(name));
}
///
/// Find resource in all loaded assemblies if not specified by full or partial (EndsWith) name.
/// Look inside "Build Action: Embedded Resource".
///
public static T FindResource(string name, params Assembly[] assemblies)
{
var name1 = name.Replace("/", ".").Replace(@"\", ".");
var name2 = name1.Replace(' ', '_');
if (assemblies.Length == 0)
assemblies = GetAssemblies();
foreach (var assembly in assemblies)
{
var resourceNames = assembly.GetManifestResourceNames();
foreach (var resourceName in resourceNames)
{
if (!resourceName.EndsWith(name1) && !resourceName.EndsWith(name2))
continue;
var stream = assembly.GetManifestResourceStream(resourceName);
return ConvertResource(stream);
}
}
return default(T);
}
///
/// Project Build Action: "Resource".
///
public static string[] GetResourceKeys(Assembly assembly)
{
string resName = assembly.GetName().Name + ".g.resources";
using (var stream = assembly.GetManifestResourceStream(resName))
using (var reader = new System.Resources.ResourceReader(stream))
return reader.Cast().Select(x => (string)x.Key).ToArray();
}
///
/// Project Build Action: "Resource".
///
public static Stream GetResourceValue(string name, Assembly assembly)
{
string resName = assembly.GetName().Name + ".g.resources";
using (var stream = assembly.GetManifestResourceStream(resName))
using (var reader = new System.Resources.ResourceReader(stream))
{
var value = reader.Cast()
.Where(x => (string)x.Key == name)
.Select(x => x.Value).FirstOrDefault();
return (Stream)value;
//var path = string.Format("{0};component/{1}", assembly.GetName().Name, name);
//var s = System.Windows.Application.GetResourceStream(new Uri(path, UriKind.Relative));
//return s.Stream;
}
}
///
/// Get embedded resource from type (*.resx file).
///
public static T GetResource(string name)
{
var resources = new System.ComponentModel.ComponentResourceManager(typeof(T));
return (T)resources.GetObject(name);
}
///
/// Get embedded resource by its full name.
///
public static T GetResource(string name, params Assembly[] assemblies)
{
var name1 = name.Replace("/", ".").Replace(@"\", ".");
var name2 = name1.Replace(' ', '_');
if (assemblies.Length == 0)
assemblies = GetAssemblies();
foreach (var assembly in assemblies)
{
var resourceNames = assembly.GetManifestResourceNames();
foreach (var resourceName in resourceNames)
{
if (resourceName != name1 && resourceName != name2)
continue;
var stream = assembly.GetManifestResourceStream(resourceName);
return ConvertResource(stream);
}
}
throw new Exception("Resource not found");
}
/// Converts a resource Stream to the specified type T: returns Stream, string (BOM-aware), System.Drawing.Image (.NET Framework), or byte[].
static T ConvertResource(Stream stream)
{
if (typeof(T) == typeof(Stream))
return (T)(object)stream;
var results = default(T);
if (typeof(T) == typeof(string))
{
// File must contain Byte Order Mark (BOM) header in order for bytes correctly encoded to string.
// If header is missing then get resource as byte[] type and encode manually.
var streamReader = new StreamReader(stream, true);
return (T)(object)streamReader.ReadToEnd();
}
#if NETFRAMEWORK // .NET Framework
else if (typeof(T) == typeof(System.Drawing.Image) || typeof(T) == typeof(System.Drawing.Bitmap))
{
return (T)(object)System.Drawing.Image.FromStream(stream);
}
#endif
else
{
var bytes = new byte[stream.Length];
#if NET7_0_OR_GREATER
stream.ReadExactly(bytes, 0, (int)stream.Length);
#else
stream.Read(bytes, 0, (int)stream.Length);
#endif
results = (T)(object)bytes;
}
return results;
}
/// Retrieves all loaded assemblies, prioritizing the executing, calling, and entry assemblies for resource lookup.
static Assembly[] GetAssemblies()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
var orderDesc = new Assembly[]
{
Assembly.GetExecutingAssembly(),
Assembly.GetCallingAssembly(),
Assembly.GetEntryAssembly(),
};
// Move assemblies to top.
foreach (var item in orderDesc)
{
if (assemblies.Contains(item))
{
assemblies.Remove(item);
assemblies.Insert(0, item);
}
}
return assemblies.ToArray();
}
#endregion
/* LongDelay example with CancellationToken:
// Create a token that auto-cancels after 10 seconds.
var source = new CancellationTokenSource(10000);
// Delay for 20 seconds.
try { LongDelay(20000, source.Token).Wait(); }
catch (TaskCanceledException) { } // Cancel silently.
catch (Exception) { throw; }
*/
/// Allow to delay Task for 292,471,209 years.
/// Usage makes sense if the process won't be recycled before the delay expires.
public static async Task LongDelay(
TimeSpan delay,
CancellationToken cancellationToken = default(CancellationToken)
) => await LongDelay((long)delay.TotalMilliseconds, cancellationToken).ConfigureAwait(false);
/// Allow to delay Task for 292,471,209 years.
/// Usage makes sense if the process won't be recycled before the delay expires.
public static async Task LongDelay(
long millisecondsDelay,
CancellationToken cancellationToken = default(CancellationToken)
)
{
// Use 'do' to run Task.Delay at least once to reproduce the same behavior.
do
{
var delay = (int)Math.Min(int.MaxValue, millisecondsDelay);
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
millisecondsDelay -= delay;
} while (millisecondsDelay > 0);
}
#region Debounce Execution
///
/// Contains the CancellationTokenSource for each delegate to manage debouncing.
///
static ConcurrentDictionary DebounceActions = new ConcurrentDictionary();
///
/// Holds debouncing information for a specific delegate.
///
class DebounceData
{
public int Counter = 0;
public object LockObject = new object();
}
[Obsolete("Use `async Task Debounce(Action action, int? delay = null, params object[] args)` instead.")]
public static async Task Delay(Action action, int? delay = null, params object[] args)
=> await _Debounce(action, delay, args);
[Obsolete("Use `async Task Debounce(Func action, int? delay = null, params object[] args)` instead.")]
public static async Task Delay(Func action, int? delay = null, params object[] args)
=> await _Debounce(action, delay, args);
///
/// Executes an action after a delay, canceling any previous pending executions of the same action.
/// This method ensures that the action is invoked only after the specified delay has elapsed since the last invocation request.
///
/// The action to debounce.
/// The delay in milliseconds to wait before invoking the action. Defaults to 500 milliseconds if not specified.
/// A Task representing the asynchronous debounced operation.
public static async Task Debounce(Action action, int? delay = null)
=> await _Debounce(action, delay);
///
/// Executes an action after a delay, canceling any previous pending executions of the same action.
/// This method ensures that the action is invoked only after the specified delay has elapsed since the last invocation request.
///
/// The action to debounce.
/// The delay in milliseconds to wait before invoking the action. Defaults to 500 milliseconds if not specified.
/// A Task representing the asynchronous debounced operation.
public static async Task Debounce(Action action, T arg, int? delay = null)
=> await _Debounce(action, delay, new object[] { arg });
///
/// Executes an asynchronous function after a delay, canceling any previous pending executions of the same function.
/// This method ensures that the action is invoked only after the specified delay has elapsed since the last invocation request.
///
/// The asynchronous function to debounce.
/// The delay in milliseconds to wait before invoking the function. Defaults to 500 milliseconds if not specified.
/// A Task representing the asynchronous debounced operation.
public static async Task Debounce(Func action, int? delay = null)
=> await _Debounce(action, delay);
///
/// Debounces the specified action, ensuring it's only invoked after a specified delay since the last call.
/// Subsequent calls within the delay period reset the timer.
/// This method automatically detects WPF UI thread requirements and marshals execution appropriately.
///
/// The delegate to debounce.
/// The delay in milliseconds before the delegate is invoked. Defaults to 500 milliseconds if not specified.
/// Optional arguments to pass to the delegate when invoked.
/// A Task representing the asynchronous debounced operation.
public static async Task _Debounce(Delegate action, int? delay = null, params object[] args)
{
if (action == null)
return;
int delayValue = delay ?? 500;
var debounceData = DebounceActions.GetOrAdd(action, new DebounceData());
int currentCount;
lock (debounceData.LockObject)
{
debounceData.Counter++;
currentCount = debounceData.Counter;
}
await Task.Delay(delayValue);
bool shouldExecute = false;
lock (debounceData.LockObject)
{
// This is the latest scheduled call; mark for execution
shouldExecute = currentCount == debounceData.Counter;
}
if (shouldExecute)
{
// Smart UI thread marshaling - detect and marshal to UI thread if needed
await ExecuteWithUIThreadMarshaling(action, args);
}
}
///
/// Executes the given delegate with automatic UI thread marshaling if WPF is available and needed.
///
private static async Task ExecuteWithUIThreadMarshaling(Delegate action, params object[] args)
{
// Check if WPF is available and we need UI thread marshaling
var dispatcher = GetWpfDispatcher();
if (dispatcher != null && !dispatcher.CheckAccess())
{
// We're not on the UI thread, marshal the call
await dispatcher.BeginInvoke(new Action(() =>
{
try
{
action.DynamicInvoke(args);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error in debounced UI action: {ex}");
}
}));
return;
}
// Execute normally (either no WPF, already on UI thread, or non-WPF environment)
action.DynamicInvoke(args);
}
///
/// Attempts to get the WPF Dispatcher for the current application.
/// Returns null if WPF is not available or no dispatcher is found.
///
private static Dispatcher GetWpfDispatcher()
{
try
{
// Try to get dispatcher from current thread first
var currentDispatcher = Dispatcher.FromThread(Thread.CurrentThread);
if (currentDispatcher != null)
return currentDispatcher;
// Try to get dispatcher from application
var app = System.Windows.Application.Current;
if (app != null)
return app.Dispatcher;
// Try to get any available dispatcher
return Dispatcher.CurrentDispatcher;
}
catch
{
// WPF might not be available or initialized
return null;
}
}
#endregion
#if NETFRAMEWORK // .NET Framework
#region Disk Activity
// Sometimes it is good to pause if there is too much disk activity.
// By letting windows/SQL to commit all changes to disk we can improve speed.
private PerformanceCounter _diskReadCounter = new PerformanceCounter();
private PerformanceCounter _diskWriteCounter = new PerformanceCounter();
/// Reads the specified PerformanceCounter (category, counter, instance) and returns its next value.
private static double GetCounterValue(PerformanceCounter pc, string categoryName, string counterName, string instanceName)
{
pc.CategoryName = categoryName;
pc.CounterName = counterName;
pc.InstanceName = instanceName;
return pc.NextValue();
}
/// Specifies disk I/O metric types: ReadAndWrite, Read-only, or Write-only operations.
public enum DiskData { ReadAndWrite, Read, Write };
/// Gets disk I/O bytes per second using the specified DiskData metric via PhysicalDisk _Total counters.
public double GetDiskData(DiskData dd)
{
return dd == DiskData.Read ?
GetCounterValue(_diskReadCounter, "PhysicalDisk", "Disk Read Bytes/sec", "_Total") :
dd == DiskData.Write ?
GetCounterValue(_diskWriteCounter, "PhysicalDisk", "Disk Write Bytes/sec", "_Total") :
dd == DiskData.ReadAndWrite ?
GetCounterValue(_diskReadCounter, "PhysicalDisk", "Disk Read Bytes/sec", "_Total") +
GetCounterValue(_diskWriteCounter, "PhysicalDisk", "Disk Write Bytes/sec", "_Total") :
0;
}
#endregion
#endif
#region Comparisons
private static Regex _GuidRegex;
/// Regex matching GUID strings in various formats: 32 digits, hyphenated, with braces or parentheses, or hex-coded lists.
public static Regex GuidRegex
{
get
{
if (_GuidRegex is null)
{
_GuidRegex = new Regex(
"^[A-Fa-f0-9]{32}$|" +
"^({|\\()?[A-Fa-f0-9]{8}-([A-Fa-f0-9]{4}-){3}[A-Fa-f0-9]{12}(}|\\))?$|" +
"^({)?[0xA-Fa-f0-9]{3,10}(, {0,1}[0xA-Fa-f0-9]{3,6}){2}, {0,1}({)([0xA-Fa-f0-9]{3,4}, {0,1}){7}[0xA-Fa-f0-9]{3,4}(}})$");
}
return _GuidRegex;
}
}
/// Determines whether the specified string is a valid GUID format; returns false if null or empty.
public static bool IsGuid(string s)
{
return string.IsNullOrEmpty(s)
? false
: GuidRegex.IsMatch(s);
}
///
/// Returns true if two ranges overlap.
///
public static bool IsOverlap(
T? min1, T? max1,
T? min2, T? max2 = default, bool inclusive = false
) where T : struct, IComparable
{
// Check arguments.
if (min1 != null && max1 != null && min1.Value.CompareTo(max1.Value) > 0)
throw new ArgumentException($"{nameof(min1)} can not be after {nameof(max1)}.");
if (min2 != null && max2 != null && min2.Value.CompareTo(max2.Value) > 0)
throw new ArgumentException($"{nameof(min2)} can not be after {nameof(max2)}.");
// The first range begins before the second ends AND
// The second range begins before the first ends.
// -----|...|---------
// ---------|...|-----
// Null is treated as a full range.
return
(min1 is null || max2 is null || min1.Value.CompareTo(max2.Value) <= (inclusive ? 0 : -1)) &&
(min2 is null || max1 is null || min2.Value.CompareTo(max1.Value) <= (inclusive ? 0 : -1));
}
#endregion
#region Run functions synchronously.
///
/// Runs the specified asynchronous function synchronously.
///
/// The asynchronous function to run.
///
/// This method avoids deadlocks by temporarily removing the current SynchronizationContext,
/// allowing the asynchronous function to execute without waiting for the context to be available.
/// The main disadvantage when compared to the Task.RunSynchronously() method is that
/// it bypasses the Task scheduler, which could lead to potential performance issues.
///
public static void RunSynchronously(Func asyncFunc)
{
// Save the current synchronization context
var context = SynchronizationContext.Current;
// Temporarily remove the current synchronization context
SynchronizationContext.SetSynchronizationContext(null);
try
{
// Execute the asynchronous function and wait for it to complete
asyncFunc().GetAwaiter().GetResult();
}
finally
{
// Restore the original synchronization context
SynchronizationContext.SetSynchronizationContext(context);
}
}
///
/// Runs the specified asynchronous function synchronously and returns the result.
///
/// The type of the result.
/// The asynchronous function to run.
/// The result of the asynchronous function.
///
/// This method avoids deadlocks by temporarily removing the current SynchronizationContext,
/// allowing the asynchronous function to execute without waiting for the context to be available.
/// The main disadvantage when compared to the Task.RunSynchronously() method is that
/// it bypasses the Task scheduler, which could lead to potential performance issues.
///
public static TResult RunSynchronously(Func> asyncFunc)
{
// Save the current synchronization context
var context = SynchronizationContext.Current;
// Temporarily remove the current synchronization context
SynchronizationContext.SetSynchronizationContext(null);
try
{
// Execute the asynchronous function and wait for it to complete, then return the result
return asyncFunc().GetAwaiter().GetResult();
}
finally
{
// Restore the original synchronization context
SynchronizationContext.SetSynchronizationContext(context);
}
}
#endregion
#region IDisposable
// Dispose() calls Dispose(true)
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// The bulk of the clean-up code is implemented in Dispose(bool)
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
#if NETFRAMEWORK // .NET Framework
// Free managed resources.
if (_diskReadCounter != null)
{
_diskReadCounter.Dispose();
_diskReadCounter = null;
}
if (_diskWriteCounter != null)
{
_diskWriteCounter.Dispose();
_diskWriteCounter = null;
}
#endif
}
}
#endregion
}
}
================================================
FILE: FocusLogger/JocysCom/ComponentModel/BindingListInvoked.cs
================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace JocysCom.ClassLibrary.ComponentModel
{
/// Marshals list modifications and notifications to a TaskScheduler (e.g., UI thread) to prevent cross-thread errors.
///
/// Provides AddRange for bulk addition and overrides to dispatch operations via SynchronizingObject, with optional async invocation.
/// Commented-out FixLeak method offers a WPF-specific memory leak workaround.
///
public class BindingListInvoked : BindingList
{
public BindingListInvoked() : base() { }
public BindingListInvoked(IList list)
: base(list) { }
public BindingListInvoked(IEnumerable enumeration)
: base(new List(enumeration)) { }
public void AddRange(IEnumerable list)
{
foreach (T item in list)
{ Add(item); }
}
#region ISynchronizeInvoker
/// TaskScheduler used to marshal list operations; null disables synchronization, executing operations on the calling thread.
public TaskScheduler SynchronizingObject { get; set; }
delegate void ItemDelegate(int index, T item);
/// When true, invocation uses Task.Factory.StartNew to queue asynchronously; when false, runs synchronously on the TaskScheduler.
public bool AsynchronousInvoke { get; set; }
// Dispatches the delegate to SynchronizingObject's TaskScheduler when required; respects AsynchronousInvoke for async vs sync execution.
void Invoke(Delegate method, params object[] args)
{
var so = SynchronizingObject;
if (so is null || !JocysCom.ClassLibrary.Controls.ControlsHelper.InvokeRequired)
{
DynamicInvoke(method, args);
}
else
{
// Note that Control.Invoke(...) is a synchronous action on the main GUI thread,
// and will wait for EnableBackControl() to return.
// so.Invoke(...) line could freeze if main GUI thread is busy and can't give
// attention to any .Invoke requests from background threads.
//
// Main GUI thread could be blocked because:
// a) Modal dialog is up (which means that it's not listening to new requests).
// b) It is checking something in a tight continuous loop.
// c) Main thread crashed because of exception.
//
// Try inserting a Application.DoEvents() in the loop, which will pause
// execution and force the main thread to process messages and any outstanding .Invoke requests.
if (AsynchronousInvoke)
Task.Factory.StartNew(() =>
{
DynamicInvoke(method, args);
}, CancellationToken.None, TaskCreationOptions.None, so);
else
{
var task = new Task(() =>
{
DynamicInvoke(method, args);
});
task.RunSynchronously(so);
}
}
}
// Lock to serialize concurrent list modifications.
object OneChangeAtTheTime = new object();
// Executes the delegate under a lock and enriches exceptions with type and SynchronizingObject context data.
void DynamicInvoke(Delegate method, params object[] args)
{
try
{
lock (OneChangeAtTheTime)
{
method.DynamicInvoke(args);
}
}
catch (Exception ex)
{
// Add data to help with debugging.
var prefix = string.Format("{0}", nameof(BindingListInvoked)) + ".";
ex.Data.Add(prefix + "T", typeof(T).FullName);
ex.Data.Add(prefix + "SynchronizingObject", SynchronizingObject?.GetType().FullName);
ex.Data.Add(prefix + "AsynchronousInvoke", AsynchronousInvoke);
throw;
}
}
protected override void RemoveItem(int index)
{
Invoke((Action)base.RemoveItem, index);
}
protected override void InsertItem(int index, T item)
{
Invoke((ItemDelegate)base.InsertItem, index, item);
}
protected override void SetItem(int index, T item)
{
Invoke((ItemDelegate)base.SetItem, index, item);
}
protected override void OnListChanged(ListChangedEventArgs e)
{
Invoke((Action)base.OnListChanged, e);
}
protected override void OnAddingNew(AddingNewEventArgs e)
{
Invoke((Action)base.OnAddingNew, e);
}
//public void FixLeak()
//{
// var flags = BindingFlags.Instance | BindingFlags.NonPublic;
// var fi = GetType().BaseType.BaseType.GetField("onListChanged", flags);
// var d = (Delegate)fi.GetValue(this);
// if (d != null)
// {
// if (d.Target is System.Windows.Data.BindingListCollectionView view)
// {
// view.DetachFromSourceCollection();
// var vfi = view.GetType().BaseType.GetField("_currentItem", flags);
// vfi.SetValue(view, null);
// fi.SetValue(this, null);
// }
// }
//}
#endregion
}
}
================================================
FILE: FocusLogger/JocysCom/ComponentModel/NotifyPropertyChanged.cs
================================================
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
namespace JocysCom.ClassLibrary.ComponentModel
{
///
/// INotifyPropertyChanged base with optional WPF Dispatcher marshalling of property-change notifications to the UI thread.
///
///
/// Supports MVVM data binding scenarios, with optional UI-thread invocation of property change events.
///
public class NotifyPropertyChanged : INotifyPropertyChanged
{
#region ■ INotifyPropertyChanged
private readonly SynchronizationContext _ctx = SynchronizationContext.Current ?? new SynchronizationContext();
///
/// Notifies clients that a property value has changed.
///
// SUPPRESS: CWE-502: Deserialization of Untrusted Data
// Fix: Apply [field: NonSerialized] attribute to an event inside class with [Serialized] attribute.
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
///
/// Raises the PropertyChanged event. When UseApplicationDispatcher is true, the invocation is marshaled to the WPF UI thread via Application.Current.Dispatcher.
///
/// Name of the property.
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (UseApplicationDispatcher)
{
// If already on different thread then...
if (SynchronizationContext.Current != _ctx)
{
void RaiseCore() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
_ctx.Post(_ => RaiseCore(), null);
return;
}
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
///
/// When true, marshals property-change notifications to the WPF UI thread via Application.Current.Dispatcher. Defaults to false.
///
[field: NonSerialized, DefaultValue(false)]
public bool UseApplicationDispatcher = false;
///
/// Sets the backing field if the new value differs (per Equals), then invokes OnPropertyChanged. Skips notifications when value is unchanged.
///
/// Type of the backing field.
/// Reference to the backing field.
/// New value to assign.
/// Name of the property; supplied by CallerMemberName automatically.
protected void SetProperty(ref T property, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(property, value))
return;
property = value;
// Invoke overridden OnPropertyChanged method in the most derived class of the object.
OnPropertyChanged(propertyName);
}
#endregion
}
}
================================================
FILE: FocusLogger/JocysCom/ComponentModel/PropertyComparer.cs
================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace JocysCom.ClassLibrary.ComponentModel
{
/// Implements IComparer<T> to sort items by a single PropertyDescriptor or a ListSortDescriptionCollection.
///
/// Part of Be.Timvw.Framework.ComponentModel (http://betimvwframework.codeplex.com/).
/// Used by SortableBindingList<T> to apply simple and advanced sorting in data-binding contexts.
/// A parallel SortComparer<T> in the Collections namespace provides an alternative implementation.
///
public class PropertyComparer : IComparer
{
private readonly ListSortDescriptionCollection _SortCollection = null;
private readonly PropertyDescriptor _PropDesc = null;
private readonly ListSortDirection _Direction = ListSortDirection.Ascending;
public PropertyComparer(PropertyDescriptor propDesc, ListSortDirection direction)
{
_PropDesc = propDesc;
_Direction = direction;
}
public PropertyComparer(ListSortDescriptionCollection sortCollection)
{
_SortCollection = sortCollection;
}
int IComparer.Compare(T x, T y)
{
return Compare(x, y);
}
///
/// Compares x and y using the configured sort criteria.
/// Uses the single PropertyDescriptor and direction if provided; otherwise applies the ListSortDescriptionCollection recursively.
/// Returns 0 when no sort criteria are specified.
///
protected int Compare(T x, T y)
{
if (_PropDesc != null)
{
var xValue = _PropDesc.GetValue(x);
var yValue = _PropDesc.GetValue(y);
return CompareValues(xValue, yValue, _Direction);
}
else if (_SortCollection != null && _SortCollection.Count > 0)
return RecursiveCompareInternal(x, y, 0);
else
return 0;
}
///
/// Compares two property values, using IComparable when available or falling back to string comparison, and applies sort direction.
///
private int CompareValues(object xValue, object yValue, ListSortDirection direction)
{
int retValue;
if (xValue is null && yValue is null)
retValue = 0;
else if (xValue is IComparable)
retValue = ((IComparable)xValue).CompareTo(yValue);
else if (yValue is IComparable)
retValue = ((IComparable)yValue).CompareTo(xValue);
else if (!xValue.Equals(yValue))
retValue = xValue.ToString().CompareTo(yValue.ToString());
else
retValue = 0;
return (direction == ListSortDirection.Ascending ? 1 : -1) * retValue;
}
///
/// Recursively compares items by each sort description in the collection until a non-zero result is found or the end is reached.
///
private int RecursiveCompareInternal(T x, T y, int index)
{
if (index >= _SortCollection.Count)
return 0;
var listSortDesc = _SortCollection[index];
var xValue = listSortDesc.PropertyDescriptor.GetValue(x);
var yValue = listSortDesc.PropertyDescriptor.GetValue(y);
var retValue = CompareValues(xValue, yValue, listSortDesc.SortDirection);
return (retValue == 0)
? RecursiveCompareInternal(x, y, ++index)
: retValue;
}
}
}
================================================
FILE: FocusLogger/JocysCom/ComponentModel/SortableBindingList.cs
================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace JocysCom.ClassLibrary.ComponentModel
{
///
/// Be.Timvw.Framework.ComponentModel
/// http://betimvwframework.codeplex.com/
///
///
[Serializable]
public class SortableBindingList : BindingListInvoked, IBindingListView, IRaiseItemChangedEvents
{
public SortableBindingList() : base() { }
public SortableBindingList(IList list)
: base(list) { }
public SortableBindingList(IEnumerable enumeration)
: base(new List(enumeration)) { }
public static SortableBindingList From(IEnumerable list)
{
return new SortableBindingList(list);
}
protected override bool SupportsSearchingCore => true;
protected override bool SupportsSortingCore => true;
protected override bool IsSortedCore => _Sorted;
protected override ListSortDirection SortDirectionCore => _SortDirection;
protected override PropertyDescriptor SortPropertyCore => _SortProperty;
ListSortDescriptionCollection IBindingListView.SortDescriptions => SortDescriptions;
protected ListSortDescriptionCollection SortDescriptions => _SortDescriptions;
bool IBindingListView.SupportsAdvancedSorting => SupportsAdvancedSorting;
protected bool SupportsAdvancedSorting => true;
bool IBindingListView.SupportsFiltering => SupportsFiltering;
protected bool SupportsFiltering => true;
bool IRaiseItemChangedEvents.RaisesItemChangedEvents => RaisesItemChangedEvents;
protected bool RaisesItemChangedEvents => true;
private bool _Sorted = false;
private bool _Filtered = false;
private string _FilterString = null;
private ListSortDirection _SortDirection = ListSortDirection.Ascending;
[NonSerialized]
private PropertyDescriptor _SortProperty = null;
[NonSerialized]
private ListSortDescriptionCollection _SortDescriptions = new ListSortDescriptionCollection();
private readonly List _OriginalCollection = new List();
bool IBindingList.AllowNew => CheckReadOnly();
bool IBindingList.AllowRemove => CheckReadOnly();
private bool CheckReadOnly() { return !_Sorted && !_Filtered; }
protected override int FindCore(PropertyDescriptor property, object key)
{
// Simple iteration:
for (var i = 0; i < Count; i++)
{
var item = this[i];
if (property.GetValue(item).Equals(key))
return i;
}
return -1; // Not found
}
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
{
_SortDirection = direction;
_SortProperty = property;
var comparer = new PropertyComparer(property, direction);
ApplySortInternal(comparer);
}
void IBindingListView.ApplySort(ListSortDescriptionCollection sorts)
{
ApplySort(sorts);
}
protected void ApplySort(ListSortDescriptionCollection sorts)
{
_SortProperty = null;
_SortDescriptions = sorts;
var comparer = new PropertyComparer(sorts);
ApplySortInternal(comparer);
}
private void ApplySortInternal(PropertyComparer comparer)
{
if (_OriginalCollection.Count == 0)
_OriginalCollection.AddRange(this);
var listRef = Items as List;
if (listRef is null)
return;
listRef.Sort(comparer);
_Sorted = true;
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
protected override void RemoveSortCore()
{
if (!_Sorted)
return;
Clear();
foreach (var item in _OriginalCollection)
Add(item);
_OriginalCollection.Clear();
_SortProperty = null;
_SortDescriptions = null;
_Sorted = false;
}
string IBindingListView.Filter { get { return Filter; } set { Filter = value; } }
protected string Filter
{
get { return _FilterString; }
set
{
_FilterString = value;
_Filtered = true;
UpdateFilter();
}
}
void IBindingListView.RemoveFilter() { RemoveFilter(); }
protected void RemoveFilter()
{
if (!_Filtered)
return;
_FilterString = null;
_Filtered = false;
_Sorted = false;
_SortDescriptions = null;
_SortProperty = null;
Clear();
foreach (var item in _OriginalCollection)
Add(item);
_OriginalCollection.Clear();
}
protected virtual void UpdateFilter()
{
var equalsPos = _FilterString.IndexOf('=');
// Get property name
var propName = _FilterString.Substring(0, equalsPos).Trim();
// Get filter criteria
var criteria = _FilterString.Substring(equalsPos + 1,
_FilterString.Length - equalsPos - 1).Trim();
// Strip leading and trailing quotes
criteria = criteria.Trim('\'', '"');
// Get a property descriptor for the filter property
var propDesc = TypeDescriptor.GetProperties(typeof(T))[propName];
if (_OriginalCollection.Count == 0)
_OriginalCollection.AddRange(this);
var currentCollection = new List(this);
Clear();
foreach (var item in currentCollection)
{
var value = propDesc.GetValue(item);
if (string.Format("{0}", value) == criteria)
Add(item);
}
}
protected override void InsertItem(int index, T item)
{
foreach (PropertyDescriptor propDesc in TypeDescriptor.GetProperties(item))
{
if (propDesc.SupportsChangeEvents)
propDesc.AddValueChanged(item, OnItemChanged);
}
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
var item = Items[index];
var propDescs = TypeDescriptor.GetProperties(item);
foreach (PropertyDescriptor propDesc in propDescs)
{
if (propDesc.SupportsChangeEvents)
propDesc.RemoveValueChanged(item, OnItemChanged);
}
base.RemoveItem(index);
}
private void OnItemChanged(object sender, EventArgs args)
{
var index = Items.IndexOf((T)sender);
OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged, index));
}
public void RemoveAll(Func