summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-17 21:33:40 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-17 21:57:13 +0800
commitad0943f88ba9f5ed6eae198ecb4835c0f44701de (patch)
tree6a8eb1fee42e7ee173e74788dafeb27c52429683
chore: initialize project structure and add core modules
-rw-r--r--.cargo/config.toml7
-rw-r--r--.gitignore5
-rw-r--r--Cargo.lock87
-rw-r--r--Cargo.toml26
-rw-r--r--LICENSE21
-rw-r--r--rola-bucket/Cargo.toml8
-rw-r--r--rola-bucket/src/lib.rs1
-rw-r--r--rola-cli/Cargo.toml10
-rw-r--r--rola-cli/src/bin/rola.rs1
-rw-r--r--rola-cli/src/lib.rs1
-rw-r--r--rola-desktop.sln18
-rw-r--r--rola-desktop/App.axaml15
-rw-r--r--rola-desktop/App.axaml.cs47
-rw-r--r--rola-desktop/Assets/avalonia-logo.icobin0 -> 175875 bytes
-rw-r--r--rola-desktop/Program.cs21
-rw-r--r--rola-desktop/ViewLocator.cs31
-rw-r--r--rola-desktop/ViewModels/MainWindowViewModel.cs6
-rw-r--r--rola-desktop/ViewModels/ViewModelBase.cs7
-rw-r--r--rola-desktop/Views/MainWindow.axaml20
-rw-r--r--rola-desktop/Views/MainWindow.axaml.cs11
-rw-r--r--rola-desktop/app.manifest18
-rw-r--r--rola-desktop/rola-desktop.csproj32
-rw-r--r--rola-devtools/Cargo.lock7
-rw-r--r--rola-devtools/Cargo.toml6
-rw-r--r--rola-devtools/scripts/check-all.py38
-rw-r--r--rola-devtools/scripts/run-desktop.py11
-rw-r--r--rola-devtools/scripts/windows-folder-hide.ps143
-rw-r--r--rola-devtools/src/bin/welcome.rs3
-rw-r--r--rola-devtools/src/lib.rs1
-rw-r--r--rola-draft/Cargo.toml8
-rw-r--r--rola-draft/src/lib.rs1
-rw-r--r--rola-utils/functions/Cargo.toml8
-rw-r--r--rola-utils/functions/src/levenshtein_distance.rs117
-rw-r--r--rola-utils/functions/src/lib.rs2
-rw-r--r--rola-utils/macros/Cargo.toml14
-rw-r--r--rola-utils/macros/src/lib.rs1
-rw-r--r--rola-vcs/Cargo.toml13
-rw-r--r--rola-vcs/internal_macros/Cargo.toml14
-rw-r--r--rola-vcs/internal_macros/src/constants.rs115
-rw-r--r--rola-vcs/internal_macros/src/lib.rs39
-rw-r--r--rola-vcs/src/consts/common.rs7
-rw-r--r--rola-vcs/src/consts/mod.rs2
-rw-r--r--rola-vcs/src/lib.rs11
-rw-r--r--run-tools.ps160
-rw-r--r--run-tools.sh64
45 files changed, 978 insertions, 0 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..11eebcf
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,7 @@
+[build]
+target-dir = "./.temp/target"
+
+[env]
+
+[alias]
+rola-doc = "doc --workspace --no-deps --open"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e2ecd8d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.idea
+.temp
+rola-desktop/obj
+rola-desktop/bin
+rola-devtools/scripts/last_check
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..edb6412
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,87 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rola-bucket"
+version = "0.1.0"
+
+[[package]]
+name = "rola-cli"
+version = "0.1.0"
+dependencies = [
+ "shared_functions",
+ "shared_macros",
+]
+
+[[package]]
+name = "rola-draft"
+version = "0.1.0"
+
+[[package]]
+name = "rorolala"
+version = "0.1.0"
+dependencies = [
+ "rola-bucket",
+ "rola-draft",
+ "rorolala_internal_macros",
+ "shared_functions",
+ "shared_macros",
+]
+
+[[package]]
+name = "rorolala_internal_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "shared_functions"
+version = "0.1.0"
+
+[[package]]
+name = "shared_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..7f14aac
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,26 @@
+[workspace]
+resolver = "2"
+members = [
+ "rola-bucket",
+ "rola-cli",
+ "rola-vcs/internal_macros",
+ "rola-utils/functions",
+ "rola-utils/macros",
+ "rola-draft",
+ "rola-vcs",
+]
+exclude = ["rola-devtools"]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2024"
+authors = ["RoroLalaVCS", "Weicao-CatilGrass <catil_grass@qq.com>"]
+license = "MIT"
+
+[workspace.dependencies]
+rorolala = { path = "rola-vcs" }
+rorolala_internal_macros = { path = "rola-vcs/internal_macros" }
+shared_functions = { path = "rola-utils/functions" }
+shared_macros = { path = "rola-utils/macros" }
+rola-bucket = { path = "rola-bucket" }
+rola-draft = { path = "rola-draft" }
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..96889a1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 RoroLala VCS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/rola-bucket/Cargo.toml b/rola-bucket/Cargo.toml
new file mode 100644
index 0000000..85d7fab
--- /dev/null
+++ b/rola-bucket/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "rola-bucket"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
diff --git a/rola-bucket/src/lib.rs b/rola-bucket/src/lib.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/rola-bucket/src/lib.rs
@@ -0,0 +1 @@
+
diff --git a/rola-cli/Cargo.toml b/rola-cli/Cargo.toml
new file mode 100644
index 0000000..9ee6816
--- /dev/null
+++ b/rola-cli/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "rola-cli"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+shared_functions.workspace = true
+shared_macros.workspace = true
diff --git a/rola-cli/src/bin/rola.rs b/rola-cli/src/bin/rola.rs
new file mode 100644
index 0000000..f328e4d
--- /dev/null
+++ b/rola-cli/src/bin/rola.rs
@@ -0,0 +1 @@
+fn main() {}
diff --git a/rola-cli/src/lib.rs b/rola-cli/src/lib.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/rola-cli/src/lib.rs
@@ -0,0 +1 @@
+
diff --git a/rola-desktop.sln b/rola-desktop.sln
new file mode 100644
index 0000000..8864f46
--- /dev/null
+++ b/rola-desktop.sln
@@ -0,0 +1,18 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rola-desktop", "rola-desktop\rola-desktop.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/rola-desktop/App.axaml b/rola-desktop/App.axaml
new file mode 100644
index 0000000..bd879fe
--- /dev/null
+++ b/rola-desktop/App.axaml
@@ -0,0 +1,15 @@
+<Application xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ x:Class="rola_desktop.App"
+ xmlns:local="using:rola_desktop"
+ RequestedThemeVariant="Default">
+ <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
+
+ <Application.DataTemplates>
+ <local:ViewLocator/>
+ </Application.DataTemplates>
+
+ <Application.Styles>
+ <FluentTheme />
+ </Application.Styles>
+</Application> \ No newline at end of file
diff --git a/rola-desktop/App.axaml.cs b/rola-desktop/App.axaml.cs
new file mode 100644
index 0000000..6b665bc
--- /dev/null
+++ b/rola-desktop/App.axaml.cs
@@ -0,0 +1,47 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Data.Core;
+using Avalonia.Data.Core.Plugins;
+using System.Linq;
+using Avalonia.Markup.Xaml;
+using rola_desktop.ViewModels;
+using rola_desktop.Views;
+
+namespace rola_desktop;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ // Avoid duplicate validations from both Avalonia and the CommunityToolkit.
+ // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
+ DisableAvaloniaDataAnnotationValidation();
+ desktop.MainWindow = new MainWindow
+ {
+ DataContext = new MainWindowViewModel(),
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ private void DisableAvaloniaDataAnnotationValidation()
+ {
+ // Get an array of plugins to remove
+ var dataValidationPluginsToRemove =
+ BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
+
+ // remove each entry found
+ foreach (var plugin in dataValidationPluginsToRemove)
+ {
+ BindingPlugins.DataValidators.Remove(plugin);
+ }
+ }
+} \ No newline at end of file
diff --git a/rola-desktop/Assets/avalonia-logo.ico b/rola-desktop/Assets/avalonia-logo.ico
new file mode 100644
index 0000000..f7da8bb
--- /dev/null
+++ b/rola-desktop/Assets/avalonia-logo.ico
Binary files differ
diff --git a/rola-desktop/Program.cs b/rola-desktop/Program.cs
new file mode 100644
index 0000000..30e9ca6
--- /dev/null
+++ b/rola-desktop/Program.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using System;
+
+namespace rola_desktop;
+
+sealed class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure<App>()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+}
diff --git a/rola-desktop/ViewLocator.cs b/rola-desktop/ViewLocator.cs
new file mode 100644
index 0000000..eec4713
--- /dev/null
+++ b/rola-desktop/ViewLocator.cs
@@ -0,0 +1,31 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using rola_desktop.ViewModels;
+
+namespace rola_desktop;
+
+public class ViewLocator : IDataTemplate
+{
+
+ public Control? Build(object? param)
+ {
+ if (param is null)
+ return null;
+
+ var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
+ var type = Type.GetType(name);
+
+ if (type != null)
+ {
+ return (Control)Activator.CreateInstance(type)!;
+ }
+
+ return new TextBlock { Text = "Not Found: " + name };
+ }
+
+ public bool Match(object? data)
+ {
+ return data is ViewModelBase;
+ }
+}
diff --git a/rola-desktop/ViewModels/MainWindowViewModel.cs b/rola-desktop/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..2db588d
--- /dev/null
+++ b/rola-desktop/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,6 @@
+namespace rola_desktop.ViewModels;
+
+public partial class MainWindowViewModel : ViewModelBase
+{
+ public string Greeting { get; } = "Welcome to Avalonia!";
+}
diff --git a/rola-desktop/ViewModels/ViewModelBase.cs b/rola-desktop/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..f0aa7f1
--- /dev/null
+++ b/rola-desktop/ViewModels/ViewModelBase.cs
@@ -0,0 +1,7 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace rola_desktop.ViewModels;
+
+public class ViewModelBase : ObservableObject
+{
+}
diff --git a/rola-desktop/Views/MainWindow.axaml b/rola-desktop/Views/MainWindow.axaml
new file mode 100644
index 0000000..6ed88af
--- /dev/null
+++ b/rola-desktop/Views/MainWindow.axaml
@@ -0,0 +1,20 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:vm="using:rola_desktop.ViewModels"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="rola_desktop.Views.MainWindow"
+ x:DataType="vm:MainWindowViewModel"
+ Icon="/Assets/avalonia-logo.ico"
+ Title="rola_desktop">
+
+ <Design.DataContext>
+ <!-- This only sets the DataContext for the previewer in an IDE,
+ to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
+ <vm:MainWindowViewModel/>
+ </Design.DataContext>
+
+ <TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
+
+</Window>
diff --git a/rola-desktop/Views/MainWindow.axaml.cs b/rola-desktop/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..14f9599
--- /dev/null
+++ b/rola-desktop/Views/MainWindow.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace rola_desktop.Views;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+} \ No newline at end of file
diff --git a/rola-desktop/app.manifest b/rola-desktop/app.manifest
new file mode 100644
index 0000000..6454463
--- /dev/null
+++ b/rola-desktop/app.manifest
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+ <!-- This manifest is used on Windows only.
+ Don't remove it as it might cause problems with window transparency and embedded controls.
+ For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
+ <assemblyIdentity version="1.0.0.0" name="rola_desktop.Desktop"/>
+
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!-- A list of the Windows versions that this application has been tested on
+ and is designed to work with. Uncomment the appropriate elements
+ and Windows will automatically select the most compatible environment. -->
+
+ <!-- Windows 10 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+ </application>
+ </compatibility>
+</assembly>
diff --git a/rola-desktop/rola-desktop.csproj b/rola-desktop/rola-desktop.csproj
new file mode 100644
index 0000000..5d6e1f8
--- /dev/null
+++ b/rola-desktop/rola-desktop.csproj
@@ -0,0 +1,32 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>WinExe</OutputType>
+ <TargetFramework>net9.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
+ <ApplicationManifest>app.manifest</ApplicationManifest>
+ <AvaloniaUseCompiledBindingsByDefault
+ >true</AvaloniaUseCompiledBindingsByDefault>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Folder Include="Models\" />
+ <AvaloniaResource Include="Assets\**" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Avalonia" Version="11.3.2" />
+ <PackageReference Include="Avalonia.Desktop" Version="11.3.2" />
+ <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.2" />
+ <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.2" />
+ <PackageReference Include="Avalonia.Diagnostics" Version="11.3.2">
+ <IncludeAssets
+ Condition="'$(Configuration)' != 'Debug'"
+ >None</IncludeAssets>
+ <PrivateAssets
+ Condition="'$(Configuration)' != 'Debug'"
+ >All</PrivateAssets>
+ </PackageReference>
+ <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
+ </ItemGroup>
+</Project>
diff --git a/rola-devtools/Cargo.lock b/rola-devtools/Cargo.lock
new file mode 100644
index 0000000..f03c9e3
--- /dev/null
+++ b/rola-devtools/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "rola-devtools"
+version = "0.1.0"
diff --git a/rola-devtools/Cargo.toml b/rola-devtools/Cargo.toml
new file mode 100644
index 0000000..dd6e6d7
--- /dev/null
+++ b/rola-devtools/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "rola-devtools"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
diff --git a/rola-devtools/scripts/check-all.py b/rola-devtools/scripts/check-all.py
new file mode 100644
index 0000000..cd2ddf1
--- /dev/null
+++ b/rola-devtools/scripts/check-all.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+from pathlib import Path
+
+script_dir = Path(__file__).resolve().parent
+root_dir = script_dir.parent.parent
+
+steps = [
+ ("cargo check --workspace", [
+ "cargo", "check", "--workspace",
+ ]),
+ ("cargo clippy --workspace -- -D warnings", [
+ "cargo", "clippy", "--workspace", "--", "-D", "warnings",
+ ]),
+ ("cargo build --workspace --release", [
+ "cargo", "build", "--workspace", "--release",
+ ]),
+ ("dotnet restore", [
+ "dotnet", "restore", "rola-desktop.sln",
+ ]),
+ ("dotnet build", [
+ "dotnet", "build", "rola-desktop.sln", "--nologo",
+ ]),
+]
+
+ok = True
+for label, cmd in steps:
+ print(f"\nSTEP - \"{label}\": ", flush=True)
+ result = subprocess.run(cmd, cwd=root_dir)
+ if result.returncode != 0:
+ print(f" [FAILED] Failed (exit code {result.returncode})", flush=True)
+ ok = False
+ break
+ print(f" [SUCCESS] Passed", flush=True)
+
+sys.exit(0 if ok else 1)
diff --git a/rola-devtools/scripts/run-desktop.py b/rola-devtools/scripts/run-desktop.py
new file mode 100644
index 0000000..e9d4093
--- /dev/null
+++ b/rola-devtools/scripts/run-desktop.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+from pathlib import Path
+
+script_dir = Path(__file__).resolve().parent
+project_dir = script_dir.parent.parent / "rola-desktop"
+csproj = project_dir / "rola-desktop.csproj"
+
+subprocess.run(["dotnet", "run", "--project", str(csproj), *sys.argv[1:]])
diff --git a/rola-devtools/scripts/windows-folder-hide.ps1 b/rola-devtools/scripts/windows-folder-hide.ps1
new file mode 100644
index 0000000..0ab2632
--- /dev/null
+++ b/rola-devtools/scripts/windows-folder-hide.ps1
@@ -0,0 +1,43 @@
+# Check `last_check`
+
+$lastCheckFile = Join-Path $PSScriptRoot "last_check"
+$currentTime = Get-Date
+$timeThreshold = 10
+
+if (Test-Path $lastCheckFile) {
+ $lastCheckTime = Get-Content $lastCheckFile | Get-Date
+ $timeDiff = ($currentTime - $lastCheckTime).TotalMinutes
+
+ if ($timeDiff -lt $timeThreshold) {
+ exit
+ }
+}
+
+$currentTime.ToString() | Out-File -FilePath $lastCheckFile -Force
+
+# Hide Files
+
+Set-Location -Path (Join-Path $PSScriptRoot "..\..")
+
+Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object {
+ $_.FullName -notmatch '\\.temp\\' -and $_.FullName -notmatch '\\.git\\'
+} | ForEach-Object {
+ attrib -h $_.FullName 2>&1 | Out-Null
+}
+
+Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object {
+ $_.Name -match '^\..*' -and $_.FullName -notmatch '\\\.\.$' -and $_.FullName -notmatch '\\\.$'
+} | ForEach-Object {
+ attrib +h $_.FullName 2>&1 | Out-Null
+}
+
+if (Get-Command git -ErrorAction SilentlyContinue) {
+ git status --ignored --short | ForEach-Object {
+ if ($_ -match '^!!\s+(.+)$') {
+ $ignoredPath = $matches[1]
+ if ($ignoredPath -notmatch '\.lnk$' -and (Test-Path $ignoredPath)) {
+ attrib +h $ignoredPath 2>&1 | Out-Null
+ }
+ }
+ }
+}
diff --git a/rola-devtools/src/bin/welcome.rs b/rola-devtools/src/bin/welcome.rs
new file mode 100644
index 0000000..3b1982f
--- /dev/null
+++ b/rola-devtools/src/bin/welcome.rs
@@ -0,0 +1,3 @@
+fn main() {
+ println!("Welcome!");
+}
diff --git a/rola-devtools/src/lib.rs b/rola-devtools/src/lib.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/rola-devtools/src/lib.rs
@@ -0,0 +1 @@
+
diff --git a/rola-draft/Cargo.toml b/rola-draft/Cargo.toml
new file mode 100644
index 0000000..af5edc6
--- /dev/null
+++ b/rola-draft/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "rola-draft"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
diff --git a/rola-draft/src/lib.rs b/rola-draft/src/lib.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/rola-draft/src/lib.rs
@@ -0,0 +1 @@
+
diff --git a/rola-utils/functions/Cargo.toml b/rola-utils/functions/Cargo.toml
new file mode 100644
index 0000000..dd482c4
--- /dev/null
+++ b/rola-utils/functions/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "shared_functions"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
diff --git a/rola-utils/functions/src/levenshtein_distance.rs b/rola-utils/functions/src/levenshtein_distance.rs
new file mode 100644
index 0000000..7b8be6c
--- /dev/null
+++ b/rola-utils/functions/src/levenshtein_distance.rs
@@ -0,0 +1,117 @@
+pub struct LevenshteinDistance;
+
+impl LevenshteinDistance {
+ pub fn filter_similar<'a>(str: &str, slice: &[&'a str], threshold: usize) -> Vec<&'a str> {
+ slice
+ .iter()
+ .filter(|s| levenshtein_distance(str, s) <= threshold)
+ .copied()
+ .collect()
+ }
+
+ pub fn compare(a: impl AsRef<str>, b: impl AsRef<str>) -> usize {
+ let a = a.as_ref();
+ let b = b.as_ref();
+ levenshtein_distance(a, b)
+ }
+}
+
+fn levenshtein_distance(a: &str, b: &str) -> usize {
+ let a_chars: Vec<char> = a.chars().collect();
+ let b_chars: Vec<char> = b.chars().collect();
+ let m = a_chars.len();
+ let n = b_chars.len();
+ if m == 0 {
+ return n;
+ }
+ if n == 0 {
+ return m;
+ }
+
+ let mut prev: Vec<usize> = (0..=n).collect();
+ let mut curr = vec![0; n + 1];
+ for (i, ca) in a_chars.iter().enumerate() {
+ curr[0] = i + 1;
+ for (j, cb) in b_chars.iter().enumerate() {
+ let cost = if ca == cb { 0 } else { 1 };
+ let del = prev[j + 1] + 1;
+ let ins = curr[j] + 1;
+ let rep = prev[j] + cost;
+ curr[j + 1] = del.min(ins).min(rep);
+ }
+ std::mem::swap(&mut prev, &mut curr);
+ }
+ prev[n]
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_levenshtein_distance_identical_strings() {
+ assert_eq!(LevenshteinDistance::compare("hello", "hello"), 0);
+ }
+
+ #[test]
+ fn test_levenshtein_distance_completely_different() {
+ let dist = LevenshteinDistance::compare("abc", "xyz");
+ assert_eq!(dist, 3);
+ }
+
+ #[test]
+ fn test_levenshtein_distance_one_insertion() {
+ assert_eq!(LevenshteinDistance::compare("cat", "cats"), 1);
+ }
+
+ #[test]
+ fn test_levenshtein_distance_one_deletion() {
+ assert_eq!(LevenshteinDistance::compare("dogs", "dog"), 1);
+ }
+
+ #[test]
+ fn test_levenshtein_distance_one_substitution() {
+ assert_eq!(LevenshteinDistance::compare("cat", "cut"), 1);
+ }
+
+ #[test]
+ fn test_levenshtein_distance_empty_strings() {
+ assert_eq!(LevenshteinDistance::compare("", ""), 0);
+ }
+
+ #[test]
+ fn test_levenshtein_distance_empty_vs_nonempty() {
+ assert_eq!(LevenshteinDistance::compare("", "abc"), 3);
+ assert_eq!(LevenshteinDistance::compare("xyz", ""), 3);
+ }
+
+ #[test]
+ fn test_levenshtein_distance_unicode() {
+ // Chinese characters
+ assert_eq!(LevenshteinDistance::compare("你好", "你好"), 0);
+ assert_eq!(LevenshteinDistance::compare("你好", "您好"), 1);
+ assert_eq!(LevenshteinDistance::compare("你好", "您不好"), 2);
+ }
+
+ #[test]
+ fn test_filter_similar_exact_threshold() {
+ let words = vec!["cat", "cart", "ca", "cats", "cut"];
+ let result = LevenshteinDistance::filter_similar("cat", &words, 2);
+ // cat -> cat: 0, cat -> cart: 1, cat -> ca: 1, cat -> cats: 1, cat -> cut: 1
+ assert_eq!(result.len(), 5);
+ }
+
+ #[test]
+ fn test_filter_similar_empty_slice() {
+ let words: Vec<&str> = vec![];
+ let result = LevenshteinDistance::filter_similar("hello", &words, 3);
+ assert!(result.is_empty());
+ }
+
+ #[test]
+ fn test_filter_similar_threshold_zero() {
+ let words = vec!["hello", "hallo", "hello!"];
+ let result = LevenshteinDistance::filter_similar("hello", &words, 0);
+ assert_eq!(result, vec!["hello"]);
+ }
+}
diff --git a/rola-utils/functions/src/lib.rs b/rola-utils/functions/src/lib.rs
new file mode 100644
index 0000000..ff2ee7e
--- /dev/null
+++ b/rola-utils/functions/src/lib.rs
@@ -0,0 +1,2 @@
+mod levenshtein_distance;
+pub use levenshtein_distance::*;
diff --git a/rola-utils/macros/Cargo.toml b/rola-utils/macros/Cargo.toml
new file mode 100644
index 0000000..f750afa
--- /dev/null
+++ b/rola-utils/macros/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "shared_macros"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "2.0", features = ["full", "visit-mut"] }
+quote = "1.0"
+proc-macro2 = "1.0"
diff --git a/rola-utils/macros/src/lib.rs b/rola-utils/macros/src/lib.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/rola-utils/macros/src/lib.rs
@@ -0,0 +1 @@
+
diff --git a/rola-vcs/Cargo.toml b/rola-vcs/Cargo.toml
new file mode 100644
index 0000000..ce32c14
--- /dev/null
+++ b/rola-vcs/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "rorolala"
+version.workspace = true
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+
+[dependencies]
+rorolala_internal_macros.workspace = true
+shared_functions.workspace = true
+shared_macros.workspace = true
+rola-bucket.workspace = true
+rola-draft.workspace = true
diff --git a/rola-vcs/internal_macros/Cargo.toml b/rola-vcs/internal_macros/Cargo.toml
new file mode 100644
index 0000000..0d6f641
--- /dev/null
+++ b/rola-vcs/internal_macros/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "rorolala_internal_macros"
+version.workspace = true
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "2", features = ["full"] }
+quote = "1"
+proc-macro2 = "1"
diff --git a/rola-vcs/internal_macros/src/constants.rs b/rola-vcs/internal_macros/src/constants.rs
new file mode 100644
index 0000000..2e76bfe
--- /dev/null
+++ b/rola-vcs/internal_macros/src/constants.rs
@@ -0,0 +1,115 @@
+use proc_macro::TokenStream;
+use quote::{format_ident, quote};
+use syn::{Expr, Item, ItemConst, ItemMod, Lit, parse_macro_input, parse_quote};
+
+/// Entry point called from lib.rs.
+pub fn expand(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ let mut input_mod = parse_macro_input!(item as ItemMod);
+
+ let (_, items) = match &mut input_mod.content {
+ Some(content) => content,
+ None => panic!("#[constants] can only be applied to a module with a body"),
+ };
+
+ let mut new_items: Vec<Item> = Vec::with_capacity(items.len());
+
+ for item in items.iter() {
+ if let Item::Const(const_item) = item {
+ let func = transform_const(const_item);
+ new_items.push(func);
+ } else {
+ new_items.push(item.clone());
+ }
+ }
+
+ let mod_ident = &input_mod.ident;
+ let vis = &input_mod.vis;
+
+ let output = quote! {
+ #[allow(non_snake_case)]
+ #vis mod #mod_ident {
+ #(#new_items)*
+ }
+ };
+
+ output.into()
+}
+
+/// Transforms a single `const` item into a function.
+fn transform_const(const_item: &ItemConst) -> Item {
+ let name = &const_item.ident;
+ let attrs = &const_item.attrs;
+
+ // Extract the string literal value from the const
+ let value_str = match &*const_item.expr {
+ Expr::Lit(expr_lit) => match &expr_lit.lit {
+ Lit::Str(lit_str) => lit_str.value(),
+ _ => panic!(
+ "#[constants] only supports `&str` literals, \
+ but `{name}` has a non-string literal"
+ ),
+ },
+ _ => panic!(
+ "#[constants] only supports literal expressions, \
+ but `{name}` has a non-literal expression"
+ ),
+ };
+
+ let placeholders = extract_placeholders(&value_str);
+
+ if placeholders.is_empty() {
+ parse_quote! {
+ #(#attrs)*
+ pub fn #name() -> String {
+ #value_str.to_string()
+ }
+ }
+ } else {
+ let params: Vec<_> = placeholders
+ .iter()
+ .map(|p| {
+ let ident = format_ident!("{p}");
+ quote! { #ident: impl ::core::convert::AsRef<str> }
+ })
+ .collect();
+
+ let format_args: Vec<_> = placeholders
+ .iter()
+ .map(|p| {
+ let ident = format_ident!("{p}");
+ quote! { #ident = #ident.as_ref() }
+ })
+ .collect();
+
+ parse_quote! {
+ #(#attrs)*
+ pub fn #name(#(#params),*) -> String {
+ ::std::format!(#value_str, #(#format_args),*)
+ }
+ }
+ }
+}
+
+/// Extracts all `{name}` placeholder identifiers from a format string.
+fn extract_placeholders(s: &str) -> Vec<String> {
+ let mut placeholders = Vec::new();
+ let mut chars = s.char_indices().peekable();
+
+ while let Some((_, c)) = chars.next() {
+ if c == '{' {
+ let mut name = String::new();
+ for (_, c) in &mut chars {
+ if c == '}' {
+ break;
+ }
+ name.push(c);
+ }
+ let trimmed = name.trim().to_string();
+ if !trimmed.is_empty() {
+ placeholders.push(trimmed);
+ }
+ }
+ }
+
+ placeholders
+}
diff --git a/rola-vcs/internal_macros/src/lib.rs b/rola-vcs/internal_macros/src/lib.rs
new file mode 100644
index 0000000..f6c3cb7
--- /dev/null
+++ b/rola-vcs/internal_macros/src/lib.rs
@@ -0,0 +1,39 @@
+mod constants;
+
+use proc_macro::TokenStream;
+
+/// Transforms `pub const` items in a module into equivalent functions.
+///
+/// Constants without `{param}` placeholders become `fn NAME() -> String`.
+/// Constants with `{param}` placeholders become `fn NAME(param: impl AsRef<str>) -> String`,
+/// using `format!()` to fill in the placeholders.
+///
+/// The entire module is annotated with `#[allow(non_snake_case)]`.
+///
+/// # Example
+///
+/// ```ignore
+/// #[rorolala_internal_macros::constants]
+/// pub mod paths {
+/// pub const ROLA_DRAFT_DIR: &str = ".rola";
+/// pub const ROLA_BINDED_BUCKET_FILE: &str = ".rola/BIND/{bucket}";
+/// }
+/// ```
+///
+/// expands to:
+///
+/// ```ignore
+/// #[allow(non_snake_case)]
+/// pub mod paths {
+/// pub fn ROLA_DRAFT_DIR() -> String {
+/// ".rola".to_string()
+/// }
+/// pub fn ROLA_BINDED_BUCKET_FILE(bucket: impl AsRef<str>) -> String {
+/// format!(".rola/BIND/{bucket}", bucket = bucket.as_ref())
+/// }
+/// }
+/// ```
+#[proc_macro_attribute]
+pub fn constants(attr: TokenStream, item: TokenStream) -> TokenStream {
+ constants::expand(attr, item)
+}
diff --git a/rola-vcs/src/consts/common.rs b/rola-vcs/src/consts/common.rs
new file mode 100644
index 0000000..e2bfd8c
--- /dev/null
+++ b/rola-vcs/src/consts/common.rs
@@ -0,0 +1,7 @@
+#[rorolala_internal_macros::constants]
+mod consts {
+ pub const ROLA_DRAFT_DIR: &str = ".rola";
+ pub const ROLA_BINDED_BUCKET_FILE: &str = ".rola/BIND/{bucket}";
+}
+
+pub use consts::*;
diff --git a/rola-vcs/src/consts/mod.rs b/rola-vcs/src/consts/mod.rs
new file mode 100644
index 0000000..598ae17
--- /dev/null
+++ b/rola-vcs/src/consts/mod.rs
@@ -0,0 +1,2 @@
+mod common;
+pub use common::*;
diff --git a/rola-vcs/src/lib.rs b/rola-vcs/src/lib.rs
new file mode 100644
index 0000000..ac52569
--- /dev/null
+++ b/rola-vcs/src/lib.rs
@@ -0,0 +1,11 @@
+#![allow(unused)]
+
+pub mod bucket {
+ pub use rola_bucket::*;
+}
+
+pub mod draft {
+ pub use rola_draft::*;
+}
+
+pub mod consts;
diff --git a/run-tools.ps1 b/run-tools.ps1
new file mode 100644
index 0000000..3b0bcf2
--- /dev/null
+++ b/run-tools.ps1
@@ -0,0 +1,60 @@
+Set-Location -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -ErrorAction Stop
+
+# Collect all available tool names
+$tools = @()
+
+if (Test-Path "rola-devtools/scripts") {
+ $scripts = Get-ChildItem -Path "rola-devtools/scripts/*.ps1", "rola-devtools/scripts/*.py"
+ foreach ($script in $scripts) {
+ if ($script -is [System.IO.FileInfo]) {
+ $tools += $script.BaseName
+ }
+ }
+}
+if (Test-Path "rola-devtools/src/bin") {
+ $files = Get-ChildItem -Path "rola-devtools/src/bin/*.rs"
+ foreach ($file in $files) {
+ if ($file -is [System.IO.FileInfo]) {
+ $tools += $file.BaseName
+ }
+ }
+}
+
+if ($args.Count -eq 0) {
+ Write-Host "Available:"
+ for ($i = 0; $i -lt $tools.Count; $i++) {
+ Write-Host (" [{0,2}] {1}" -f ($i + 1), $tools[$i])
+ }
+ exit 1
+}
+
+$target_name = $args[0]
+
+# Check if input is a number
+if ($target_name -match '^\d+$') {
+ $idx = [int]$target_name - 1
+ if ($idx -ge 0 -and $idx -lt $tools.Count) {
+ $target_name = $tools[$idx]
+ } else {
+ Write-Host "Error: invalid number '$target_name', valid range is 1-$($tools.Count)"
+ exit 1
+ }
+}
+
+# Collect remaining arguments to pass to the script
+$script_args = $args[1..$args.Count]
+
+$script_file_ps1 = "rola-devtools/scripts/${target_name}.ps1"
+$script_file_py = "rola-devtools/scripts/${target_name}.py"
+$rust_file = "rola-devtools/src/bin/${target_name}.rs"
+
+if (Test-Path $script_file_ps1) {
+ & $script_file_ps1 $script_args
+} elseif (Test-Path $script_file_py) {
+ python $script_file_py $script_args
+} elseif (Test-Path $rust_file) {
+ cargo run --manifest-path rola-devtools/Cargo.toml --bin $target_name --quiet -- $script_args
+} else {
+ Write-Host "Error: target '$target_name' does not exist as a script or Rust program"
+ exit 1
+}
diff --git a/run-tools.sh b/run-tools.sh
new file mode 100644
index 0000000..95464a8
--- /dev/null
+++ b/run-tools.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+cd "$(dirname "$0")" || exit 1
+
+# Collect all available tool names
+tools=()
+
+if [ -d "rola-devtools/scripts" ]; then
+ for file in rola-devtools/scripts/*.sh; do
+ if [ -f "$file" ]; then
+ tools+=("$(basename "$file" .sh)")
+ fi
+ done
+ for file in rola-devtools/scripts/*.py; do
+ if [ -f "$file" ]; then
+ tools+=("$(basename "$file" .py)")
+ fi
+ done
+fi
+if [ -d "rola-devtools/src/bin" ]; then
+ for file in rola-devtools/src/bin/*.rs; do
+ if [ -f "$file" ]; then
+ tools+=("$(basename "$file" .rs)")
+ fi
+ done
+fi
+
+if [ $# -eq 0 ]; then
+ echo "Available:"
+ for i in "${!tools[@]}"; do
+ printf " [%2d] %s\n" $((i + 1)) "${tools[$i]}"
+ done
+ exit 1
+fi
+
+target_bin="$1"
+shift # Remove the first argument (tool name), keep the rest as tool arguments
+
+# Check if input is a number
+if [[ "$target_bin" =~ ^[0-9]+$ ]]; then
+ idx=$((target_bin - 1))
+ if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#tools[@]}" ]; then
+ target_bin="${tools[$idx]}"
+ else
+ echo "Error: invalid number '$target_bin', valid range is 1-${#tools[@]}"
+ exit 1
+ fi
+fi
+
+target_script="rola-devtools/scripts/${target_bin}.sh"
+target_python="rola-devtools/scripts/${target_bin}.py"
+target_file="rola-devtools/src/bin/${target_bin}.rs"
+
+if [ -f "$target_script" ]; then
+ chmod +x "$target_script"
+ "$target_script" "$@"
+elif [ -f "$target_python" ]; then
+ python "$target_python" "$@"
+elif [ -f "$target_file" ]; then
+ cargo run --manifest-path rola-devtools/Cargo.toml --bin "$target_bin" --quiet -- "$@"
+else
+ echo "Error: target '$target_bin' does not exist"
+ exit 1
+fi