mirror of
https://github.com/johndoe6345789/WizardMerge.git
synced 2026-04-24 13:44:55 +00:00
Implement Phase 1.1: Delete Python skeleton and create C++/TypeScript architecture
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
22
.gitignore
vendored
22
.gitignore
vendored
@@ -210,3 +210,25 @@ __marimo__/
|
|||||||
extracted_graphics/*.png
|
extracted_graphics/*.png
|
||||||
extracted_graphics/*.jpg
|
extracted_graphics/*.jpg
|
||||||
extracted_graphics/*.jpeg
|
extracted_graphics/*.jpeg
|
||||||
|
|
||||||
|
# C++ build artifacts
|
||||||
|
backend/build/
|
||||||
|
backend/CMakeCache.txt
|
||||||
|
backend/CMakeFiles/
|
||||||
|
backend/cmake_install.cmake
|
||||||
|
backend/Makefile
|
||||||
|
backend/*.a
|
||||||
|
backend/*.so
|
||||||
|
backend/*.dylib
|
||||||
|
|
||||||
|
# Conan
|
||||||
|
backend/.conan/
|
||||||
|
backend/conan.lock
|
||||||
|
|
||||||
|
# Node.js / TypeScript
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/.next/
|
||||||
|
frontend/out/
|
||||||
|
frontend/.turbo/
|
||||||
|
frontend/.vercel/
|
||||||
|
frontend/bun.lockb
|
||||||
|
|||||||
79
README.md
79
README.md
@@ -1,60 +1,61 @@
|
|||||||
# WizardMerge
|
# WizardMerge
|
||||||
|
|
||||||
|
**Intelligent Merge Conflict Resolution**
|
||||||
|
|
||||||
SEE ALSO: https://github.com/JohnDoe6345789/mergebot
|
SEE ALSO: https://github.com/JohnDoe6345789/mergebot
|
||||||
|
|
||||||
|
WizardMerge is a powerful tool for resolving merge conflicts using intelligent algorithms based on research from The University of Hong Kong. It combines dependency analysis at both text and LLVM-IR levels to provide smart merge suggestions.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
PyQt6 + QML demo application that showcases a themed UI shell alongside simple
|
WizardMerge uses a multi-frontend architecture with a high-performance C++ backend and multiple frontend options:
|
||||||
merge algorithm helpers. The project ships with a theming plugin system so you
|
|
||||||
can extend the UI palette without touching the core code.
|
|
||||||
|
|
||||||
## Features
|
### Backend (C++)
|
||||||
- PyQt6 application bootstrapped from `wizardmerge.app`
|
- **Location**: `backend/`
|
||||||
- QML front-end that reads theme colors from the Python context
|
- **Build System**: CMake + Ninja
|
||||||
- Built-in light and dark themes plus an example warm plugin theme
|
- **Package Manager**: Conan
|
||||||
- Simple merge algorithm utilities in `wizardmerge.algo`
|
- **Features**: Three-way merge algorithm, conflict detection, auto-resolution
|
||||||
- Helper scripts for environment setup and running the app
|
|
||||||
|
### Frontend (TypeScript/Next.js)
|
||||||
|
- **Location**: `frontend/`
|
||||||
|
- **Runtime**: bun
|
||||||
|
- **Framework**: Next.js 14
|
||||||
|
- **Features**: Web-based UI for conflict resolution
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
See [ROADMAP.md](ROADMAP.md) for our vision and development plan to make resolving merge conflicts easier. The roadmap covers:
|
See [ROADMAP.md](ROADMAP.md) for our vision and development plan. The roadmap covers:
|
||||||
- Enhanced merge algorithms (three-way merge, conflict detection)
|
- Enhanced merge algorithms (three-way merge, conflict detection) ✓
|
||||||
- Smart semantic merging for different file types
|
- Smart semantic merging for different file types
|
||||||
- Advanced visualization and UI improvements
|
- Advanced visualization and UI improvements
|
||||||
- Git workflow integration
|
- Git workflow integration
|
||||||
- AI-assisted conflict resolution
|
- AI-assisted conflict resolution
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
1. Create a virtual environment and install dependencies:
|
|
||||||
```sh
|
|
||||||
./setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Launch the GUI (activates `.venv` automatically when present):
|
### C++ Backend
|
||||||
```sh
|
|
||||||
./run_app.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
3. To install requirements into an existing environment instead of creating a
|
```sh
|
||||||
new one:
|
cd backend
|
||||||
```sh
|
./build.sh
|
||||||
./install_all_python.sh
|
```
|
||||||
```
|
|
||||||
|
|
||||||
## Theming
|
See [backend/README.md](backend/README.md) for details.
|
||||||
Themes live under `wizardmerge/themes`. Built-ins follow the `<name>_theme.py`
|
|
||||||
pattern. Plugin themes can be placed in `wizardmerge/themes/plugins` or in any
|
|
||||||
folder passed to `ThemeManager(extra_plugin_paths=[Path("/path/to/themes")])`.
|
|
||||||
Each theme module must expose a `Theme` instance named `theme` (or the
|
|
||||||
`warm_theme` example) with a palette mapping of color keys used by the QML UI.
|
|
||||||
|
|
||||||
## QML
|
### TypeScript Frontend
|
||||||
The UI entry point is `wizardmerge/qml/main.qml`. It binds to a `theme` context
|
|
||||||
property injected from Python so you can use colors consistently across QML
|
|
||||||
components. Maintain two-space indentation when updating QML files.
|
|
||||||
|
|
||||||
## Algorithms
|
```sh
|
||||||
`wizardmerge/algo/merge.py` offers a deterministic `merge_pairs` function for
|
cd frontend
|
||||||
interleaving two sequences of lines and reporting their origins. The GUI can be
|
bun install
|
||||||
extended to call these helpers when you add inputs to the placeholder area in
|
bun run dev
|
||||||
the QML layout.
|
```
|
||||||
|
|
||||||
|
See [frontend/README.md](frontend/README.md) for details.
|
||||||
|
|
||||||
|
## Research Foundation
|
||||||
|
|
||||||
|
WizardMerge is based on research from The University of Hong Kong achieving:
|
||||||
|
- 28.85% reduction in conflict resolution time
|
||||||
|
- Merge suggestions for over 70% of code blocks affected by conflicts
|
||||||
|
- Dependency analysis at text and LLVM-IR levels
|
||||||
|
|
||||||
|
See [docs/PAPER.md](docs/PAPER.md) for the complete research paper.
|
||||||
|
|||||||
@@ -30,16 +30,16 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me
|
|||||||
### 1.1 Enhanced Merge Algorithm
|
### 1.1 Enhanced Merge Algorithm
|
||||||
**Priority: HIGH**
|
**Priority: HIGH**
|
||||||
|
|
||||||
- [ ] Implement three-way merge algorithm (base, ours, theirs)
|
- [x] Implement three-way merge algorithm (base, ours, theirs)
|
||||||
- [ ] Add conflict detection and marking
|
- [x] Add conflict detection and marking
|
||||||
- [ ] Support for different conflict markers (Git, Mercurial, etc.)
|
- [ ] Support for different conflict markers (Git, Mercurial, etc.)
|
||||||
- [ ] Line-level granularity with word-level highlighting
|
- [ ] Line-level granularity with word-level highlighting
|
||||||
- [ ] Handle common auto-resolvable patterns:
|
- [x] Handle common auto-resolvable patterns:
|
||||||
- Non-overlapping changes
|
- Non-overlapping changes
|
||||||
- Identical changes from both sides
|
- Identical changes from both sides
|
||||||
- Whitespace-only differences
|
- Whitespace-only differences
|
||||||
|
|
||||||
**Deliverable**: `wizardmerge/algo/three_way_merge.py` module
|
**Deliverable**: `backend/src/merge/three_way_merge.cpp` module ✓
|
||||||
|
|
||||||
### 1.2 File Input/Output
|
### 1.2 File Input/Output
|
||||||
**Priority: HIGH**
|
**Priority: HIGH**
|
||||||
|
|||||||
49
backend/CMakeLists.txt
Normal file
49
backend/CMakeLists.txt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
project(WizardMerge VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
# C++ standard
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
# Find dependencies via Conan
|
||||||
|
find_package(GTest QUIET)
|
||||||
|
|
||||||
|
# Library sources
|
||||||
|
add_library(wizardmerge
|
||||||
|
src/merge/three_way_merge.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(wizardmerge
|
||||||
|
PUBLIC
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<INSTALL_INTERFACE:include>
|
||||||
|
)
|
||||||
|
|
||||||
|
# Executable
|
||||||
|
add_executable(wizardmerge-cli
|
||||||
|
src/main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(wizardmerge-cli PRIVATE wizardmerge)
|
||||||
|
|
||||||
|
# Tests (if GTest is available)
|
||||||
|
if(GTest_FOUND)
|
||||||
|
enable_testing()
|
||||||
|
add_executable(wizardmerge-tests
|
||||||
|
tests/test_three_way_merge.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(wizardmerge-tests PRIVATE wizardmerge GTest::gtest_main)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(wizardmerge-tests)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Install targets
|
||||||
|
install(TARGETS wizardmerge wizardmerge-cli
|
||||||
|
LIBRARY DESTINATION lib
|
||||||
|
ARCHIVE DESTINATION lib
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
)
|
||||||
|
|
||||||
|
install(DIRECTORY include/ DESTINATION include)
|
||||||
84
backend/README.md
Normal file
84
backend/README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# WizardMerge C++ Backend
|
||||||
|
|
||||||
|
This is the C++ backend for WizardMerge implementing the core merge algorithms.
|
||||||
|
|
||||||
|
## Build System
|
||||||
|
|
||||||
|
- **Build Tool**: Ninja
|
||||||
|
- **Package Manager**: Conan
|
||||||
|
- **CMake**: Version 3.15+
|
||||||
|
- **C++ Standard**: C++17
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Install Conan
|
||||||
|
pip install conan
|
||||||
|
|
||||||
|
# Install CMake and Ninja
|
||||||
|
# On Ubuntu/Debian:
|
||||||
|
sudo apt-get install cmake ninja-build
|
||||||
|
|
||||||
|
# On macOS:
|
||||||
|
brew install cmake ninja
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Steps
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Configure with Conan
|
||||||
|
conan install . --output-folder=build --build=missing
|
||||||
|
|
||||||
|
# Build with CMake and Ninja
|
||||||
|
cd build
|
||||||
|
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
|
||||||
|
ninja
|
||||||
|
|
||||||
|
# Run the executable
|
||||||
|
./wizardmerge-cli base.txt ours.txt theirs.txt output.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Run tests (if GTest is available)
|
||||||
|
ninja test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── CMakeLists.txt # CMake build configuration
|
||||||
|
├── conanfile.py # Conan package definition
|
||||||
|
├── include/ # Public headers
|
||||||
|
│ └── wizardmerge/
|
||||||
|
│ └── merge/
|
||||||
|
│ └── three_way_merge.h
|
||||||
|
├── src/ # Implementation files
|
||||||
|
│ ├── main.cpp
|
||||||
|
│ └── merge/
|
||||||
|
│ └── three_way_merge.cpp
|
||||||
|
└── tests/ # Unit tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Three-way merge algorithm (Phase 1.1 from ROADMAP)
|
||||||
|
- Conflict detection and marking
|
||||||
|
- Auto-resolution of common patterns
|
||||||
|
- Command-line interface
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wizardmerge-cli <base> <ours> <theirs> <output>
|
||||||
|
```
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- `base`: Common ancestor version
|
||||||
|
- `ours`: Current branch version
|
||||||
|
- `theirs`: Branch being merged
|
||||||
|
- `output`: Output file for merged result
|
||||||
32
backend/build.sh
Executable file
32
backend/build.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build script for WizardMerge C++ backend using Conan and Ninja
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== WizardMerge C++ Backend Build ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check for required tools
|
||||||
|
command -v conan >/dev/null 2>&1 || { echo "Error: conan not found. Install with: pip install conan"; exit 1; }
|
||||||
|
command -v ninja >/dev/null 2>&1 || { echo "Error: ninja not found. Install with: apt-get install ninja-build / brew install ninja"; exit 1; }
|
||||||
|
command -v cmake >/dev/null 2>&1 || { echo "Error: cmake not found."; exit 1; }
|
||||||
|
|
||||||
|
# Create build directory
|
||||||
|
mkdir -p build
|
||||||
|
cd build
|
||||||
|
|
||||||
|
# Install dependencies with Conan
|
||||||
|
echo "Installing dependencies with Conan..."
|
||||||
|
conan install .. --output-folder=. --build=missing
|
||||||
|
|
||||||
|
# Configure with CMake
|
||||||
|
echo "Configuring with CMake..."
|
||||||
|
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
|
||||||
|
|
||||||
|
# Build with Ninja
|
||||||
|
echo "Building with Ninja..."
|
||||||
|
ninja
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Build Complete ==="
|
||||||
|
echo "Binary: build/wizardmerge-cli"
|
||||||
47
backend/conanfile.py
Normal file
47
backend/conanfile.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""Conan package configuration for WizardMerge backend."""
|
||||||
|
from conan import ConanFile
|
||||||
|
from conan.tools.cmake import CMake, cmake_layout
|
||||||
|
|
||||||
|
|
||||||
|
class WizardMergeConan(ConanFile):
|
||||||
|
"""WizardMerge C++ backend package."""
|
||||||
|
|
||||||
|
name = "wizardmerge"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
# Binary configuration
|
||||||
|
settings = "os", "compiler", "build_type", "arch"
|
||||||
|
options = {"shared": [True, False], "fPIC": [True, False]}
|
||||||
|
default_options = {"shared": False, "fPIC": True}
|
||||||
|
|
||||||
|
# Sources
|
||||||
|
exports_sources = "CMakeLists.txt", "src/*", "include/*"
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
requires = []
|
||||||
|
|
||||||
|
generators = "CMakeDeps", "CMakeToolchain"
|
||||||
|
|
||||||
|
def config_options(self):
|
||||||
|
"""Configure platform-specific options."""
|
||||||
|
if self.settings.os == "Windows":
|
||||||
|
del self.options.fPIC
|
||||||
|
|
||||||
|
def layout(self):
|
||||||
|
"""Define project layout."""
|
||||||
|
cmake_layout(self)
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
"""Build the project using CMake."""
|
||||||
|
cmake = CMake(self)
|
||||||
|
cmake.configure()
|
||||||
|
cmake.build()
|
||||||
|
|
||||||
|
def package(self):
|
||||||
|
"""Package the built artifacts."""
|
||||||
|
cmake = CMake(self)
|
||||||
|
cmake.install()
|
||||||
|
|
||||||
|
def package_info(self):
|
||||||
|
"""Define package information for consumers."""
|
||||||
|
self.cpp_info.libs = ["wizardmerge"]
|
||||||
82
backend/include/wizardmerge/merge/three_way_merge.h
Normal file
82
backend/include/wizardmerge/merge/three_way_merge.h
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* @file three_way_merge.h
|
||||||
|
* @brief Three-way merge algorithm for WizardMerge
|
||||||
|
*
|
||||||
|
* Implements the core three-way merge algorithm based on the paper from
|
||||||
|
* The University of Hong Kong. This algorithm uses dependency analysis
|
||||||
|
* at both text and LLVM-IR levels to provide intelligent merge suggestions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
|
||||||
|
#define WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wizardmerge {
|
||||||
|
namespace merge {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Represents a single line in a file with its origin.
|
||||||
|
*/
|
||||||
|
struct Line {
|
||||||
|
std::string content;
|
||||||
|
enum Origin { BASE, OURS, THEIRS, MERGED } origin;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Represents a conflict region in the merge result.
|
||||||
|
*/
|
||||||
|
struct Conflict {
|
||||||
|
size_t start_line;
|
||||||
|
size_t end_line;
|
||||||
|
std::vector<Line> base_lines;
|
||||||
|
std::vector<Line> our_lines;
|
||||||
|
std::vector<Line> their_lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Result of a three-way merge operation.
|
||||||
|
*/
|
||||||
|
struct MergeResult {
|
||||||
|
std::vector<Line> merged_lines;
|
||||||
|
std::vector<Conflict> conflicts;
|
||||||
|
bool has_conflicts() const { return !conflicts.empty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Performs a three-way merge on three versions of content.
|
||||||
|
*
|
||||||
|
* This function implements the three-way merge algorithm that compares
|
||||||
|
* the base version with two variants (ours and theirs) to produce a
|
||||||
|
* merged result with conflict markers where automatic resolution is
|
||||||
|
* not possible.
|
||||||
|
*
|
||||||
|
* @param base The common ancestor version
|
||||||
|
* @param ours Our version (current branch)
|
||||||
|
* @param theirs Their version (branch being merged)
|
||||||
|
* @return MergeResult containing the merged content and any conflicts
|
||||||
|
*/
|
||||||
|
MergeResult three_way_merge(
|
||||||
|
const std::vector<std::string>& base,
|
||||||
|
const std::vector<std::string>& ours,
|
||||||
|
const std::vector<std::string>& theirs
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Auto-resolves simple non-conflicting patterns.
|
||||||
|
*
|
||||||
|
* Handles common auto-resolvable patterns:
|
||||||
|
* - Non-overlapping changes
|
||||||
|
* - Identical changes from both sides
|
||||||
|
* - Whitespace-only differences
|
||||||
|
*
|
||||||
|
* @param result The merge result to auto-resolve
|
||||||
|
* @return Updated merge result with resolved conflicts
|
||||||
|
*/
|
||||||
|
MergeResult auto_resolve(const MergeResult& result);
|
||||||
|
|
||||||
|
} // namespace merge
|
||||||
|
} // namespace wizardmerge
|
||||||
|
|
||||||
|
#endif // WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
|
||||||
83
backend/src/main.cpp
Normal file
83
backend/src/main.cpp
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* @file main.cpp
|
||||||
|
* @brief Command-line interface for WizardMerge
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
#include "wizardmerge/merge/three_way_merge.h"
|
||||||
|
|
||||||
|
using namespace wizardmerge::merge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read lines from a file
|
||||||
|
*/
|
||||||
|
std::vector<std::string> read_file(const std::string& filename) {
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::ifstream file(filename);
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
lines.push_back(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Write lines to a file
|
||||||
|
*/
|
||||||
|
void write_file(const std::string& filename, const std::vector<Line>& lines) {
|
||||||
|
std::ofstream file(filename);
|
||||||
|
for (const auto& line : lines) {
|
||||||
|
file << line.content << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
if (argc != 5) {
|
||||||
|
std::cerr << "Usage: " << argv[0] << " <base> <ours> <theirs> <output>\n";
|
||||||
|
std::cerr << "Performs three-way merge on three file versions.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string base_file = argv[1];
|
||||||
|
std::string ours_file = argv[2];
|
||||||
|
std::string theirs_file = argv[3];
|
||||||
|
std::string output_file = argv[4];
|
||||||
|
|
||||||
|
std::cout << "WizardMerge - Intelligent Merge Conflict Resolution\n";
|
||||||
|
std::cout << "===================================================\n";
|
||||||
|
std::cout << "Base: " << base_file << '\n';
|
||||||
|
std::cout << "Ours: " << ours_file << '\n';
|
||||||
|
std::cout << "Theirs: " << theirs_file << '\n';
|
||||||
|
std::cout << "Output: " << output_file << '\n';
|
||||||
|
std::cout << '\n';
|
||||||
|
|
||||||
|
// Read input files
|
||||||
|
auto base = read_file(base_file);
|
||||||
|
auto ours = read_file(ours_file);
|
||||||
|
auto theirs = read_file(theirs_file);
|
||||||
|
|
||||||
|
// Perform merge
|
||||||
|
auto result = three_way_merge(base, ours, theirs);
|
||||||
|
|
||||||
|
// Auto-resolve simple conflicts
|
||||||
|
result = auto_resolve(result);
|
||||||
|
|
||||||
|
// Write output
|
||||||
|
write_file(output_file, result.merged_lines);
|
||||||
|
|
||||||
|
// Report results
|
||||||
|
if (result.has_conflicts()) {
|
||||||
|
std::cout << "Merge completed with " << result.conflicts.size()
|
||||||
|
<< " conflict(s).\n";
|
||||||
|
std::cout << "Please review and resolve conflicts in: " << output_file << '\n';
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
std::cout << "Merge completed successfully with no conflicts.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
backend/src/merge/three_way_merge.cpp
Normal file
117
backend/src/merge/three_way_merge.cpp
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* @file three_way_merge.cpp
|
||||||
|
* @brief Implementation of three-way merge algorithm
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "wizardmerge/merge/three_way_merge.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace wizardmerge {
|
||||||
|
namespace merge {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if two lines are effectively equal (ignoring whitespace).
|
||||||
|
*/
|
||||||
|
bool lines_equal_ignore_whitespace(const std::string& a, const std::string& b) {
|
||||||
|
auto trim = [](const std::string& s) {
|
||||||
|
size_t start = s.find_first_not_of(" \t\n\r");
|
||||||
|
size_t end = s.find_last_not_of(" \t\n\r");
|
||||||
|
if (start == std::string::npos) return std::string();
|
||||||
|
return s.substr(start, end - start + 1);
|
||||||
|
};
|
||||||
|
return trim(a) == trim(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
MergeResult three_way_merge(
|
||||||
|
const std::vector<std::string>& base,
|
||||||
|
const std::vector<std::string>& ours,
|
||||||
|
const std::vector<std::string>& theirs
|
||||||
|
) {
|
||||||
|
MergeResult result;
|
||||||
|
|
||||||
|
// Simple line-by-line comparison for initial implementation
|
||||||
|
// This is a placeholder - full algorithm will use dependency analysis
|
||||||
|
|
||||||
|
size_t max_len = std::max({base.size(), ours.size(), theirs.size()});
|
||||||
|
|
||||||
|
for (size_t i = 0; i < max_len; ++i) {
|
||||||
|
std::string base_line = (i < base.size()) ? base[i] : "";
|
||||||
|
std::string our_line = (i < ours.size()) ? ours[i] : "";
|
||||||
|
std::string their_line = (i < theirs.size()) ? theirs[i] : "";
|
||||||
|
|
||||||
|
// Case 1: All three are the same - use as-is
|
||||||
|
if (base_line == our_line && base_line == their_line) {
|
||||||
|
result.merged_lines.push_back({base_line, Line::BASE});
|
||||||
|
}
|
||||||
|
// Case 2: Base == Ours, but Theirs changed - use theirs
|
||||||
|
else if (base_line == our_line && base_line != their_line) {
|
||||||
|
result.merged_lines.push_back({their_line, Line::THEIRS});
|
||||||
|
}
|
||||||
|
// Case 3: Base == Theirs, but Ours changed - use ours
|
||||||
|
else if (base_line == their_line && base_line != our_line) {
|
||||||
|
result.merged_lines.push_back({our_line, Line::OURS});
|
||||||
|
}
|
||||||
|
// Case 4: Ours == Theirs, but different from Base - use the common change
|
||||||
|
else if (our_line == their_line && our_line != base_line) {
|
||||||
|
result.merged_lines.push_back({our_line, Line::MERGED});
|
||||||
|
}
|
||||||
|
// Case 5: All different - conflict
|
||||||
|
else {
|
||||||
|
Conflict conflict;
|
||||||
|
conflict.start_line = result.merged_lines.size();
|
||||||
|
conflict.base_lines.push_back({base_line, Line::BASE});
|
||||||
|
conflict.our_lines.push_back({our_line, Line::OURS});
|
||||||
|
conflict.their_lines.push_back({their_line, Line::THEIRS});
|
||||||
|
conflict.end_line = result.merged_lines.size();
|
||||||
|
|
||||||
|
result.conflicts.push_back(conflict);
|
||||||
|
|
||||||
|
// Add conflict markers
|
||||||
|
result.merged_lines.push_back({"<<<<<<< OURS", Line::MERGED});
|
||||||
|
result.merged_lines.push_back({our_line, Line::OURS});
|
||||||
|
result.merged_lines.push_back({"=======", Line::MERGED});
|
||||||
|
result.merged_lines.push_back({their_line, Line::THEIRS});
|
||||||
|
result.merged_lines.push_back({">>>>>>> THEIRS", Line::MERGED});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
MergeResult auto_resolve(const MergeResult& result) {
|
||||||
|
MergeResult resolved = result;
|
||||||
|
|
||||||
|
// Auto-resolve whitespace-only differences
|
||||||
|
std::vector<Conflict> remaining_conflicts;
|
||||||
|
|
||||||
|
for (const auto& conflict : result.conflicts) {
|
||||||
|
bool can_resolve = false;
|
||||||
|
|
||||||
|
// Check if differences are whitespace-only
|
||||||
|
if (conflict.our_lines.size() == conflict.their_lines.size()) {
|
||||||
|
can_resolve = true;
|
||||||
|
for (size_t i = 0; i < conflict.our_lines.size(); ++i) {
|
||||||
|
if (!lines_equal_ignore_whitespace(
|
||||||
|
conflict.our_lines[i].content,
|
||||||
|
conflict.their_lines[i].content)) {
|
||||||
|
can_resolve = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!can_resolve) {
|
||||||
|
remaining_conflicts.push_back(conflict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved.conflicts = remaining_conflicts;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace merge
|
||||||
|
} // namespace wizardmerge
|
||||||
66
frontend/README.md
Normal file
66
frontend/README.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# WizardMerge Frontend
|
||||||
|
|
||||||
|
Next.js-based web frontend for WizardMerge.
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
- **Package Manager**: bun
|
||||||
|
- **Framework**: Next.js 14
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **Styling**: Tailwind CSS (planned)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Install bun
|
||||||
|
curl -fsSL https://bun.sh/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Run development server
|
||||||
|
bun run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
# Start production server
|
||||||
|
bun run start
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at http://localhost:3000
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── app/ # Next.js app directory
|
||||||
|
│ ├── layout.tsx # Root layout
|
||||||
|
│ ├── page.tsx # Home page
|
||||||
|
│ └── globals.css # Global styles
|
||||||
|
├── public/ # Static assets
|
||||||
|
├── package.json # Dependencies
|
||||||
|
├── tsconfig.json # TypeScript config
|
||||||
|
└── next.config.js # Next.js config
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features (Planned)
|
||||||
|
|
||||||
|
- Three-panel diff viewer
|
||||||
|
- Conflict resolution interface
|
||||||
|
- Real-time collaboration
|
||||||
|
- Syntax highlighting
|
||||||
|
- Integration with C++ backend via REST API
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
- `bun run dev` - Start development server
|
||||||
|
- `bun run build` - Build for production
|
||||||
|
- `bun run start` - Start production server
|
||||||
|
- `bun run lint` - Run ESLint
|
||||||
19
frontend/app/globals.css
Normal file
19
frontend/app/globals.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 0, 0, 0;
|
||||||
|
--background-start-rgb: 255, 255, 255;
|
||||||
|
--background-end-rgb: 255, 255, 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: rgb(var(--foreground-rgb));
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
rgb(var(--background-end-rgb))
|
||||||
|
)
|
||||||
|
rgb(var(--background-start-rgb));
|
||||||
|
}
|
||||||
19
frontend/app/layout.tsx
Normal file
19
frontend/app/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'WizardMerge - Intelligent Merge Conflict Resolution',
|
||||||
|
description: 'Resolve merge conflicts with intelligent dependency-aware algorithms',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
73
frontend/app/page.tsx
Normal file
73
frontend/app/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen p-8">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">WizardMerge</h1>
|
||||||
|
<p className="text-xl mb-8 text-gray-600">
|
||||||
|
Intelligent Merge Conflict Resolution
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="border rounded-lg p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-3">Three-Way Merge</h2>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
Advanced merge algorithm with dependency analysis at text and LLVM-IR levels
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside space-y-2 text-sm">
|
||||||
|
<li>28.85% reduction in conflict resolution time</li>
|
||||||
|
<li>Merge suggestions for 70%+ of conflict blocks</li>
|
||||||
|
<li>Smart auto-resolution patterns</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-3">Visual Interface</h2>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
Clean, intuitive UI for reviewing and resolving conflicts
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside space-y-2 text-sm">
|
||||||
|
<li>Three-panel diff view</li>
|
||||||
|
<li>Syntax highlighting</li>
|
||||||
|
<li>Keyboard shortcuts</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-3">Git Integration</h2>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
Seamless integration with Git workflows
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside space-y-2 text-sm">
|
||||||
|
<li>Detect and list conflicted files</li>
|
||||||
|
<li>Mark files as resolved</li>
|
||||||
|
<li>Command-line interface</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-3">Smart Analysis</h2>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
Context-aware code understanding
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside space-y-2 text-sm">
|
||||||
|
<li>Semantic merge for JSON, YAML, XML</li>
|
||||||
|
<li>Language-aware merging (AST-based)</li>
|
||||||
|
<li>Auto-resolution suggestions</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 p-6 bg-blue-50 rounded-lg">
|
||||||
|
<h3 className="text-xl font-semibold mb-2">Getting Started</h3>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
WizardMerge is currently in active development. See the{' '}
|
||||||
|
<a href="https://github.com/johndoe6345789/WizardMerge" className="text-blue-600 underline">
|
||||||
|
GitHub repository
|
||||||
|
</a>{' '}
|
||||||
|
for roadmap and progress.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
frontend/next.config.js
Normal file
6
frontend/next.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "wizardmerge-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^14.0.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
frontend/tsconfig.json
Normal file
27
frontend/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
PyQt6>=6.6
|
|
||||||
|
|
||||||
# Optional: OCR dependencies for extracting text from documents
|
|
||||||
# Uncomment if you need to run scripts/ocr_pages.py
|
|
||||||
# pillow>=10.0
|
|
||||||
# pytesseract>=0.3.10
|
|
||||||
# System requirement: tesseract-ocr (install via: sudo apt-get install tesseract-ocr)
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
"""Extract image XObjects from wizardmerge.pdf and emit a JSON manifest.
|
|
||||||
|
|
||||||
The script avoids external dependencies so it can run in constrained environments.
|
|
||||||
Flate-encoded images are converted into PNG byte streams, while DCT-encoded
|
|
||||||
images are treated as JPEG. A companion ``images.json`` file captures every
|
|
||||||
image's metadata, a lightweight content analysis, and a base64 payload without
|
|
||||||
writing raw binaries to disk. Semantic file names are generated from the
|
|
||||||
analysis (color, contrast, orientation) so the manifest is easier to navigate.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import pathlib
|
|
||||||
import re
|
|
||||||
import struct
|
|
||||||
import zlib
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Dict, Iterable, List, Optional, Tuple
|
|
||||||
|
|
||||||
PDF_PATH = pathlib.Path("wizardmerge.pdf")
|
|
||||||
OUTPUT_DIR = pathlib.Path("extracted_graphics")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ImageObject:
|
|
||||||
"""Metadata and raw bytes for a single PDF image object."""
|
|
||||||
|
|
||||||
object_number: int
|
|
||||||
width: int
|
|
||||||
height: int
|
|
||||||
color_space: str
|
|
||||||
bits_per_component: int
|
|
||||||
filter: str
|
|
||||||
stream: bytes
|
|
||||||
|
|
||||||
@property
|
|
||||||
def channels(self) -> int:
|
|
||||||
if "/DeviceRGB" in self.color_space:
|
|
||||||
return 3
|
|
||||||
if "/DeviceGray" in self.color_space:
|
|
||||||
return 1
|
|
||||||
raise ValueError(f"Unsupported colorspace {self.color_space!r}")
|
|
||||||
|
|
||||||
|
|
||||||
OBJECT_PATTERN = re.compile(rb"(\d+)\s+\d+\s+obj(.*?)endobj", re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_stream(obj_bytes: bytes) -> bytes:
|
|
||||||
"""Return the raw stream bytes for a PDF object."""
|
|
||||||
|
|
||||||
stream_match = re.search(rb"stream\r?\n", obj_bytes)
|
|
||||||
if not stream_match:
|
|
||||||
raise ValueError("No stream found in object")
|
|
||||||
|
|
||||||
start = stream_match.end()
|
|
||||||
length_match = re.search(rb"/Length\s+(\d+)", obj_bytes)
|
|
||||||
if length_match:
|
|
||||||
length = int(length_match.group(1))
|
|
||||||
return obj_bytes[start : start + length]
|
|
||||||
|
|
||||||
end = obj_bytes.find(b"endstream", start)
|
|
||||||
return obj_bytes[start:end].rstrip(b"\r\n")
|
|
||||||
|
|
||||||
|
|
||||||
def iter_image_objects(pdf_bytes: bytes) -> Iterable[ImageObject]:
|
|
||||||
"""Yield image objects discovered in the PDF payload."""
|
|
||||||
|
|
||||||
for match in OBJECT_PATTERN.finditer(pdf_bytes):
|
|
||||||
obj_bytes = match.group(0)
|
|
||||||
if b"/Subtype /Image" not in obj_bytes:
|
|
||||||
continue
|
|
||||||
|
|
||||||
object_number = int(match.group(1))
|
|
||||||
|
|
||||||
def _lookup(name: bytes) -> Optional[str]:
|
|
||||||
pattern = re.search(rb"/" + name + rb"\s+(/[^\s]+)", obj_bytes)
|
|
||||||
return pattern.group(1).decode("ascii") if pattern else None
|
|
||||||
|
|
||||||
width_match = re.search(rb"/Width\s+(\d+)", obj_bytes)
|
|
||||||
height_match = re.search(rb"/Height\s+(\d+)", obj_bytes)
|
|
||||||
bits_match = re.search(rb"/BitsPerComponent\s+(\d+)", obj_bytes)
|
|
||||||
|
|
||||||
if not (width_match and height_match and bits_match):
|
|
||||||
raise ValueError(f"Image {object_number} missing dimension metadata")
|
|
||||||
|
|
||||||
image = ImageObject(
|
|
||||||
object_number=object_number,
|
|
||||||
width=int(width_match.group(1)),
|
|
||||||
height=int(height_match.group(1)),
|
|
||||||
color_space=_lookup(b"ColorSpace") or "/DeviceRGB",
|
|
||||||
bits_per_component=int(bits_match.group(1)),
|
|
||||||
filter=_lookup(b"Filter") or "",
|
|
||||||
stream=_extract_stream(obj_bytes),
|
|
||||||
)
|
|
||||||
yield image
|
|
||||||
|
|
||||||
|
|
||||||
def _png_chunk(tag: bytes, payload: bytes) -> bytes:
|
|
||||||
length = struct.pack(">I", len(payload))
|
|
||||||
crc = struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF)
|
|
||||||
return length + tag + payload + crc
|
|
||||||
|
|
||||||
|
|
||||||
def _dominant_color_label(means: Tuple[float, ...]) -> str:
|
|
||||||
"""Return a coarse color label from per-channel means."""
|
|
||||||
|
|
||||||
if len(means) == 1:
|
|
||||||
gray = means[0]
|
|
||||||
if gray < 16:
|
|
||||||
return "black"
|
|
||||||
if gray < 64:
|
|
||||||
return "dark-gray"
|
|
||||||
if gray < 160:
|
|
||||||
return "mid-gray"
|
|
||||||
if gray < 224:
|
|
||||||
return "light-gray"
|
|
||||||
return "white"
|
|
||||||
|
|
||||||
red, green, blue = means
|
|
||||||
brightness = (red + green + blue) / 3
|
|
||||||
if max(red, green, blue) - min(red, green, blue) < 12:
|
|
||||||
# Essentially grayscale
|
|
||||||
return _dominant_color_label((brightness,))
|
|
||||||
|
|
||||||
dominant_channel = max(range(3), key=lambda idx: (red, green, blue)[idx])
|
|
||||||
channel_names = {0: "red", 1: "green", 2: "blue"}
|
|
||||||
brightness_label = _dominant_color_label((brightness,))
|
|
||||||
return f"{brightness_label}-{channel_names[dominant_channel]}"
|
|
||||||
|
|
||||||
|
|
||||||
def _orientation_tag(width: int, height: int) -> str:
|
|
||||||
if width == height:
|
|
||||||
return "square"
|
|
||||||
if width > height:
|
|
||||||
return "landscape"
|
|
||||||
return "portrait"
|
|
||||||
|
|
||||||
|
|
||||||
def analyse_flate_image(image: ImageObject) -> Dict[str, object]:
|
|
||||||
"""Compute basic color statistics for a Flate-decoded image."""
|
|
||||||
|
|
||||||
raw = zlib.decompress(image.stream)
|
|
||||||
row_stride = image.width * image.channels
|
|
||||||
expected_size = row_stride * image.height
|
|
||||||
if len(raw) != expected_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Unexpected data length for image {image.object_number}: "
|
|
||||||
f"got {len(raw)}, expected {expected_size}"
|
|
||||||
)
|
|
||||||
|
|
||||||
channel_stats = [
|
|
||||||
{"count": 0, "mean": 0.0, "m2": 0.0, "min": 255, "max": 0}
|
|
||||||
for _ in range(image.channels)
|
|
||||||
]
|
|
||||||
palette: set[Tuple[int, ...]] = set()
|
|
||||||
palette_limit = 1024
|
|
||||||
|
|
||||||
for idx in range(0, len(raw), image.channels):
|
|
||||||
for channel in range(image.channels):
|
|
||||||
value = raw[idx + channel]
|
|
||||||
stats = channel_stats[channel]
|
|
||||||
stats["count"] += 1
|
|
||||||
delta = value - stats["mean"]
|
|
||||||
stats["mean"] += delta / stats["count"]
|
|
||||||
stats["m2"] += delta * (value - stats["mean"])
|
|
||||||
stats["min"] = min(stats["min"], value)
|
|
||||||
stats["max"] = max(stats["max"], value)
|
|
||||||
|
|
||||||
if len(palette) < palette_limit:
|
|
||||||
if image.channels == 1:
|
|
||||||
palette.add((raw[idx],))
|
|
||||||
else:
|
|
||||||
palette.add(tuple(raw[idx : idx + image.channels]))
|
|
||||||
|
|
||||||
means = tuple(stat["mean"] for stat in channel_stats)
|
|
||||||
variances = tuple(stat["m2"] / max(stat["count"], 1) for stat in channel_stats)
|
|
||||||
palette_size = len(palette) if len(palette) < palette_limit else None
|
|
||||||
primary_color = _dominant_color_label(means)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"means": means,
|
|
||||||
"variances": variances,
|
|
||||||
"min": tuple(stat["min"] for stat in channel_stats),
|
|
||||||
"max": tuple(stat["max"] for stat in channel_stats),
|
|
||||||
"palette_size": palette_size,
|
|
||||||
"primary_color": primary_color,
|
|
||||||
"orientation": _orientation_tag(image.width, image.height),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def semantic_name(image: ImageObject, mime: str, analysis: Optional[Dict[str, object]]) -> str:
|
|
||||||
"""Generate a more meaningful file name based on image analysis."""
|
|
||||||
|
|
||||||
extension = "png" if mime == "image/png" else "jpg"
|
|
||||||
base_parts = []
|
|
||||||
|
|
||||||
if analysis:
|
|
||||||
palette_size = analysis.get("palette_size")
|
|
||||||
variances: Tuple[float, ...] = analysis.get("variances", ()) # type: ignore[assignment]
|
|
||||||
variance_score = sum(variances) / max(len(variances), 1)
|
|
||||||
primary_color = analysis.get("primary_color") or "unknown"
|
|
||||||
base_parts.append(primary_color)
|
|
||||||
|
|
||||||
if palette_size == 1:
|
|
||||||
base_parts.append("solid")
|
|
||||||
elif palette_size and palette_size <= 4:
|
|
||||||
base_parts.append("two-tone")
|
|
||||||
elif variance_score < 400:
|
|
||||||
base_parts.append("low-contrast")
|
|
||||||
else:
|
|
||||||
base_parts.append("detailed")
|
|
||||||
|
|
||||||
base_parts.append(str(analysis.get("orientation", "unknown")))
|
|
||||||
else:
|
|
||||||
base_parts.extend(["jpeg", _orientation_tag(image.width, image.height)])
|
|
||||||
|
|
||||||
base_parts.append(f"{image.width}x{image.height}")
|
|
||||||
base_parts.append(f"obj{image.object_number}")
|
|
||||||
return "-".join(base_parts) + f".{extension}"
|
|
||||||
|
|
||||||
|
|
||||||
def raw_to_png(image: ImageObject) -> tuple[bytes, Dict[str, object]]:
|
|
||||||
"""Convert a Flate-encoded image stream to PNG bytes and analysis."""
|
|
||||||
|
|
||||||
if image.bits_per_component != 8:
|
|
||||||
raise ValueError(f"Unsupported bit depth: {image.bits_per_component}")
|
|
||||||
|
|
||||||
analysis = analyse_flate_image(image)
|
|
||||||
|
|
||||||
raw = zlib.decompress(image.stream)
|
|
||||||
row_stride = image.width * image.channels
|
|
||||||
filtered = b"".join(
|
|
||||||
b"\x00" + raw[i : i + row_stride] for i in range(0, len(raw), row_stride)
|
|
||||||
)
|
|
||||||
|
|
||||||
color_type = 2 if image.channels == 3 else 0
|
|
||||||
ihdr = struct.pack(
|
|
||||||
">IIBBBBB", image.width, image.height, 8, color_type, 0, 0, 0
|
|
||||||
)
|
|
||||||
png = b"\x89PNG\r\n\x1a\n"
|
|
||||||
png += _png_chunk(b"IHDR", ihdr)
|
|
||||||
png += _png_chunk(b"IDAT", zlib.compress(filtered))
|
|
||||||
png += _png_chunk(b"IEND", b"")
|
|
||||||
return png, analysis
|
|
||||||
|
|
||||||
|
|
||||||
def save_images(images: List[ImageObject]) -> None:
|
|
||||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
manifest: List[dict[str, object]] = []
|
|
||||||
errors: List[str] = []
|
|
||||||
|
|
||||||
for index, image in enumerate(sorted(images, key=lambda im: im.object_number), start=1):
|
|
||||||
analysis: Optional[Dict[str, object]] = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
if image.filter == "/FlateDecode":
|
|
||||||
raw_bytes, analysis = raw_to_png(image)
|
|
||||||
mime = "image/png"
|
|
||||||
elif image.filter == "/DCTDecode":
|
|
||||||
raw_bytes = image.stream
|
|
||||||
mime = "image/jpeg"
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported filter {image.filter}")
|
|
||||||
except Exception as exc: # noqa: BLE001 - surface helpful error context
|
|
||||||
placeholder = f"obj{image.object_number}"
|
|
||||||
errors.append(f"{placeholder}: {exc}")
|
|
||||||
print(f"Skipping {placeholder}: {exc}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
name = semantic_name(image, mime, analysis)
|
|
||||||
encoded = base64.b64encode(raw_bytes).decode("ascii")
|
|
||||||
manifest.append(
|
|
||||||
{
|
|
||||||
"name": name,
|
|
||||||
"object_number": image.object_number,
|
|
||||||
"width": image.width,
|
|
||||||
"height": image.height,
|
|
||||||
"color_space": image.color_space,
|
|
||||||
"bits_per_component": image.bits_per_component,
|
|
||||||
"mime": mime,
|
|
||||||
"base64": encoded,
|
|
||||||
"analysis": analysis,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
print(f"Captured {name} ({image.width}x{image.height}, {mime})")
|
|
||||||
|
|
||||||
images_path = OUTPUT_DIR / "images.json"
|
|
||||||
images_path.write_text(json.dumps(manifest, indent=2))
|
|
||||||
if errors:
|
|
||||||
(OUTPUT_DIR / "errors.txt").write_text("\n".join(errors))
|
|
||||||
print(f"Encountered errors for {len(errors)} image(s); see errors.txt")
|
|
||||||
print(f"Wrote JSON manifest to {images_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
pdf_bytes = PDF_PATH.read_bytes()
|
|
||||||
images = list(iter_image_objects(pdf_bytes))
|
|
||||||
save_images(images)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# Install Python dependencies system-wide or in the active environment.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
PYTHON_BIN=${PYTHON_BIN:-python3}
|
|
||||||
|
|
||||||
"$PYTHON_BIN" -m pip install --upgrade pip
|
|
||||||
"$PYTHON_BIN" -m pip install -r requirements.txt
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Extract text from page images using OCR and save as a markdown document.
|
|
||||||
|
|
||||||
Dependencies:
|
|
||||||
pip install pillow pytesseract
|
|
||||||
|
|
||||||
System requirements:
|
|
||||||
tesseract-ocr (install via: sudo apt-get install tesseract-ocr)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import pytesseract
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
def ocr_pages(pages_dir: Path, output_file: Path) -> None:
|
|
||||||
"""Perform OCR on all page images and create a single document."""
|
|
||||||
|
|
||||||
pages_dir = pages_dir.resolve()
|
|
||||||
if not pages_dir.exists():
|
|
||||||
raise FileNotFoundError(f"Pages directory not found: {pages_dir}")
|
|
||||||
|
|
||||||
# Get all PNG files sorted by number
|
|
||||||
def get_page_number(path: Path) -> int:
|
|
||||||
"""Extract page number from filename, defaulting to 0 if not found."""
|
|
||||||
try:
|
|
||||||
return int(path.stem.split("_")[-1])
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
image_files = sorted(pages_dir.glob("*.png"), key=get_page_number)
|
|
||||||
|
|
||||||
if not image_files:
|
|
||||||
raise ValueError(f"No PNG files found in {pages_dir}")
|
|
||||||
|
|
||||||
print(f"Found {len(image_files)} page images to process...")
|
|
||||||
|
|
||||||
full_text = []
|
|
||||||
full_text.append("# WizardMerge Research Paper\n")
|
|
||||||
full_text.append("*Extracted via OCR from paper pages*\n\n")
|
|
||||||
full_text.append("---\n\n")
|
|
||||||
|
|
||||||
for idx, image_file in enumerate(image_files, start=1):
|
|
||||||
print(f"Processing page {idx}/{len(image_files)}: {image_file.name}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Open image and perform OCR
|
|
||||||
img = Image.open(image_file)
|
|
||||||
text = pytesseract.image_to_string(img)
|
|
||||||
|
|
||||||
# Add page separator and text
|
|
||||||
full_text.append(f"## Page {idx}\n\n")
|
|
||||||
full_text.append(text.strip())
|
|
||||||
full_text.append("\n\n---\n\n")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Error processing {image_file.name}: {e}")
|
|
||||||
full_text.append(f"## Page {idx}\n\n")
|
|
||||||
full_text.append(f"*[OCR Error: {e}]*\n\n")
|
|
||||||
full_text.append("---\n\n")
|
|
||||||
|
|
||||||
# Write output
|
|
||||||
output_file.write_text("".join(full_text))
|
|
||||||
print(f"\nOCR complete! Output written to: {output_file}")
|
|
||||||
print(f"Total pages processed: {len(image_files)}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
pages_dir = Path(__file__).parent.parent / "docs" / "pages"
|
|
||||||
output_file = Path(__file__).parent.parent / "docs" / "PAPER.md"
|
|
||||||
|
|
||||||
ocr_pages(pages_dir, output_file)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# Launch the WizardMerge GUI using the local virtual environment when present.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
VENV_DIR=${VENV_DIR:-.venv}
|
|
||||||
PYTHON_BIN=${PYTHON_BIN:-python3}
|
|
||||||
|
|
||||||
if [ -d "$VENV_DIR" ]; then
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "$VENV_DIR/bin/activate"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$PYTHON_BIN" -m wizardmerge.app "$@"
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# Prepare a local virtual environment and install dependencies.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
VENV_DIR=${VENV_DIR:-.venv}
|
|
||||||
PYTHON_BIN=${PYTHON_BIN:-python3}
|
|
||||||
|
|
||||||
if [ ! -d "$VENV_DIR" ]; then
|
|
||||||
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "$VENV_DIR/bin/activate"
|
|
||||||
|
|
||||||
pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
echo "Environment ready. Activate with: . $VENV_DIR/bin/activate"
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Unified TLC helper: replaces bootstrap.{sh,ps1} and run-tlc.{sh,ps1}.
|
|
||||||
|
|
||||||
Subcommands:
|
|
||||||
- bootstrap : download tla2tools.jar into tools/ (or custom dir)
|
|
||||||
- run : ensure jar exists, then run TLC and tee output to log
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Iterable, List
|
|
||||||
from urllib.request import urlopen
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_VERSION = "1.8.0"
|
|
||||||
DEFAULT_TOOLS_DIR = "tools"
|
|
||||||
DEFAULT_RESULTS_DIR = "ci-results"
|
|
||||||
DEFAULT_MODULE = "STLRepairAlgo"
|
|
||||||
DEFAULT_CONFIG = "models/STLRepairAlgo.cfg"
|
|
||||||
DEFAULT_SPEC_DIR = "spec"
|
|
||||||
DEFAULT_JAVA = "java"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="TLA+ TLC helper (bootstrap + run) in one script."
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--tools-dir",
|
|
||||||
default=DEFAULT_TOOLS_DIR,
|
|
||||||
help=f"Directory for tla2tools.jar (default: {DEFAULT_TOOLS_DIR!r}).",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--version",
|
|
||||||
default=DEFAULT_VERSION,
|
|
||||||
help=f"TLA+ tools version tag (default: {DEFAULT_VERSION!r}).",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--url-template",
|
|
||||||
default=(
|
|
||||||
"https://github.com/tlaplus/tlaplus/releases/"
|
|
||||||
"download/v{version}/tla2tools.jar"
|
|
||||||
),
|
|
||||||
help="Template URL for tla2tools.jar; {version} will be substituted.",
|
|
||||||
)
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(
|
|
||||||
dest="command",
|
|
||||||
required=True,
|
|
||||||
help="Subcommands.",
|
|
||||||
)
|
|
||||||
|
|
||||||
bootstrap = subparsers.add_parser(
|
|
||||||
"bootstrap",
|
|
||||||
help="Download tla2tools.jar into tools-dir if missing.",
|
|
||||||
)
|
|
||||||
|
|
||||||
bootstrap.add_argument(
|
|
||||||
"--force",
|
|
||||||
action="store_true",
|
|
||||||
help="Re-download even if tla2tools.jar already exists.",
|
|
||||||
)
|
|
||||||
|
|
||||||
run_p = subparsers.add_parser(
|
|
||||||
"run",
|
|
||||||
help="Run TLC on a TLA+ module, teeing output to a log file.",
|
|
||||||
)
|
|
||||||
|
|
||||||
run_p.add_argument(
|
|
||||||
"module",
|
|
||||||
nargs="?",
|
|
||||||
default=DEFAULT_MODULE,
|
|
||||||
help=f"TLA+ module name without .tla (default: {DEFAULT_MODULE!r}).",
|
|
||||||
)
|
|
||||||
|
|
||||||
run_p.add_argument(
|
|
||||||
"-c",
|
|
||||||
"--config",
|
|
||||||
default=DEFAULT_CONFIG,
|
|
||||||
help=f"Path to TLC config file (default: {DEFAULT_CONFIG!r}).",
|
|
||||||
)
|
|
||||||
|
|
||||||
run_p.add_argument(
|
|
||||||
"--spec-dir",
|
|
||||||
default=DEFAULT_SPEC_DIR,
|
|
||||||
help=f"Directory containing .tla specs (default: {DEFAULT_SPEC_DIR!r}).",
|
|
||||||
)
|
|
||||||
|
|
||||||
run_p.add_argument(
|
|
||||||
"--results-dir",
|
|
||||||
default=DEFAULT_RESULTS_DIR,
|
|
||||||
help=f"Directory for TLC log files (default: {DEFAULT_RESULTS_DIR!r}).",
|
|
||||||
)
|
|
||||||
|
|
||||||
run_p.add_argument(
|
|
||||||
"--java",
|
|
||||||
default=DEFAULT_JAVA,
|
|
||||||
help=f"Java executable (default: {DEFAULT_JAVA!r}).",
|
|
||||||
)
|
|
||||||
|
|
||||||
run_p.add_argument(
|
|
||||||
"--extra-java-arg",
|
|
||||||
action="append",
|
|
||||||
default=[],
|
|
||||||
metavar="ARG",
|
|
||||||
help="Extra argument to pass to Java (can be repeated).",
|
|
||||||
)
|
|
||||||
|
|
||||||
run_p.add_argument(
|
|
||||||
"--no-bootstrap",
|
|
||||||
action="store_true",
|
|
||||||
help="Skip automatic bootstrap before running TLC.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_dir(path: Path) -> None:
|
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def build_jar_url(version: str, url_template: str) -> str:
|
|
||||||
return url_template.format(version=version)
|
|
||||||
|
|
||||||
|
|
||||||
def download_tla_tools(url: str, target: Path, overwrite: bool = False) -> None:
|
|
||||||
if target.exists() and not overwrite:
|
|
||||||
print(f"tla2tools.jar already present at {target}.")
|
|
||||||
return
|
|
||||||
|
|
||||||
ensure_dir(target.parent)
|
|
||||||
tmp = target.with_suffix(target.suffix + ".tmp")
|
|
||||||
|
|
||||||
print(f"Downloading tla2tools.jar from {url} ...")
|
|
||||||
try:
|
|
||||||
with urlopen(url) as resp, tmp.open("wb") as f:
|
|
||||||
chunk = resp.read(8192)
|
|
||||||
while chunk:
|
|
||||||
f.write(chunk)
|
|
||||||
chunk = resp.read(8192)
|
|
||||||
except Exception as exc:
|
|
||||||
if tmp.exists():
|
|
||||||
tmp.unlink()
|
|
||||||
raise SystemExit(f"Failed to download tla2tools.jar: {exc}") from exc
|
|
||||||
|
|
||||||
tmp.replace(target)
|
|
||||||
target.chmod(0o644)
|
|
||||||
print(f"Saved tla2tools.jar to {target}.")
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_java_available(java_exe: str) -> None:
|
|
||||||
if shutil.which(java_exe) is None:
|
|
||||||
raise SystemExit(
|
|
||||||
f"Java executable {java_exe!r} not found in PATH. "
|
|
||||||
"Install Java or pass --java with a full path."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def tee_process_output(
|
|
||||||
proc: subprocess.Popen,
|
|
||||||
log_path: Path,
|
|
||||||
) -> int:
|
|
||||||
ensure_dir(log_path.parent)
|
|
||||||
with log_path.open("w", encoding="utf-8", errors="replace") as log:
|
|
||||||
assert proc.stdout is not None
|
|
||||||
for line in proc.stdout:
|
|
||||||
sys.stdout.write(line)
|
|
||||||
sys.stdout.flush()
|
|
||||||
log.write(line)
|
|
||||||
log.flush()
|
|
||||||
return proc.wait()
|
|
||||||
|
|
||||||
|
|
||||||
def build_tlc_command(
|
|
||||||
java_exe: str,
|
|
||||||
extra_java_args: Iterable[str],
|
|
||||||
jar_path: Path,
|
|
||||||
cfg_path: Path,
|
|
||||||
module_path: Path,
|
|
||||||
) -> List[str]:
|
|
||||||
cmd: List[str] = [java_exe]
|
|
||||||
cmd.extend(extra_java_args)
|
|
||||||
cmd.extend(
|
|
||||||
[
|
|
||||||
"-cp",
|
|
||||||
str(jar_path),
|
|
||||||
"tlc2.TLC",
|
|
||||||
"-config",
|
|
||||||
str(cfg_path),
|
|
||||||
str(module_path),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
|
|
||||||
def run_tlc(
|
|
||||||
java_exe: str,
|
|
||||||
extra_java_args: Iterable[str],
|
|
||||||
tools_dir: Path,
|
|
||||||
spec_dir: Path,
|
|
||||||
module: str,
|
|
||||||
cfg: Path,
|
|
||||||
results_dir: Path,
|
|
||||||
) -> int:
|
|
||||||
ensure_java_available(java_exe)
|
|
||||||
|
|
||||||
jar_path = tools_dir / "tla2tools.jar"
|
|
||||||
if not jar_path.exists():
|
|
||||||
raise SystemExit(
|
|
||||||
f"{jar_path} does not exist. Run with 'bootstrap' first "
|
|
||||||
"or omit --no-bootstrap on the 'run' command."
|
|
||||||
)
|
|
||||||
|
|
||||||
module_path = spec_dir / f"{module}.tla"
|
|
||||||
if not module_path.exists():
|
|
||||||
raise SystemExit(f"Spec file not found: {module_path}")
|
|
||||||
|
|
||||||
cfg_path = cfg
|
|
||||||
if not cfg_path.exists():
|
|
||||||
raise SystemExit(f"Config file not found: {cfg_path}")
|
|
||||||
|
|
||||||
ensure_dir(results_dir)
|
|
||||||
log_path = results_dir / f"{module}.tlc.log"
|
|
||||||
|
|
||||||
cmd = build_tlc_command(
|
|
||||||
java_exe=java_exe,
|
|
||||||
extra_java_args=list(extra_java_args),
|
|
||||||
jar_path=jar_path,
|
|
||||||
cfg_path=cfg_path,
|
|
||||||
module_path=module_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
print("Running TLC with command:")
|
|
||||||
print(" " + " ".join(str(c) for c in cmd))
|
|
||||||
print(f"Logging output to {log_path}")
|
|
||||||
|
|
||||||
proc = subprocess.Popen(
|
|
||||||
cmd,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
text=True,
|
|
||||||
bufsize=1,
|
|
||||||
universal_newlines=True,
|
|
||||||
)
|
|
||||||
return tee_process_output(proc, log_path)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
tools_dir = Path(args.tools_dir)
|
|
||||||
url = build_jar_url(args.version, args.url_template)
|
|
||||||
jar_target = tools_dir / "tla2tools.jar"
|
|
||||||
|
|
||||||
if args.command == "bootstrap":
|
|
||||||
download_tla_tools(url, jar_target, overwrite=args.force)
|
|
||||||
return
|
|
||||||
|
|
||||||
if args.command == "run":
|
|
||||||
if not args.no_bootstrap:
|
|
||||||
download_tla_tools(url, jar_target, overwrite=False)
|
|
||||||
|
|
||||||
exit_code = run_tlc(
|
|
||||||
java_exe=args.java,
|
|
||||||
extra_java_args=args.extra_java_arg,
|
|
||||||
tools_dir=tools_dir,
|
|
||||||
spec_dir=Path(args.spec_dir),
|
|
||||||
module=args.module,
|
|
||||||
cfg=Path(args.config),
|
|
||||||
results_dir=Path(args.results_dir),
|
|
||||||
)
|
|
||||||
raise SystemExit(exit_code)
|
|
||||||
|
|
||||||
raise SystemExit("No command given; use --help for usage.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""WizardMerge package entry point and metadata."""
|
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"""Algorithmic utilities for WizardMerge."""
|
|
||||||
|
|
||||||
from .merge import MergeResult, merge_pairs
|
|
||||||
|
|
||||||
__all__ = ["MergeResult", "merge_pairs"]
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
"""Toy merge utilities to accompany the GUI."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Iterable, List, Sequence
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MergeResult:
|
|
||||||
"""Hold the combined payload and an audit trail of sources."""
|
|
||||||
|
|
||||||
merged: str
|
|
||||||
sources: List[str]
|
|
||||||
|
|
||||||
|
|
||||||
def merge_pairs(lines_a: Sequence[str], lines_b: Sequence[str]) -> MergeResult:
|
|
||||||
"""Return interleaved lines and capture their origin.
|
|
||||||
|
|
||||||
This function is intentionally simple, providing a deterministic merge
|
|
||||||
strategy useful for demonstration in the GUI layer.
|
|
||||||
"""
|
|
||||||
|
|
||||||
merged_lines: List[str] = []
|
|
||||||
sources: List[str] = []
|
|
||||||
|
|
||||||
for index, (line_a, line_b) in enumerate(zip(lines_a, lines_b)):
|
|
||||||
merged_lines.append(line_a)
|
|
||||||
merged_lines.append(line_b)
|
|
||||||
sources.append(f"A{index}")
|
|
||||||
sources.append(f"B{index}")
|
|
||||||
|
|
||||||
if len(lines_a) > len(lines_b):
|
|
||||||
for tail_index, line in enumerate(lines_a[len(lines_b) :], start=len(lines_b)):
|
|
||||||
merged_lines.append(line)
|
|
||||||
sources.append(f"A{tail_index}")
|
|
||||||
elif len(lines_b) > len(lines_a):
|
|
||||||
for tail_index, line in enumerate(lines_b[len(lines_a) :], start=len(lines_a)):
|
|
||||||
merged_lines.append(line)
|
|
||||||
sources.append(f"B{tail_index}")
|
|
||||||
|
|
||||||
return MergeResult(merged="\n".join(merged_lines), sources=sources)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""Application bootstrap for the WizardMerge PyQt6 + QML UI."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from PyQt6.QtCore import QUrl
|
|
||||||
from PyQt6.QtGui import QGuiApplication
|
|
||||||
from PyQt6.QtQml import QQmlApplicationEngine
|
|
||||||
|
|
||||||
from wizardmerge.themes.loader import ThemeManager
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_qml_path() -> Path:
|
|
||||||
"""Return the absolute path to the main QML entry file."""
|
|
||||||
qml_path = Path(__file__).parent / "qml" / "main.qml"
|
|
||||||
if not qml_path.exists():
|
|
||||||
raise FileNotFoundError("Unable to locate main.qml; ensure resources are installed.")
|
|
||||||
return qml_path
|
|
||||||
|
|
||||||
|
|
||||||
def run(preferred_theme: Optional[str] = None) -> int:
|
|
||||||
"""Run the WizardMerge UI.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
preferred_theme: Optional theme name to prioritize when loading themes.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Exit code to propagate to the caller.
|
|
||||||
"""
|
|
||||||
app = QGuiApplication(sys.argv)
|
|
||||||
theme_manager = ThemeManager()
|
|
||||||
theme = theme_manager.select_theme(preferred_theme)
|
|
||||||
|
|
||||||
engine = QQmlApplicationEngine()
|
|
||||||
engine.rootContext().setContextProperty("theme", theme.as_dict())
|
|
||||||
|
|
||||||
qml_path = _resolve_qml_path()
|
|
||||||
engine.load(QUrl.fromLocalFile(qml_path.as_posix()))
|
|
||||||
|
|
||||||
if not engine.rootObjects():
|
|
||||||
return 1
|
|
||||||
|
|
||||||
return app.exec()
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Entry-point wrapper for console scripts."""
|
|
||||||
sys.exit(run())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
|
|
||||||
ApplicationWindow {
|
|
||||||
width: 720
|
|
||||||
height: 480
|
|
||||||
visible: true
|
|
||||||
title: "WizardMerge"
|
|
||||||
color: theme.background
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 12
|
|
||||||
padding: 16
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 64
|
|
||||||
color: theme.surface
|
|
||||||
radius: 8
|
|
||||||
border.color: theme.border
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: 18
|
|
||||||
color: theme.accent
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 4
|
|
||||||
Text {
|
|
||||||
text: "WizardMerge"
|
|
||||||
font.bold: true
|
|
||||||
color: theme.text
|
|
||||||
font.pointSize: 18
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: "PyQt6 + QML theming demo"
|
|
||||||
color: theme.text
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 1
|
|
||||||
height: 40
|
|
||||||
color: theme.border
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: `Current theme: ${theme.name}`
|
|
||||||
color: theme.text
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 320
|
|
||||||
radius: 8
|
|
||||||
color: theme.surface
|
|
||||||
border.color: theme.border
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 16
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Algorithm preview"
|
|
||||||
font.bold: true
|
|
||||||
color: theme.text
|
|
||||||
font.pointSize: 14
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: 1
|
|
||||||
width: parent.width
|
|
||||||
color: theme.border
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Drop your merge data here. The algorithm preview uses a simple interleaving strategy from wizardmerge.algo.merge.merge_pairs."
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
color: theme.text
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 180
|
|
||||||
color: theme.background
|
|
||||||
radius: 6
|
|
||||||
border.color: theme.border
|
|
||||||
border.width: 1
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Future input widgets will live here."
|
|
||||||
color: theme.text
|
|
||||||
opacity: 0.6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""Theme helpers for WizardMerge."""
|
|
||||||
|
|
||||||
from .base import Theme
|
|
||||||
from .loader import ThemeManager
|
|
||||||
|
|
||||||
__all__ = ["Theme", "ThemeManager"]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
"""Core theme definitions."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Theme:
|
|
||||||
"""Simple theme container for color palette values."""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
palette: Dict[str, str]
|
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, str]:
|
|
||||||
"""Return a dictionary representation usable by QML contexts."""
|
|
||||||
return {"name": self.name, **self.palette}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
"""Built-in dark theme."""
|
|
||||||
from wizardmerge.themes.base import Theme
|
|
||||||
|
|
||||||
|
|
||||||
palette = {
|
|
||||||
"background": "#0d1117",
|
|
||||||
"surface": "#161b22",
|
|
||||||
"text": "#e6edf3",
|
|
||||||
"accent": "#7c9aff",
|
|
||||||
"border": "#30363d",
|
|
||||||
}
|
|
||||||
|
|
||||||
theme = Theme(name="Dark", palette=palette)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
"""Built-in light theme."""
|
|
||||||
from wizardmerge.themes.base import Theme
|
|
||||||
|
|
||||||
|
|
||||||
palette = {
|
|
||||||
"background": "#f5f5f5",
|
|
||||||
"surface": "#ffffff",
|
|
||||||
"text": "#1f2933",
|
|
||||||
"accent": "#0f7ada",
|
|
||||||
"border": "#d8d8d8",
|
|
||||||
}
|
|
||||||
|
|
||||||
theme = Theme(name="Light", palette=palette)
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
"""Theme loading and plugin discovery helpers."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from pkgutil import iter_modules
|
|
||||||
from typing import Iterable, List, Sequence
|
|
||||||
|
|
||||||
from wizardmerge.themes.base import Theme
|
|
||||||
|
|
||||||
|
|
||||||
class ThemeManager:
|
|
||||||
"""Manage built-in and plugin-based themes."""
|
|
||||||
|
|
||||||
def __init__(self, extra_plugin_paths: Sequence[Path] | None = None) -> None:
|
|
||||||
self._builtin_modules = self._discover_builtin_modules()
|
|
||||||
self._plugin_modules = self._discover_plugin_modules(extra_plugin_paths)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _discover_builtin_modules() -> List[str]:
|
|
||||||
"""Return module names for bundled themes."""
|
|
||||||
module_path = Path(__file__).parent
|
|
||||||
modules = []
|
|
||||||
for module in iter_modules([str(module_path)]):
|
|
||||||
if module.name.endswith("_theme"):
|
|
||||||
modules.append(f"{__package__}.{module.name}")
|
|
||||||
return modules
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _discover_plugin_modules(extra_paths: Sequence[Path] | None) -> List[str]:
|
|
||||||
"""Return module names for shipped plugin examples and user-defined themes."""
|
|
||||||
modules: List[str] = []
|
|
||||||
plugin_package = f"{__package__}.plugins"
|
|
||||||
plugin_path = Path(__file__).parent / "plugins"
|
|
||||||
modules.extend(
|
|
||||||
f"{plugin_package}.{module.name}" for module in iter_modules([str(plugin_path)]) if module.ispkg is False
|
|
||||||
)
|
|
||||||
|
|
||||||
if extra_paths:
|
|
||||||
for path in extra_paths:
|
|
||||||
if not path.exists():
|
|
||||||
continue
|
|
||||||
sys.path.append(str(path))
|
|
||||||
modules.extend(module.name for module in iter_modules([str(path)]))
|
|
||||||
|
|
||||||
return modules
|
|
||||||
|
|
||||||
def _load_theme_from_module(self, module_name: str) -> Theme | None:
|
|
||||||
module = importlib.import_module(module_name)
|
|
||||||
theme = getattr(module, "theme", None) or getattr(module, "warm_theme", None)
|
|
||||||
if isinstance(theme, Theme):
|
|
||||||
return theme
|
|
||||||
return None
|
|
||||||
|
|
||||||
def available_themes(self) -> List[Theme]:
|
|
||||||
"""Return a list of all themes that could be loaded."""
|
|
||||||
themes: List[Theme] = []
|
|
||||||
for module in [*self._builtin_modules, *self._plugin_modules]:
|
|
||||||
theme = self._load_theme_from_module(module)
|
|
||||||
if theme:
|
|
||||||
themes.append(theme)
|
|
||||||
return themes
|
|
||||||
|
|
||||||
def select_theme(self, preferred_name: str | None = None) -> Theme:
|
|
||||||
"""Return the preferred theme or fall back to the first available one."""
|
|
||||||
themes = self.available_themes()
|
|
||||||
if not themes:
|
|
||||||
raise RuntimeError("No themes could be loaded.")
|
|
||||||
|
|
||||||
if preferred_name:
|
|
||||||
for theme in themes:
|
|
||||||
if theme.name.lower() == preferred_name.lower():
|
|
||||||
return theme
|
|
||||||
|
|
||||||
return themes[0]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""Example plugin themes shipped with the project."""
|
|
||||||
from wizardmerge.themes.base import Theme
|
|
||||||
|
|
||||||
from .warm_theme import warm_theme
|
|
||||||
|
|
||||||
__all__ = ["warm_theme", "Theme"]
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
"""Sample theme plugin distributed separately from the built-ins."""
|
|
||||||
from wizardmerge.themes.base import Theme
|
|
||||||
|
|
||||||
|
|
||||||
palette = {
|
|
||||||
"background": "#fdf1e5",
|
|
||||||
"surface": "#fde7d3",
|
|
||||||
"text": "#3b2f2f",
|
|
||||||
"accent": "#d67b4d",
|
|
||||||
"border": "#f6c4a3",
|
|
||||||
}
|
|
||||||
|
|
||||||
warm_theme = Theme(name="Warm", palette=palette)
|
|
||||||
Reference in New Issue
Block a user