Compare commits

..
16 Commits
55 changed files with 3648 additions and 2025 deletions
+198 -74
View File
@@ -34,7 +34,7 @@ clsid = "{B5291320-FE7C-4069-BF87-A0AC327FCD20}"
#
# This key is optional. If you don't fill this key,
# Windows will use your executable embedded Win32 resources for friendly name.
name = "default"
name = "self"
# The icon of this program.
#
@@ -42,7 +42,7 @@ name = "default"
#
# This key is optional. If you don't fill this key,
# Windows will use the first icon embedded in your executable as icon.
icon = "others"
icon = "self"
# The default behavior of your program.
#
@@ -69,31 +69,41 @@ behavior = "generic"
#
# Due to the same use of back-slash in path part,
# you can utilize TOML "literal strings" syntax to write this value easier.
default = '@C:\path\to\ppic.exe,-1001'
# self = '@C:\path\to\ppic.exe,-1001'
# The value of this key also can be a plain string like this commente statement,
# if you don't want to touch Win32 resources and your program do not have any I18N requirements.
# default = "Pineapple Picture"
self = "Pineapple Pictures"
# And more string resources...
jpg = '@C:\path\to\ppic.exe,-1011'
jfif = '@C:\path\to\ppic.exe,-1050'
gif = '@C:\path\to\ppic.exe,-1012'
bmp = '@C:\path\to\ppic.exe,-1013'
png = '@C:\path\to\ppic.exe,-1014'
ico = '@C:\path\to\ppic.exe,-1015'
jpeg = '@C:\path\to\ppic.exe,-1016'
tif = '@C:\path\to\ppic.exe,-1017'
tiff = '@C:\path\to\ppic.exe,-1018'
webp = '@C:\path\to\ppic.exe,-1019'
avif = '@C:\path\to\ppic.exe,-1020'
svg = '@C:\path\to\ppic.exe,-1021'
kra = '@C:\path\to\ppic.exe,-1022'
xcf = '@C:\path\to\ppic.exe,-1023'
qoi = '@C:\path\to\ppic.exe,-1024'
apng = '@C:\path\to\ppic.exe,-1025'
exr = '@C:\path\to\ppic.exe,-1026'
tga = '@C:\path\to\ppic.exe,-1027'
jpeg = 'JPEG Image'
gif = 'GIF Animation Image'
png = 'PNG Image'
svg = 'Scalable Vector Graphics'
webp = 'WebP Image'
avif = 'AV1 Image File'
ico = 'Windows Icon Image'
icns = 'Apple Icon Image'
psd = 'Photoshop Document'
kra = 'Krita Document'
xcf = 'GIMP Document'
tga = 'TARGA Image'
tif = 'Tagged Image File Format'
bmp = 'Bitmap'
wbmp = 'Wireless Bitmap'
dds = 'DirectDraw Surface'
hdr = 'High Dynamic Range Image'
iff = 'Interchange File Format'
pic = 'Softimage Picture'
pcx = 'PiCture eXchange PC Paintbrush Image'
qoi = 'Quite OK Image'
ras = 'Sun Raster Image'
ora = 'OpenRaster Image'
ani = 'Animated Cursor'
pfm = 'Portable Float Map'
rgb = 'Silicon Graphics Image'
pxr = 'Pixar Raster Image'
sct = 'Scitex Continuous Tone'
# ======== Icon Resources ========
@@ -113,18 +123,17 @@ tga = '@C:\path\to\ppic.exe,-1027'
#
# Due to the same use of back-slash in path part,
# you can utilize TOML "literal strings" syntax to write this value easier.
png = 'C:\path\to\ppic.exe,-1'
self = 'C:\path\to\ppic.exe,0'
# The value of this key also can be the absolute path to existing icon file like this commente statement.
# png = 'C:\path\to\ppic\icons\png.ico'
generic = 'C:\path\to\ppic\icons\generic.ico'
# Add more icon resources...
svg = 'C:\path\to\ppic.exe,-2'
gif = 'C:\path\to\ppic.exe,-3'
jpg = 'C:\path\to\ppic.exe,-4'
webp = 'C:\path\to\ppic.exe,-5'
avif = 'C:\path\to\ppic.exe,-6'
others = 'C:\path\to\ppic.exe,-61'
gif = 'C:\path\to\ppic\icons\gif.ico'
jpeg = 'C:\path\to\ppic\icons\jpeg.ico'
png = 'C:\path\to\ppic\icons\png.ico'
psd = 'C:\path\to\ppic\icons\psd.ico'
svg = 'C:\path\to\ppic\icons\svg.ico'
# ======== Behaviors Collection ========
@@ -162,76 +171,191 @@ generic = '"C:\path\to\ppic.exe" "%1"'
[exts.jpg]
# First, we specify its name shown in Windows Explorer.
# The value of this field is the token declared above in "strs" list.
name = "jpg"
name = "jpeg"
# Then, speficy its associated icon.
# The value of this field is the token declared above in "icons" list.
icon = "jpg"
icon = "jpeg"
# At last, specify the behavior of this extension define decide how we open this file.
# The value of this field is the token declared above in "behaviors" list.
behavior = "generic"
# And more file extensions...
[exts.jfif]
name = "jfif"
icon = "jpg"
[exts.jpeg]
name = "jpeg"
icon = "jpeg"
behavior = "generic"
[exts.jfif]
name = "jpeg"
icon = "jpeg"
behavior = "generic"
[exts.gif]
name = "gif"
icon = "gif"
behavior = "generic"
[exts.bmp]
name = "bmp"
icon = "others"
behavior = "generic"
[exts.png]
name = "png"
icon = "png"
behavior = "generic"
[exts.ico]
name = "ico"
icon = "others"
behavior = "generic"
[exts.jpeg]
name = "jpeg"
icon = "jpg"
behavior = "generic"
[exts.tif]
name = "tif"
icon = "others"
behavior = "generic"
[exts.tiff]
name = "tiff"
icon = "others"
behavior = "generic"
[exts.webp]
name = "webp"
icon = "webp"
behavior = "generic"
[exts.svg]
name = "svg"
icon = "svg"
behavior = "generic"
[exts.kra]
name = "kra"
icon = "others"
behavior = "generic"
[exts.xcf]
name = "xcf"
icon = "others"
[exts.webp]
name = "webp"
icon = "generic"
behavior = "generic"
[exts.avif]
name = "avif"
icon = "avif"
icon = "generic"
behavior = "generic"
[exts.ico]
name = "ico"
icon = "generic"
behavior = "generic"
[exts.icns]
name = "icns"
icon = "generic"
behavior = "generic"
[exts.psd]
name = "psd"
icon = "psd"
behavior = "generic"
[exts.psb]
name = "psd"
icon = "psd"
behavior = "generic"
[exts.pdd]
name = "psd"
icon = "psd"
behavior = "generic"
[exts.psdt]
name = "psd"
icon = "psd"
behavior = "generic"
[exts.kra]
name = "kra"
icon = "generic"
behavior = "generic"
[exts.xcf]
name = "xcf"
icon = "generic"
behavior = "generic"
[exts.tga]
name = "tga"
icon = "generic"
behavior = "generic"
[exts.tif]
name = "tif"
icon = "generic"
behavior = "generic"
[exts.tiff]
name = "tif"
icon = "generic"
behavior = "generic"
[exts.bmp]
name = "bmp"
icon = "generic"
behavior = "generic"
[exts.wbmp]
name = "wbmp"
icon = "generic"
behavior = "generic"
[exts.dds]
name = "dds"
icon = "generic"
behavior = "generic"
[exts.hdr]
name = "hdr"
icon = "generic"
behavior = "generic"
[exts.iff]
name = "iff"
icon = "generic"
behavior = "generic"
[exts.pic]
name = "pic"
icon = "generic"
behavior = "generic"
[exts.pcx]
name = "pcx"
icon = "generic"
behavior = "generic"
[exts.qoi]
name = "qoi"
icon = "others"
icon = "generic"
behavior = "generic"
[exts.apng]
name = "apng"
icon = "png"
[exts.ras]
name = "ras"
icon = "generic"
behavior = "generic"
[exts.exr]
name = "exr"
icon = "others"
[exts.sun]
name = "ras"
icon = "generic"
behavior = "generic"
[exts.ora]
name = "ora"
icon = "generic"
behavior = "generic"
[exts.ani]
name = "ani"
icon = "generic"
behavior = "generic"
[exts.pfm]
name = "pfm"
icon = "generic"
behavior = "generic"
[exts.phm]
name = "pfm"
icon = "generic"
behavior = "generic"
[exts.rgb]
name = "rgb"
icon = "generic"
behavior = "generic"
[exts.rgba]
name = "rgb"
icon = "generic"
behavior = "generic"
[exts.bw]
name = "rgb"
icon = "generic"
behavior = "generic"
[exts.sgi]
name = "rgb"
icon = "generic"
behavior = "generic"
[exts.pxr]
name = "pxr"
icon = "generic"
behavior = "generic"
[exts.sct]
name = "sct"
icon = "generic"
behavior = "generic"
+36
View File
@@ -0,0 +1,36 @@
cmake_minimum_required(VERSION 3.20)
project(qwfassoc_suite LANGUAGES CXX)
# Qt 6 requires C++17 at minimum.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Let CMake auto-process Q_OBJECT, .ui files and .qrc resources for every
# subproject below.
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
# Make the bundled Findwfassoc.cmake module visible to find_package().
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# Qt and LinguistTools are used by both subprojects, so they are looked up at
# the top level. The same is true for wfassoc: although only the qwfassoc
# library links against it directly, PUBLIC propagation from the library
# target makes the dependency available to qwfassoc-standalone as well.
# toml11 is only needed when the standalone executable is built, so it is
# looked up conditionally below.
find_package(Qt6 REQUIRED COMPONENTS Widgets LinguistTools)
find_package(wfassoc REQUIRED)
# The standalone executable is optional: embedders may want only the library.
option(QWFASSOC_BUILD_STANDALONE "Build the qwfassoc-standalone executable" ON)
add_subdirectory(qwfassoc)
if(QWFASSOC_BUILD_STANDALONE)
find_package(toml11 REQUIRED)
add_subdirectory(qwfassoc-standalone)
endif()
-241
View File
@@ -1,241 +0,0 @@
我要求你使用Qt Widget编写一个GUI程序。该GUI程序是wfassoc的一个可视化界面。wfassoc是一个由Rust编写,并暴露出C接口,可以操作Windows注册表,来管理应用程序的注册和卸载,以及文件关联的动态链接库。
我要求你在@example/qwfassoc/TASKS.md 中先做好详细的规划,而不是上来就写代码。我会安排其他人来负责执行你做的规划。
# 项目要求
- 使用Qt Widget编写界面,而不是QML。
- 使用UI文件而不是C++语句来构建界面。
- 我使用的是Qt 6,使用CMake作为构建系统,不要使用Qt的qmake。
- 使用toml11作为TOML读取库。
# 界面要求
这是一个基于 Qt Widgets 的标准对话框界面描述。你可以按照以下层级结构来构建代码:
## 主窗口容器 (Main Window)
* 类: `QDialog`。
* 窗口标题: "xxx选项"。xxx在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。
* 窗口图标:在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。
* 大小限制:固定大小480x600
## 选项卡 (Top Tabs)
* 组件: `QTabWidget`。
* 标签页 (Tabs): 从左到右依次添加以下标签页:
1. 应用程序
2. 文件关联
* 大小:选项卡占据对话框全部内容
## "应用程序"选项卡内容
该选项卡内部使用垂直布局 (`QVBoxLayout`),包含一个主要的分组区域:
### 区域内容
* 容器: `QGroupBox`。
* 区域标题:安装与卸载
* 布局: 垂直布局 (`QVBoxLayout`)。
* 上半部分
* 水平布局(`QHBoxLayout`)。
* 左侧: 一个 `QLabel` 显示图标,该图标表示要设定的应用程序的图标。在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。
* 右侧: 一个 `QLabel` 显示文本"在此安装或卸载xxx",表示要设定的应用程序的名称。xxx在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。 (文本需要设置自动换行 `setWordWrap(true)`)。
* 下半部分
* 水平布局(`QHBoxLayout`)。
* 内容为两个 `QPushButton`,文本分别为:
* 安装:为当前对象(系统或当前用户,由应用程序初始化时从命令行参数获取)安装应用。如果应用程序已经安装,则不可点击。
* 卸载:为当前对象(系统或当前用户,由应用程序初始化时从命令行参数获取)卸载应用。如果应用程序没有安装,则不可点击。
## "文件关联"选项卡内容
在这个选项页内部,使用一个垂直布局 (`QVBoxLayout`) 来排列以下控件:
* 说明文本:
* 组件: `QLabel`。
* 文本: "使用 xxx 关联的文件类型:"。xxx在应用程序初始化时,通过wfassoc的Program提供的接口,运行时获取。
* 功能按钮行:
* 布局: `QHBoxLayout` (水平布局)。
* 组件: 两个 `QPushButton`。
* 文本: 两个按钮的文本都是 "+"。
* 位置: 位于列表上方,用于“全选”操作(第一次点击,将所有没关联的文件扩展名(显示为空白)设置为应用程序提供的打开方式。如果没有空白内容,或第二次点击,将所有文件全部设置为应用程序提供的打开方式)。
* 文件类型列表 (核心组件):
* 组件: `QTableWidget` (表格控件)。
* 列数: 3列。
* 表头 (Header):
* 第1列标题: "类型"
* 第2列标题: "uuu" (uuu在运行时进行获取,为当前用户名)
* 第3列标题: "所有用户"
* 行内容示例:
* 第一列:一个文件类型图标,右边跟着对应的文本。表示当前文件扩展名,和当前混合视图(hybrid)下的图标。图标和文本均从wfassoc函数获取。
* 第二列:用户视图(user)下的名称。文本从wfassoc函数获取。
* 第三列:系统视图(system)下的名称。文本从wfassoc函数获取。
* 滚动条: 右侧有一个垂直滚动条 (`QScrollBar`),表示内容超出可视区域。
* 操作方式:
* 第二列和第三列的元素可点击。
* 如果元素为空白或其它打开方式,则点击后设置为当前应用程序指定的打开方式(link)。
* 如果是自身打开方式,点击后设置为空白(unlink)
* 点击后,第一列的图标需要改变,也因此你需要暂存当前应用程序提供打开方式的图标。如果第二第三列均为空,则不显示图标(仍然占位,显示为空白)。
* 点击操作并不会实时操作注册表,程序需要暂存用户的需求,然后在点击确认或应用按钮后再统一执行。
* 底部按钮栏
* 布局: `QHBoxLayout` (水平布局),通常右对齐或使用 `QDialogButtonBox`。
* 组件: 三个 `QPushButton`。
* 按钮文本 (从左到右):
1. "确定" (通常设为默认按钮 `setDefault(true)`)。
2. "取消"。
3. "应用":点击后应用修改,并留在页面 (如果没有修改,则不可用)。
额外注意:
* 如果应用程序没有安装,则本页面下所有内容均不启用。
* 如果启动时命令行指定以为当前用户安装的模式启动,则系统那一栏所有按钮都不可用
# 代码要求
- 有关wfassoc的接口,请查阅@wfassoc-cdylib/codegen/wfassoc++.h 我要求你使用这个头文件中提供的内容来进行编写。
- wfassoc++.h文件中没有注释,如果你想查看注释,请访问@wfassoc-cdylib/codegen/wfassoc.h 文件。wfassoc++.h是wfassoc.h的C++包装。
- wfassoc.h所暴露的接口是由Rust编写的,通常查看wfassoc.h可满足所有需求。如果仍有不确定的内容,可查看其对应Rust项目的源码,位于@wfassoc-cdylib/src 目录下。或更进一步地查看其依赖的源码,位于@wfassoc/src 目录下。
- 该GUI程序需要接受两个必要的命令行参数,请使用Qt内置的命令行解析器进行解析:
- `-c`或`--manifest`:指定要配置的应用程序的清单文件。
- `-f`或`--for`:指定应用程序要安装到的
- 清单文件的样例是@example/manifest/ppic.toml
- 在应用程序加载时,或者执行操作时,如果发生错误(例如底层wfassoc发生错误,丢失命令行选项等,则弹出对话框报错,然后立即退出程序)
- 程序基本流程:
- 接受命令行,检查命令行参数是否合法
- 加载命令行指定的manifest文件,并使用sanitizer检查错误。你可以阅读@wfassoc-exec/src/manifest.rs 文件来看看我是如何在Rust中检查它的。
- 按照给定的manifest文件,使用wfassoc库构建schema,然后再构建program。
- 初始化窗口。
- 调用wfassoc program提供的函数,检查应用程序是否安装,设置窗口的安装部分的按钮enable。
- 调用wfassoc program提供的函数,遍历所有文件扩展名和关联情况,设置窗口的文件关联表格。
- 用户可以在"应用程序"选项卡中注册和卸载应用程序,点击后弹出窗口表示安装或卸载成功,然后检测是否安装,并刷新各个控件的enable状态。
- 用户可以在"文件关联"选项卡中设置是否以当前应用程序打开某些扩展名。用户可以点击全选按钮或单元格来进行设置,应用程序暂存修改,等用户点击确认或应用后再应用修改。如果点击的是应用,则刷新当前页面。
# 额外要求
- 不要尝试去编译来检查错误。我会安排其他人来检查程序是否能正常运行,并汇报回来,你再修改。
- 你不需要关心能否找到Qtwfassoc和toml11这些库。
- 对于wfassoc,你只需要将@wfassoc-cdylib/codegen/Findwfassoc.cmake 复制到@example/qwfassoc/cmake 目录下,并在此目录下编写一个README.md,表明这个文件是从哪里复制来的即可。然后把复制的cmake文件所在目录加入find_package目录,然后使用find_package寻找wfassoc即可。至于去哪里找这个库,我会安排其他人来做。
- 对于Qt和toml11我会安排其他人来做,你只需要用find_package来找他们就行,不需要操心能不能找到。
- 如果你对某项需求有疑问,请问我,而不是进行猜测。
### 总结代码结构示意 (伪代码):
```cpp
QDialog *dialog = new QDialog();
dialog->setWindowTitle("选项");
QVBoxLayout *mainLayout = new QVBoxLayout(dialog);
// 1. Tab Widget
QTabWidget *tabWidget = new QTabWidget();
tabWidget->addTab(new QWidget(), "系统");
tabWidget->addTab(new QWidget(), "7-Zip");
// ... 其他 tabs
// 2. System Tab Content
QWidget *systemTab = tabWidget->widget(0);
QVBoxLayout *systemLayout = new QVBoxLayout(systemTab);
// Label
QLabel *label = new QLabel("使用 7-Zip 关联的文件类型:");
systemLayout->addWidget(label);
// Buttons (+)
QHBoxLayout *btnLayout = new QHBoxLayout();
QPushButton *btnPlus1 = new QPushButton("+");
QPushButton *btnPlus2 = new QPushButton("+");
btnLayout->addWidget(btnPlus1);
btnLayout->addWidget(btnPlus2);
systemLayout->addLayout(btnLayout);
// List (TreeWidget)
QTreeWidget *treeWidget = new QTreeWidget();
treeWidget->setColumnCount(3);
treeWidget->setHeaderLabels(QStringList() << "类型" << "yyc12345" << "所有用户");
// 添加 items...
systemLayout->addWidget(treeWidget);
// 3. Bottom Buttons
QHBoxLayout *bottomLayout = new QHBoxLayout();
bottomLayout->addStretch(); // 推挤按钮到右边
QPushButton *btnOK = new QPushButton("确定");
QPushButton *btnCancel = new QPushButton("取消");
QPushButton *btnApply = new QPushButton("应用(A)");
btnApply->setEnabled(false); // 禁用
QPushButton *btnHelp = new QPushButton("帮助");
bottomLayout->addWidget(btnOK);
bottomLayout->addWidget(btnCancel);
bottomLayout->addWidget(btnApply);
bottomLayout->addWidget(btnHelp);
mainLayout->addWidget(tabWidget);
mainLayout->addLayout(bottomLayout);
```
这是一个标准的 Windows 风格属性对话框,可以通过以下 Qt Widgets 结构来描述:
### 1. 主窗口容器
* 类: `QDialog`。
* 标题: "系统属性"。
* 布局: 垂直布局 (`QVBoxLayout`)。
### 2. 顶部选项卡 (Tabs)
* 组件: `QTabWidget`。
* 标签页: 包含 "计算机名", "硬件", "高级", "系统保护", "远程"。
* 当前选中: "硬件" 标签页。
### 3. "硬件" 选项卡内容
该选项卡内部使用垂直布局 (`QVBoxLayout`),包含两个主要的分组区域(视觉上类似 `QGroupBox` 或带有边框的 `QFrame`):
#### 区域 A: 设备管理器 (上半部分)
* 容器: 一个带有边框的容器。
* 布局: 水平布局 (`QHBoxLayout`)。
* 左侧: 一个 `QLabel` 显示电脑图标。
* 中间: 一个 `QLabel` 显示说明文本:"设备管理器列出所有安装在计算机上的硬件设备。请使用设备管理器来更改设备的属性。" (文本需要设置自动换行 `setWordWrap(true)`)。
* 右侧/底部: 一个 `QPushButton`,文本为 "设备管理器(D)"。
#### 区域 B: 设备安装设置 (下半部分)
* 容器: 一个带有边框的容器。
* 布局: 垂直布局 (`QVBoxLayout`)。
* 顶部行: 水平布局 (`QHBoxLayout`)。
* 左侧: 一个 `QLabel` 显示列表/勾选图标。
* 右侧: 一个 `QLabel` 显示说明文本:"选择 Windows 是否下载制造商提供的可用于你的设备的应用和自定义图标。" (文本需要设置自动换行)。
* 底部: 一个 `QPushButton`,文本为 "设备安装设置(S)",靠右对齐。
### 4. 底部按钮栏
* 布局: 水平布局 (`QHBoxLayout`),右对齐 (通常通过 `addStretch()` 实现)。
* 组件: 三个 `QPushButton`。
* "确定"
* "取消"
* "应用(A)" (注意:截图中该按钮呈灰色,代码中需设置 `setEnabled(false)`)。
你制定的计划有一些问题,请按照下述标出的问题一一修正:
- 编写的代码和说明文件需要使用英文注释。
- 是manifest而非manifesto,表示清单文件,请修正这个拼写错误。
- 我看到你在mainwindow篇章中编写了大量的C++代码,这没有必要。你是计划者而非执行者。你需要把需要在这个头文件中实现什么?该怎么做?需要使用哪些wfassoc函数,这些函数该怎么调用?在哪里查看他们怎么调用?详细的告诉将要执行这些任务的执行者,而不是直接为他们编写好代码。你在manifest部分的任务规划就非常符合这种范式。
+153 -2
View File
@@ -1,3 +1,154 @@
# Q WFAssoc
# qwfassoc (suite)
TODO
A Qt Widgets based GUI for the [wfassoc](../../wfassoc) library, split into a
reusable shared library and a small standalone executable that exercises it.
The project is organized as two CMake subprojects:
```
qwfassoc/ Parent directory (this README)
├── CMakeLists.txt Top-level CMake; add_subdirectory's both
│ subprojects and finds Qt, wfassoc, toml11
├── cmake/
│ ├── Findwfassoc.cmake Verbatim copy of wfassoc's Find module
│ └── README.md Provenance notes for the copy
├── qwfassoc/ Shared library subproject
│ ├── CMakeLists.txt
│ ├── i18n/
│ │ └── qwfassoc_zh_CN.ts Empty placeholder translation file
│ └── src/
│ ├── qwfassoc_global.h QWFASSOC_EXPORT macro
│ ├── scope.h Shared TargetScope enum
│ ├── manifest.h Manifest data struct (no TOML dependency)
│ ├── icon_utils.h/.cpp wfassocpp::HICON -> QPixmap conversion
│ ├── application_widget.h/.cpp/.ui
│ │ Install / uninstall widget
│ └── association_widget.h/.cpp/.ui
│ File associations widget
└── qwfassoc-standalone/ Executable subproject
├── CMakeLists.txt
├── i18n/
│ └── qwfassoc-standalone_zh_CN.ts
│ Empty placeholder translation file
└── src/
├── main.cpp Entry point, CLI parsing, translator loading
├── main_window.h/.cpp/.ui
│ QDialog hosting the two widgets in a tab widget
└── manifest_parser.h/.cpp
TOML -> Manifest, Manifest -> Schema
```
## Subprojects at a glance
### `qwfassoc` (shared library)
Exports two reusable widgets that wrap wfassoc:
* `qwfassoc::ApplicationWidget` — install / uninstall the program in the
configured scope.
* `qwfassoc::AssociationWidget` — stage and apply per-extension link / unlink
operations.
Both widgets follow the **two-phase initialization** pattern expected by Qt
Designer promoted widgets: the constructor only takes a `QWidget*` parent and
leaves the widget disabled. A `setConfig(Config)` method injects the
`wfassocpp::Program` pointer and `TargetScope` (and, for the association
widget, whether the OK/Cancel buttons are visible). Each widget also exposes:
* a `refresh()` slot that re-queries the live wfassoc state, intended to be
called by the host when another component has mutated the registry;
* a `changed()` signal emitted whenever the widget itself mutates the
registry (install / uninstall / apply);
* (`AssociationWidget` only) a `finished(bool accepted)` signal emitted when
the user clicks OK (after `changed()`) or Cancel, so the host can close the
dialog.
The library also exposes the plain `qwfassoc::Manifest` data struct and a
`qwfassoc::icon_utils::fromHicon()` helper, but the TOML parsing logic (which
depends on toml11) lives in the standalone executable.
### `qwfassoc-standalone` (executable)
Reproduces the original tabbed wfassoc configurator by:
1. parsing `-c/--manifest <path>` and `-f/--for <user|system>` from the
command line;
2. building a `wfassocpp::Program` via `parseManifestFile` + `buildSchema`;
3. hosting `ApplicationWidget` and `AssociationWidget` inside a `QTabWidget`
in a `MainWindow` dialog;
4. wiring the widgets' `changed()` and `finished()` signals so that any
registry mutation refreshes both pages and OK/Cancel drive the dialog's
acceptance.
## Requirements
* **CMake** 3.20 or newer (3.21+ recommended for `qt6_add_translations`).
* A C++17 compiler.
* **Qt 6** with the `Widgets` and `LinguistTools` components.
* **wfassoc**, with `wfassoc_ROOT` pointing at an installed tree (see
[`cmake/Findwfassoc.cmake`](cmake/Findwfassoc.cmake) for the expected
directory layout).
* **toml11** — only required when building the standalone executable.
## Building
```bat
cmake -S . -B build ^
-DCMAKE_PREFIX_PATH=C:\Qt\6.x.x\msvc2022_64 ^
-Dwfassoc_ROOT=C:\path\to\wfassoc\install ^
-Dtoml11_DIR=C:\path\to\toml11\share\toml11\cmake
cmake --build build --config Release
```
To skip the standalone executable (and the toml11 dependency):
```bat
cmake -S . -B build -DQWFASSOC_BUILD_STANDALONE=OFF ...
```
The standalone executable is `build/qwfassoc-standalone/Release/qwfassoc-standalone.exe`
(or similar, depending on the generator); the library is
`build/qwfassoc/Release/qwfassoc.dll`.
## Running the standalone executable
| Short | Long | Meaning |
| ----- | ------------ | ------------------------------------------------------------------------ |
| `-c` | `--manifest` | Path to the application manifest TOML file (see [`example/manifest/ppic.toml`](../manifest/ppic.toml)). |
| `-f` | `--for` | Target scope: `user` or `system`. |
```bat
qwfassoc-standalone -c C:\path\to\ppic.toml -f user
```
## Internationalization
Source strings are English and every user-facing string is wrapped in `tr()`
(in code) or is a plain `<string>` element in the `.ui` file (which `uic`
wraps in `QCoreApplication::translate`).
Each subproject ships its own empty placeholder `.ts` file under its
`i18n/` directory and registers it with `qt6_add_translations()`:
* `qwfassoc/i18n/qwfassoc_zh_CN.ts` — covers the library widgets.
* `qwfassoc-standalone/i18n/qwfassoc-standalone_zh_CN.ts` — covers the
executable-specific messages (CLI errors, tab titles, dialog window
title, etc.).
At runtime, `installTranslators()` in `qwfassoc-standalone/src/main.cpp`
loads both `.qm` files for the user's preferred UI language from the
`:/i18n/` resource prefix. Translators are expected to fill in the `.ts`
files; no actual translation work is performed by the build on its own.
## Notes and Limitations
* "Self" detection in the file-association table is based on comparing the
display name returned by wfassoc with the display name this program would
use. Two programs sharing the exact same display name could therefore be
confused.
* The system column in `AssociationWidget` is rendered disabled (using
`Qt::ItemIsSelectable` without `Qt::ItemIsEnabled`) when `TargetScope` is
`User`; the cells stay visible but cannot be clicked.
* All errors originating from wfassoc are surfaced through `QMessageBox`
dialogs; fatal errors during startup cause the process to exit with a
non-zero status code.
File diff suppressed because it is too large Load Diff
+17 -3
View File
@@ -1,5 +1,19 @@
# cmake 模块说明
# qwfassoc/cmake
此目录下的 `Findwfassoc.cmake` 是从项目根目录 `wfassoc-cdylib/codegen/Findwfassoc.cmake` 复制而来。
This directory holds CMake helper modules used by the `qwfassoc` project.
该文件提供 `wfassoc::wfassoc` imported target。使用前需要设置 `wfassoc_ROOT` 变量指向 wfassoc 安装目录。
## `Findwfassoc.cmake`
This file is a verbatim copy of the upstream `Findwfassoc.cmake` shipped with
the wfassoc C dynamic library, located at:
```
wfassoc-cdylib/cbindgen/Findwfassoc.cmake
```
The copy is committed here so that `qwfassoc` can locate the wfassoc library
through a standard `find_package(wfassoc)` call without depending on the source
tree layout at configure time.
To keep this copy in sync with the upstream version, re-run the copy command
shown above whenever `wfassoc-cdylib/cbindgen/Findwfassoc.cmake` is updated.
@@ -0,0 +1,48 @@
# qwfassoc-standalone: executable that uses the qwfassoc library to reproduce
# the original tabbed wfassoc configurator.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(QWFASSOC_STANDALONE_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/manifest_parser.cpp"
)
set(QWFASSOC_STANDALONE_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/manifest_parser.h"
)
set(QWFASSOC_STANDALONE_UI
"${CMAKE_CURRENT_SOURCE_DIR}/src/main_window.ui"
)
add_executable(qwfassoc-standalone WIN32
${QWFASSOC_STANDALONE_SOURCES}
${QWFASSOC_STANDALONE_HEADERS}
${QWFASSOC_STANDALONE_UI}
)
target_include_directories(qwfassoc-standalone PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_link_libraries(qwfassoc-standalone PRIVATE
qwfassoc
Qt6::Widgets
toml11::toml11
)
# Translation pipeline for the standalone executable. The library's strings
# are translated by qwfassoc's own .ts file; this one only covers the
# executable-specific messages (CLI errors, tab titles, etc.).
set(QWFASSOC_STANDALONE_TS_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/i18n/qwfassoc-standalone_zh_CN.ts"
)
qt6_add_translations(qwfassoc-standalone
TS_FILES ${QWFASSOC_STANDALONE_TS_FILES}
)
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
</TS>
@@ -0,0 +1,156 @@
#include <QApplication>
#include <QCommandLineOption>
#include <QCommandLineParser>
#include <QLocale>
#include <QMessageBox>
#include <QString>
#include <QStringList>
#include <QTranslator>
#include <stdexcept>
#include <string>
#include <wfassoc++.h>
#include "main_window.h"
#include "manifest_parser.h"
#include "scope.h"
namespace {
// Context used for translatable strings that live outside of any QObject.
constexpr const char* kTranslationContext = "qwfassoc-standalone";
// Show a modal error dialog with the given message and return a non-zero
// exit code. Used for the various fatal conditions that may occur before the
// main dialog can be shown.
int fatal(QWidget* parent, const QString& message) {
QMessageBox::critical(parent, QApplication::applicationName(), message);
return 1;
}
// Convert the --for command line value to a TargetScope. Throws if the value
// is not one of the accepted strings.
qwfassoc::TargetScope parseScope(const QString& value) {
const QString normalized = value.trimmed().toLower();
if (normalized == QStringLiteral("user")) {
return qwfassoc::TargetScope::User;
}
if (normalized == QStringLiteral("system")) {
return qwfassoc::TargetScope::System;
}
throw std::runtime_error(
"Invalid value for --for. Use \"user\" or \"system\".");
}
// Install the translation(s) matching the user's preferred UI language, if
// any. Both the qwfassoc library's .qm and this executable's .qm are loaded
// (their .ts files live under the per-project i18n/ directories and are
// embedded under the ":/i18n" resource prefix by qt6_add_translations).
void installTranslators(QApplication& app) {
const QStringList uiLanguages = QLocale::system().uiLanguages();
for (const QString& locale : uiLanguages) {
const QString name = QLocale(locale).name();
QTranslator* libTranslator = new QTranslator(&app);
if (libTranslator->load(QStringLiteral(":/i18n/qwfassoc_") + name)) {
app.installTranslator(libTranslator);
}
QTranslator* appTranslator = new QTranslator(&app);
if (appTranslator->load(
QStringLiteral(":/i18n/qwfassoc-standalone_") + name)) {
app.installTranslator(appTranslator);
}
}
}
} // namespace
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QApplication::setApplicationName(QStringLiteral("qwfassoc-standalone"));
// Install available translations before any translatable string is
// resolved so that tr() and QCoreApplication::translate() pick up the
// right language.
installTranslators(app);
QApplication::setApplicationDisplayName(
QCoreApplication::translate(kTranslationContext,
"qwfassoc - wfassoc Configurator"));
// Parse command line arguments using Qt's built-in parser.
QCommandLineParser parser;
parser.setApplicationDescription(
QCoreApplication::translate(
kTranslationContext,
"Qt-based GUI executable for the wfassoc library."));
parser.addHelpOption();
QCommandLineOption manifestOption(
QStringList() << QStringLiteral("c") << QStringLiteral("manifest"),
QCoreApplication::translate(kTranslationContext,
"Path to the application manifest TOML file."),
QStringLiteral("manifest"));
QCommandLineOption forOption(
QStringList() << QStringLiteral("f") << QStringLiteral("for"),
QCoreApplication::translate(kTranslationContext,
"Target scope: \"user\" or \"system\"."),
QStringLiteral("scope"));
parser.addOption(manifestOption);
parser.addOption(forOption);
parser.process(app);
// Validate that both mandatory options were provided with sane values.
const QString manifestPath = parser.value(manifestOption);
const QString forValue = parser.value(forOption);
if (manifestPath.isEmpty()) {
return fatal(nullptr,
QCoreApplication::translate(
kTranslationContext,
"The --manifest/-c option is required."));
}
if (forValue.isEmpty()) {
return fatal(nullptr,
QCoreApplication::translate(
kTranslationContext,
"The --for/-f option is required."));
}
qwfassoc::TargetScope scope;
try {
scope = parseScope(forValue);
} catch (const std::exception& e) {
return fatal(nullptr, QString::fromUtf8(e.what()));
}
// Initialize the wfassoc runtime. WFStartup must run before most other
// wfassoc calls; if it fails we cannot proceed.
if (!wfassoc::WFStartup()) {
return fatal(nullptr,
QString::fromUtf8(wfassoc::WFGetLastError()));
}
// Build the manifest -> schema -> program pipeline and run the dialog.
// Program construction consumes the schema (move) and performs the deep
// validation (identifier format, dangling references, etc.).
int exitCode = 0;
try {
qwfassoc::Manifest manifest =
qwfassoc::parseManifestFile(manifestPath.toStdString());
wfassocpp::Schema schema = qwfassoc::buildSchema(manifest);
wfassocpp::Program program(std::move(schema));
qwfassoc::MainWindow window(std::move(program), scope);
exitCode = window.exec();
} catch (const std::exception& e) {
wfassoc::WFShutdown();
return fatal(nullptr, QString::fromUtf8(e.what()));
}
wfassoc::WFShutdown();
return exitCode;
}
@@ -0,0 +1,79 @@
#include "main_window.h"
#include "ui_main_window.h"
#include "application_widget.h"
#include "association_widget.h"
#include "icon_utils.h"
#include <QTabWidget>
namespace qwfassoc {
MainWindow::MainWindow(wfassocpp::Program program,
TargetScope scope,
QWidget* parent)
: QDialog(parent),
ui_(new Ui::MainWindow),
appTab_(nullptr),
assocTab_(nullptr),
program_(std::move(program)),
scope_(scope) {
ui_->setupUi(this);
// Resolve program metadata that several labels depend on.
programName_ = QString::fromUtf8(program_.ResolveName());
{
auto iconRc = program_.ResolveIcon();
auto handle = iconRc.GetIcon();
programIcon_ = icon_utils::fromHicon(handle);
}
// Compose the window title and icon.
setWindowTitle(tr("%1 Options").arg(programName_));
if (!programIcon_.isNull()) {
setWindowIcon(QIcon(programIcon_));
}
// Build the two tab pages from the library widgets.
appTab_ = new ApplicationWidget(this);
assocTab_ = new AssociationWidget(this);
ui_->tabWidget->addTab(appTab_, tr("Applications"));
ui_->tabWidget->addTab(assocTab_, tr("File Associations"));
// Two-phase initialization. The standalone executable wants the OK and
// Cancel buttons visible because they drive dialog acceptance.
appTab_->setConfig({&program_, scope_});
assocTab_->setConfig({&program_, scope_, /*showOkCancelButtons=*/true});
// Wire widget signals so that any wfassoc change refreshes both pages,
// and the association widget can request dialog closure.
connect(appTab_, &ApplicationWidget::changed, this,
&MainWindow::onAnyChanged);
connect(assocTab_, &AssociationWidget::changed, this,
&MainWindow::onAnyChanged);
connect(assocTab_, &AssociationWidget::finished, this,
&MainWindow::onFinished);
}
MainWindow::~MainWindow() = default;
void MainWindow::onAnyChanged() {
if (appTab_ != nullptr) {
appTab_->refresh();
}
if (assocTab_ != nullptr) {
assocTab_->refresh();
}
}
void MainWindow::onFinished(bool accepted) {
if (accepted) {
accept();
} else {
reject();
}
}
} // namespace qwfassoc
@@ -0,0 +1,57 @@
#pragma once
#ifndef QWFASSOC_STANDALONE_MAIN_WINDOW_H_
#define QWFASSOC_STANDALONE_MAIN_WINDOW_H_
#include <QDialog>
#include <QPixmap>
#include <QString>
#include <wfassoc++.h>
#include "scope.h"
namespace Ui {
class MainWindow;
}
namespace qwfassoc {
class ApplicationWidget;
class AssociationWidget;
}
namespace qwfassoc {
// Top-level dialog used by the qwfassoc-standalone executable. Hosts the two
// reusable widgets from the qwfassoc library inside a QTabWidget and wires
// their changed() / finished() signals together.
class MainWindow : public QDialog {
Q_OBJECT
public:
explicit MainWindow(wfassocpp::Program program,
TargetScope scope,
QWidget* parent = nullptr);
~MainWindow() override;
private slots:
// Called whenever one of the embedded widgets reports that wfassoc state
// has changed. Refreshes both widgets so they stay in sync.
void onAnyChanged();
// Called when the association widget asks the dialog to close.
void onFinished(bool accepted);
private:
Ui::MainWindow* ui_;
ApplicationWidget* appTab_;
AssociationWidget* assocTab_;
wfassocpp::Program program_;
TargetScope scope_;
QString programName_;
QPixmap programIcon_;
};
} // namespace qwfassoc
#endif // QWFASSOC_STANDALONE_MAIN_WINDOW_H_
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QDialog" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>600</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>480</width>
<height>600</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>480</width>
<height>600</height>
</size>
</property>
<property name="windowTitle">
<string>Options</string>
</property>
<layout class="QVBoxLayout" name="mainLayout">
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,144 @@
#include "manifest_parser.h"
#include <stdexcept>
#include <toml.hpp>
namespace qwfassoc {
// region: TOML Parsing
Manifest parseManifestFile(const std::string& path) {
toml::value root;
try {
root = toml::parse(path);
} catch (const std::exception& e) {
// toml::parse already produces a descriptive message including file
// path and line number; just propagate it wrapped for context.
throw std::runtime_error(
std::string("Failed to parse manifest TOML file: ") + e.what());
}
Manifest manifest;
// Helper lambda: read a string field, re-throwing with a clearer message.
auto readString = [](const toml::value& v,
const std::string& key) -> std::string {
try {
return toml::find<std::string>(v, key);
} catch (const std::exception& e) {
throw std::runtime_error(
"Manifest field \"" + key +
"\" is missing or is not a string: " + e.what());
}
};
// Required top-level scalar fields.
manifest.identifier = readString(root, "identifier");
manifest.path = readString(root, "path");
manifest.clsid = readString(root, "clsid");
// Optional top-level scalar fields.
if (root.contains("name")) {
manifest.name = readString(root, "name");
}
if (root.contains("icon")) {
manifest.icon = readString(root, "icon");
}
if (root.contains("behavior")) {
manifest.behavior = readString(root, "behavior");
}
// Helper lambda: copy a TOML table of {string -> string} into a std::map.
auto readStringTable =
[](const toml::value& parent,
const std::string& key) -> std::map<std::string, std::string> {
if (!parent.contains(key)) {
return {};
}
std::map<std::string, std::string> out;
try {
// Keep the sub-value alive as a local so that the table reference
// obtained from as_table() stays valid for the loop below,
// regardless of whether toml::find returns by reference or by
// value in the toml11 version that is linked.
const toml::value& sub = toml::find(parent, key);
for (const auto& [k, v] : sub.as_table()) {
out.emplace(k, v.as_string());
}
} catch (const std::exception& e) {
throw std::runtime_error(
"Manifest table \"" + key +
"\" contains an invalid entry: " + e.what());
}
return out;
};
manifest.strs = readStringTable(root, "strs");
manifest.icons = readStringTable(root, "icons");
manifest.behaviors = readStringTable(root, "behaviors");
// Extension table. Each entry is itself a table with name/icon/behavior.
if (root.contains("exts")) {
try {
const toml::value& exts_value = toml::find(root, "exts");
for (const auto& [ext_key, ext_value] : exts_value.as_table()) {
ManifestExt ext;
ext.name = readString(ext_value, "name");
ext.icon = readString(ext_value, "icon");
ext.behavior = readString(ext_value, "behavior");
manifest.exts.emplace(ext_key, std::move(ext));
}
} catch (const std::exception& e) {
throw std::runtime_error(
"Manifest \"exts\" table contains an invalid entry: " +
e.what());
}
}
return manifest;
}
// endregion
// region: Schema Conversion
wfassocpp::Schema buildSchema(const Manifest& manifest) {
wfassocpp::Schema schema;
// The wfassocpp wrappers translate any underlying failure into a
// std::runtime_error via _Check, so we let those propagate untouched.
schema.SetIdentifier(manifest.identifier.c_str());
schema.SetPath(manifest.path.c_str());
schema.SetClsid(manifest.clsid.c_str());
// Optional fields: passing nullptr tells wfassoc to clear the value.
schema.SetName(manifest.name.has_value() ? manifest.name->c_str()
: nullptr);
schema.SetIcon(manifest.icon.has_value() ? manifest.icon->c_str()
: nullptr);
schema.SetBehavior(manifest.behavior.has_value() ? manifest.behavior->c_str()
: nullptr);
for (const auto& [key, value] : manifest.strs) {
schema.AddStr(key.c_str(), value.c_str());
}
for (const auto& [key, value] : manifest.icons) {
schema.AddIcon(key.c_str(), value.c_str());
}
for (const auto& [key, value] : manifest.behaviors) {
schema.AddBehavior(key.c_str(), value.c_str());
}
for (const auto& [key, value] : manifest.exts) {
schema.AddExt(key.c_str(),
value.name.c_str(),
value.icon.c_str(),
value.behavior.c_str());
}
return schema;
}
// endregion
} // namespace qwfassoc
@@ -0,0 +1,25 @@
#pragma once
#ifndef QWFASSOC_STANDALONE_MANIFEST_PARSER_H_
#define QWFASSOC_STANDALONE_MANIFEST_PARSER_H_
#include <string>
#include <wfassoc++.h>
#include "manifest.h"
namespace qwfassoc {
// Parse a manifest TOML file from disk into a Manifest value.
// Throws std::runtime_error on any IO or TOML syntax error.
Manifest parseManifestFile(const std::string& path);
// Build a wfassocpp::Schema from a manifest value.
// Throws std::runtime_error (originating from wfassocpp::_Check) when the
// wfassoc library rejects an operation, e.g. on duplicate keys or dangling
// references.
wfassocpp::Schema buildSchema(const Manifest& manifest);
} // namespace qwfassoc
#endif // QWFASSOC_STANDALONE_MANIFEST_PARSER_H_
+58
View File
@@ -0,0 +1,58 @@
# qwfassoc: shared library exporting reusable Qt widgets that wrap wfassoc.
# Qt 6 requires C++17 at minimum.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(QWFASSOC_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/src/application_widget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/association_widget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/icon_utils.cpp"
)
set(QWFASSOC_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/src/qwfassoc_global.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/scope.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/manifest.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/application_widget.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/association_widget.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/icon_utils.h"
)
set(QWFASSOC_UI
"${CMAKE_CURRENT_SOURCE_DIR}/src/application_widget.ui"
"${CMAKE_CURRENT_SOURCE_DIR}/src/association_widget.ui"
)
add_library(qwfassoc SHARED
${QWFASSOC_SOURCES}
${QWFASSOC_HEADERS}
${QWFASSOC_UI}
)
# QWFASSOC_LIBRARY switches QWFASSOC_EXPORT from import to export mode.
target_compile_definitions(qwfassoc PRIVATE QWFASSOC_LIBRARY)
# Consumers (and the library itself) need to find the public headers under
# src/. PUBLIC propagates the include path to anyone linking against qwfassoc.
target_include_directories(qwfassoc PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_link_libraries(qwfassoc PUBLIC
Qt6::Widgets
wfassoc::wfassoc
)
# Translation pipeline for the library. qt6_add_translations() runs lupdate
# against the target's sources and embeds the lrelease output under the
# ":/i18n" resource prefix, where installTranslators() in the executable
# looks it up at runtime.
set(QWFASSOC_TS_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/i18n/qwfassoc_zh_CN.ts"
)
qt6_add_translations(qwfassoc
TS_FILES ${QWFASSOC_TS_FILES}
)
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
</TS>
@@ -0,0 +1,119 @@
#include "application_widget.h"
#include "ui_application_widget.h"
#include "icon_utils.h"
#include <QMessageBox>
#include <QPushButton>
#include <stdexcept>
namespace qwfassoc {
namespace {
// Convert the library's TargetScope enum to the wfassocpp::Scope value used by
// the program APIs (register / unregister / link / unlink / is_registered).
wfassocpp::Scope toWfassocScope(TargetScope scope) {
return scope == TargetScope::User ? wfassocpp::Scope::User
: wfassocpp::Scope::System;
}
} // namespace
ApplicationWidget::ApplicationWidget(QWidget* parent)
: QWidget(parent), ui_(new Ui::ApplicationWidget) {
ui_->setupUi(this);
connect(ui_->installButton, &QPushButton::clicked, this,
&ApplicationWidget::onInstallClicked);
connect(ui_->uninstallButton, &QPushButton::clicked, this,
&ApplicationWidget::onUninstallClicked);
// Until setConfig() is called we have no program to operate on; keep the
// whole widget disabled.
setEnabled(false);
}
ApplicationWidget::~ApplicationWidget() = default;
void ApplicationWidget::setConfig(const Config& config) {
program_ = config.program;
scope_ = config.scope;
if (program_ != nullptr) {
// Resolve program metadata that several labels depend on. These calls
// may throw std::runtime_error on failure; the caller is expected to
// wrap setConfig() in a try/catch and present an error dialog.
programName_ = QString::fromUtf8(program_->ResolveName());
auto iconRc = program_->ResolveIcon();
auto handle = iconRc.GetIcon();
programIcon_ = icon_utils::fromHicon(handle);
if (!programIcon_.isNull()) {
ui_->appIconLabel->setPixmap(
programIcon_.scaled(32, 32, Qt::KeepAspectRatio,
Qt::SmoothTransformation));
}
ui_->appDescLabel->setText(
tr("Install or uninstall %1 here.").arg(programName_));
setEnabled(true);
refresh();
} else {
programName_.clear();
programIcon_ = QPixmap();
ui_->appIconLabel->setPixmap(QPixmap());
ui_->appIconLabel->setText(QString());
ui_->appDescLabel->setText(QString());
setEnabled(false);
}
}
void ApplicationWidget::refresh() {
if (program_ == nullptr) {
ui_->installButton->setEnabled(false);
ui_->uninstallButton->setEnabled(false);
return;
}
const bool registered = program_->IsRegistered(toWfassocScope(scope_));
ui_->installButton->setEnabled(!registered);
ui_->uninstallButton->setEnabled(registered);
}
void ApplicationWidget::onInstallClicked() {
if (program_ == nullptr) {
return;
}
try {
program_->Register(toWfassocScope(scope_));
} catch (const std::exception& e) {
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
return;
}
QMessageBox::information(this, tr("Information"),
tr("Application installed successfully."));
refresh();
emit changed();
}
void ApplicationWidget::onUninstallClicked() {
if (program_ == nullptr) {
return;
}
try {
program_->Unregister(toWfassocScope(scope_));
} catch (const std::exception& e) {
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
return;
}
QMessageBox::information(this, tr("Information"),
tr("Application uninstalled successfully."));
refresh();
emit changed();
}
} // namespace qwfassoc
@@ -0,0 +1,74 @@
#pragma once
#ifndef QWFASSOC_APPLICATION_WIDGET_H_
#define QWFASSOC_APPLICATION_WIDGET_H_
#include <QPixmap>
#include <QString>
#include <QWidget>
#include <wfassoc++.h>
#include "qwfassoc_global.h"
#include "scope.h"
namespace Ui {
class ApplicationWidget;
}
namespace qwfassoc {
// Widget exposing install / uninstall actions for a single wfassoc program.
//
// The widget follows the two-phase initialization pattern required by Qt
// Designer promoted widgets: the constructor only takes a parent, and the
// caller must invoke setConfig() with the target program and scope before the
// widget becomes usable. Until setConfig() is called the widget is disabled.
class QWFASSOC_EXPORT ApplicationWidget : public QWidget {
Q_OBJECT
public:
// Configuration bundle passed to setConfig().
struct Config {
// Non-owning pointer to the wfassoc program. Must outlive the widget.
wfassocpp::Program* program = nullptr;
// Scope that install/unregister operations apply to.
TargetScope scope = TargetScope::User;
};
explicit ApplicationWidget(QWidget* parent = nullptr);
~ApplicationWidget() override;
// Two-phase initialization. Calling this with a non-null program enables
// the widget and triggers an initial refresh. Calling it with a null
// program (or not calling it at all) leaves the widget disabled.
void setConfig(const Config& config);
// Re-query the live wfassoc state and update the enabled state of the
// install / uninstall buttons. Called automatically by setConfig() and
// also intended to be called by the host whenever another component has
// mutated wfassoc state.
void refresh();
signals:
// Emitted whenever the user performs an action that mutates wfassoc
// state (i.e. install or uninstall). The host should refresh every
// widget that depends on wfassoc state in response.
void changed();
private slots:
void onInstallClicked();
void onUninstallClicked();
private:
Ui::ApplicationWidget* ui_;
wfassocpp::Program* program_ = nullptr;
TargetScope scope_ = TargetScope::User;
// Cached metadata used to fill the widget labels.
QString programName_;
QPixmap programIcon_;
};
} // namespace qwfassoc
#endif // QWFASSOC_APPLICATION_WIDGET_H_
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ApplicationWidget</class>
<widget class="QWidget" name="ApplicationWidget">
<layout class="QVBoxLayout" name="mainLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Install and Uninstall</string>
</property>
<layout class="QVBoxLayout" name="groupLayout">
<item>
<layout class="QHBoxLayout" name="headerLayout">
<item>
<widget class="QLabel" name="appIconLabel">
<property name="text">
<string notr="true">[icon]</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="appDescLabel">
<property name="text">
<string>Install or uninstall the application here.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="actionLayout">
<item>
<widget class="QPushButton" name="installButton">
<property name="text">
<string>Install</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uninstallButton">
<property name="text">
<string>Uninstall</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,438 @@
#include "association_widget.h"
#include "ui_association_widget.h"
#include "icon_utils.h"
#include <QHeaderView>
#include <QMessageBox>
#include <QProcessEnvironment>
#include <QPushButton>
#include <QTableWidgetItem>
#include <stdexcept>
namespace qwfassoc {
namespace {
// Convert the library's TargetScope enum to the wfassocpp::Scope value used by
// the program APIs (register / unregister / link / unlink / is_registered).
wfassocpp::Scope toWfassocScope(TargetScope scope) {
return scope == TargetScope::User ? wfassocpp::Scope::User
: wfassocpp::Scope::System;
}
// Decide which CellState a queried ExtStatus corresponds to, given the
// resolved "self" name for this extension. We treat the cell as Self when the
// resolved display name matches our own; otherwise it is treated as Other.
detail::CellState classifyCell(const QString& selfName,
const QString& observedName) {
return observedName == selfName ? detail::CellState::Self
: detail::CellState::Other;
}
// Effective icon to draw for a cell, based on its state.
QPixmap effectiveCellIcon(const detail::ExtRow& row,
const detail::CellData& cell) {
using detail::CellState;
switch (cell.state) {
case CellState::Blank:
return QPixmap();
case CellState::Self:
return row.selfIcon;
case CellState::Other:
return cell.icon;
}
return QPixmap();
}
} // namespace
AssociationWidget::AssociationWidget(QWidget* parent)
: QWidget(parent), ui_(new Ui::AssociationWidget) {
ui_->setupUi(this);
// Fetch the current user name for the second column header. The USERNAME
// environment variable is good enough on Windows; fall back to a static
// translatable placeholder if it is unset for any reason.
userName_ = QProcessEnvironment::systemEnvironment().value(
QStringLiteral("USERNAME"), tr("User"));
// Give the table as much vertical room as possible inside its layout.
ui_->mainLayout->setStretch(2, 1);
// Reasonable default column widths so dotted extensions and ProgId names
// stay readable in the 480px dialog the widget typically lives in.
ui_->assocTable->setColumnWidth(0, 90);
ui_->assocTable->setColumnWidth(1, 175);
ui_->assocTable->setColumnWidth(2, 175);
ui_->assocTable->verticalHeader()->setVisible(false);
ui_->assocTable->setShowGrid(true);
connect(ui_->selectUserButton, &QPushButton::clicked, this,
&AssociationWidget::onSelectUserClicked);
connect(ui_->selectSystemButton, &QPushButton::clicked, this,
&AssociationWidget::onSelectSystemClicked);
connect(ui_->assocTable, &QTableWidget::cellClicked, this,
&AssociationWidget::onCellClicked);
connect(ui_->okButton, &QPushButton::clicked, this,
&AssociationWidget::onOkClicked);
connect(ui_->cancelButton, &QPushButton::clicked, this,
&AssociationWidget::onCancelClicked);
connect(ui_->applyButton, &QPushButton::clicked, this,
&AssociationWidget::onApplyClicked);
// Until setConfig() is called we have no program to operate on; keep the
// whole widget disabled.
setEnabled(false);
}
AssociationWidget::~AssociationWidget() = default;
void AssociationWidget::setConfig(const Config& config) {
program_ = config.program;
scope_ = config.scope;
showOkCancelButtons_ = config.showOkCancelButtons;
ui_->okButton->setVisible(showOkCancelButtons_);
ui_->cancelButton->setVisible(showOkCancelButtons_);
if (program_ != nullptr) {
programName_ = QString::fromUtf8(program_->ResolveName());
ui_->assocHeaderLabel->setText(
tr("File types associated with %1:").arg(programName_));
QStringList headers;
headers << tr("Type") << userName_ << tr("All Users");
ui_->assocTable->setHorizontalHeaderLabels(headers);
setEnabled(true);
refresh();
} else {
programName_.clear();
ui_->assocHeaderLabel->setText(QString());
ui_->assocTable->setRowCount(0);
rows_.clear();
setEnabled(false);
}
}
void AssociationWidget::refresh() {
if (program_ == nullptr) {
return;
}
updateEnabledState();
rebuildTable();
}
void AssociationWidget::updateEnabledState() {
if (program_ == nullptr) {
ui_->selectUserButton->setEnabled(false);
ui_->selectSystemButton->setEnabled(false);
ui_->assocTable->setEnabled(false);
ui_->applyButton->setEnabled(false);
return;
}
const bool registered = program_->IsRegistered(toWfassocScope(scope_));
const bool userColumnActive = registered;
const bool systemColumnActive = registered && isSystemColumnEnabled();
ui_->selectUserButton->setEnabled(userColumnActive);
ui_->selectSystemButton->setEnabled(systemColumnActive);
ui_->assocTable->setEnabled(registered);
// The Apply button enable state is driven by pending changes too; only
// touch it here to make sure it's disabled when nothing is registered.
if (!registered) {
ui_->applyButton->setEnabled(false);
}
}
void AssociationWidget::rebuildTable() {
if (program_ == nullptr) {
return;
}
refreshing_ = true;
rows_.clear();
const size_t count = program_->ExtsLen();
rows_.reserve(count);
ui_->assocTable->setRowCount(static_cast<int>(count));
for (size_t i = 0; i < count; ++i) {
detail::ExtRow row;
row.index = i;
// Self extension info: dotted body, display name and cached icon.
auto selfExt = program_->ResolveExt(i);
row.extBody = QString::fromUtf8(selfExt.GetExt());
row.dottedExt = QString::fromUtf8(selfExt.GetDottedExt());
row.selfName = QString::fromUtf8(selfExt.GetName());
row.selfIcon = icon_utils::fromHicon(selfExt.GetIcon());
// Query the user-view and system-view states. None means blank;
// a match against our self name means Self; anything else is Other
// and we keep the original name/icon around for display.
auto userStatus = program_->QueryExt(wfassocpp::View::User, i);
if (userStatus) {
const QString observedName =
QString::fromUtf8(userStatus->GetName());
row.initialUser.state = classifyCell(row.selfName, observedName);
row.initialUser.name = observedName;
row.initialUser.icon =
icon_utils::fromHicon(userStatus->GetIcon());
}
auto systemStatus = program_->QueryExt(wfassocpp::View::System, i);
if (systemStatus) {
const QString observedName =
QString::fromUtf8(systemStatus->GetName());
row.initialSystem.state =
classifyCell(row.selfName, observedName);
row.initialSystem.name = observedName;
row.initialSystem.icon =
icon_utils::fromHicon(systemStatus->GetIcon());
}
row.pendingUser = row.initialUser;
row.pendingSystem = row.initialSystem;
rows_.push_back(std::move(row));
// Create the QTableWidgetItem cells once; subsequent refreshes only
// update their text/icon and flags.
const int rowIdx = static_cast<int>(i);
auto* typeItem = new QTableWidgetItem(rows_.back().dottedExt);
ui_->assocTable->setItem(rowIdx, 0, typeItem);
auto* userItem = new QTableWidgetItem;
userItem->setTextAlignment(Qt::AlignCenter);
ui_->assocTable->setItem(rowIdx, 1, userItem);
auto* systemItem = new QTableWidgetItem;
systemItem->setTextAlignment(Qt::AlignCenter);
ui_->assocTable->setItem(rowIdx, 2, systemItem);
refreshRowDisplay(rowIdx);
}
refreshing_ = false;
updateApplyButtonEnabled();
}
void AssociationWidget::refreshRowDisplay(int row) {
using detail::CellState;
if (row < 0 || row >= static_cast<int>(rows_.size())) {
return;
}
const detail::ExtRow& r = rows_[row];
// Column 0: hybrid icon (user-preferred) + dotted extension.
QPixmap hybridIcon;
if (r.pendingUser.state != CellState::Blank) {
hybridIcon = effectiveCellIcon(r, r.pendingUser);
} else if (r.pendingSystem.state != CellState::Blank) {
hybridIcon = effectiveCellIcon(r, r.pendingSystem);
}
QTableWidgetItem* typeItem = ui_->assocTable->item(row, 0);
if (typeItem) {
typeItem->setIcon(QIcon(hybridIcon));
typeItem->setText(r.dottedExt);
}
// Column 1: user scope display name.
QTableWidgetItem* userItem = ui_->assocTable->item(row, 1);
if (userItem) {
userItem->setText(r.pendingUser.state == CellState::Blank
? QString()
: r.pendingUser.name);
}
// Column 2: system scope display name. When the system column is
// inactive (user-only run), the cells are flagged as disabled so that
// clicks are ignored and the rendering is greyed out.
QTableWidgetItem* systemItem = ui_->assocTable->item(row, 2);
if (systemItem) {
systemItem->setText(r.pendingSystem.state == CellState::Blank
? QString()
: r.pendingSystem.name);
const Qt::ItemFlags enabledFlags =
Qt::ItemIsEnabled | Qt::ItemIsSelectable;
// Without Qt::ItemIsEnabled the cell renders disabled (greyed out)
// and cellClicked is not emitted, so clicks are silently ignored.
const Qt::ItemFlags disabledFlags = Qt::ItemIsSelectable;
if (isSystemColumnEnabled()) {
systemItem->setFlags(enabledFlags);
} else {
systemItem->setFlags(disabledFlags);
}
}
}
void AssociationWidget::updateApplyButtonEnabled() {
bool dirty = false;
for (const detail::ExtRow& r : rows_) {
if (r.pendingUser.state != r.initialUser.state ||
r.pendingSystem.state != r.initialSystem.state) {
dirty = true;
break;
}
}
ui_->applyButton->setEnabled(dirty);
}
void AssociationWidget::toggleCell(int row, int column) {
using detail::CellState;
if (refreshing_) {
return;
}
if (row < 0 || row >= static_cast<int>(rows_.size())) {
return;
}
detail::CellData* cell = nullptr;
const detail::ExtRow* rowPtr = &rows_[row];
if (column == 1) {
cell = &rows_[row].pendingUser;
} else if (column == 2) {
if (!isSystemColumnEnabled()) {
return;
}
cell = &rows_[row].pendingSystem;
} else {
return;
}
// Toggle: Self -> Blank, anything else -> Self.
if (cell->state == CellState::Self) {
cell->state = CellState::Blank;
cell->name.clear();
cell->icon = QPixmap();
} else {
cell->state = CellState::Self;
cell->name = rowPtr->selfName;
cell->icon = rowPtr->selfIcon;
}
refreshRowDisplay(row);
updateApplyButtonEnabled();
}
void AssociationWidget::selectAllInScope(bool isUser) {
using detail::CellState;
if (!isUser && !isSystemColumnEnabled()) {
return;
}
// Progressively select more. If there is at least one blank cell, the
// first click only fills blanks; otherwise the click overrides cells
// pointing at other handlers as well.
bool hasBlank = false;
for (detail::ExtRow& r : rows_) {
const detail::CellData& cell = isUser ? r.pendingUser : r.pendingSystem;
if (cell.state == CellState::Blank) {
hasBlank = true;
break;
}
}
for (detail::ExtRow& r : rows_) {
detail::CellData& cell = isUser ? r.pendingUser : r.pendingSystem;
if (hasBlank) {
if (cell.state == CellState::Blank) {
cell.state = CellState::Self;
cell.name = r.selfName;
cell.icon = r.selfIcon;
}
} else if (cell.state != CellState::Self) {
cell.state = CellState::Self;
cell.name = r.selfName;
cell.icon = r.selfIcon;
}
}
for (size_t i = 0; i < rows_.size(); ++i) {
refreshRowDisplay(static_cast<int>(i));
}
updateApplyButtonEnabled();
}
void AssociationWidget::applyAllChanges() {
// Walk through every row and commit any cell whose pending state differs
// from the initial snapshot. wfassoc's link/unlink take an index rather
// than a scope/view, so we map columns back to (scope, index) pairs.
for (const detail::ExtRow& r : rows_) {
if (r.pendingUser.state != r.initialUser.state) {
if (r.pendingUser.state == detail::CellState::Self) {
program_->LinkExt(wfassocpp::Scope::User, r.index);
} else {
program_->UnlinkExt(wfassocpp::Scope::User, r.index);
}
}
if (r.pendingSystem.state != r.initialSystem.state) {
if (r.pendingSystem.state == detail::CellState::Self) {
program_->LinkExt(wfassocpp::Scope::System, r.index);
} else {
program_->UnlinkExt(wfassocpp::Scope::System, r.index);
}
}
}
// Re-query and rebuild the table so the UI reflects the live registry.
rebuildTable();
}
bool AssociationWidget::isSystemColumnEnabled() const {
return scope_ == TargetScope::System;
}
void AssociationWidget::onSelectUserClicked() {
selectAllInScope(/*isUser=*/true);
}
void AssociationWidget::onSelectSystemClicked() {
selectAllInScope(/*isUser=*/false);
}
void AssociationWidget::onCellClicked(int row, int column) {
toggleCell(row, column);
}
void AssociationWidget::onOkClicked() {
try {
applyAllChanges();
} catch (const std::exception& e) {
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
// Sync the table with the live registry, since some changes may have
// been committed before the failure.
refresh();
return;
}
emit changed();
emit finished(/*accepted=*/true);
}
void AssociationWidget::onCancelClicked() {
emit finished(/*accepted=*/false);
}
void AssociationWidget::onApplyClicked() {
try {
applyAllChanges();
} catch (const std::exception& e) {
QMessageBox::critical(this, tr("Error"), QString::fromUtf8(e.what()));
refresh();
return;
}
emit changed();
}
} // namespace qwfassoc
@@ -0,0 +1,160 @@
#pragma once
#ifndef QWFASSOC_ASSOCIATION_WIDGET_H_
#define QWFASSOC_ASSOCIATION_WIDGET_H_
#include <QPixmap>
#include <QString>
#include <QWidget>
#include <vector>
#include <wfassoc++.h>
#include "qwfassoc_global.h"
#include "scope.h"
namespace Ui {
class AssociationWidget;
}
namespace qwfassoc {
// Internal helper types used by AssociationWidget. They live in a `detail`
// namespace to signal that they are not part of the public API even though
// they need to be visible in the header.
namespace detail {
enum class CellState {
// The extension has no associated handler in this scope.
Blank,
// The extension is associated with the application described by the
// current manifest.
Self,
// The extension is associated with some other application.
Other,
};
// Per-cell cached data.
struct CellData {
CellState state = CellState::Blank;
// Display name. Only meaningful when state == Other, since the Self name
// is shared per row (it does not change between cells).
QString name;
// Display icon. Used for both Self and Other states.
QPixmap icon;
};
// All data attached to a single row in the file association table.
struct ExtRow {
// wfassoc extension index, used when calling LinkExt/UnlinkExt/QueryExt.
size_t index = 0;
// Dotted extension body, e.g. ".jpg". Displayed in column 0.
QString dottedExt;
// Body without leading dot, e.g. "jpg".
QString extBody;
// The display name when this program is the handler.
QString selfName;
// The icon when this program is the handler.
QPixmap selfIcon;
// Snapshot taken from the live registry at load/refresh time.
CellData initialUser;
CellData initialSystem;
// Working copy edited by the user; compared against initial* on Apply.
CellData pendingUser;
CellData pendingSystem;
};
} // namespace detail
// Widget showing the per-extension file-association status of a wfassoc
// program, and letting the user stage link / unlink operations.
//
// The widget follows the two-phase initialization pattern: the constructor
// only takes a parent, and the caller invokes setConfig() with the target
// program and scope before the widget becomes usable.
//
// By default the OK and Cancel buttons are hidden because they imply a
// dialog-level operation (close). Hosts that embed this widget in a dialog
// can enable them through Config::showOkCancelButtons and react to the
// finished() signal.
class QWFASSOC_EXPORT AssociationWidget : public QWidget {
Q_OBJECT
public:
struct Config {
// Non-owning pointer to the wfassoc program. Must outlive the widget.
wfassocpp::Program* program = nullptr;
// Scope that link / unlink operations apply to. Also controls whether
// the system column is interactive (only System scope unlocks it).
TargetScope scope = TargetScope::User;
// Whether the OK and Cancel buttons are visible. They are hidden by
// default since closing the host window is a host-level decision.
bool showOkCancelButtons = false;
};
explicit AssociationWidget(QWidget* parent = nullptr);
~AssociationWidget() override;
// Two-phase initialization.
void setConfig(const Config& config);
// Re-query the live wfassoc state and rebuild the table.
void refresh();
signals:
// Emitted after the user applies pending changes (OK or Apply). The host
// should refresh every widget that depends on wfassoc state in response.
void changed();
// Emitted when the widget wants its host window to close. `accepted` is
// true when the OK button was used (after the changes were applied and
// changed() was emitted) and false when the Cancel button was used.
void finished(bool accepted);
private slots:
void onSelectUserClicked();
void onSelectSystemClicked();
void onCellClicked(int row, int column);
void onOkClicked();
void onCancelClicked();
void onApplyClicked();
private:
// Refresh install / apply button enable state based on the live registry
// and the pending edits.
void updateEnabledState();
// Drop and rebuild the table contents from the live registry.
void rebuildTable();
// Refresh a single row's displayed cells from its pending state.
void refreshRowDisplay(int row);
// Update the "Apply" button enable state based on whether any pending
// cell differs from its initial state.
void updateApplyButtonEnabled();
// Toggle the pending state of a (row, column) cell. Column 1 maps to the
// user scope, column 2 to the system scope.
void toggleCell(int row, int column);
// Apply the "+" semantics to a given scope: blank cells become Self, and
// if there are none, all non-Self cells become Self.
void selectAllInScope(bool isUser);
// Commit every pending change to the registry via wfassoc.
void applyAllChanges();
// True when the system column should be interactive.
bool isSystemColumnEnabled() const;
Ui::AssociationWidget* ui_;
wfassocpp::Program* program_ = nullptr;
TargetScope scope_ = TargetScope::User;
bool showOkCancelButtons_ = false;
QString programName_;
QString userName_;
std::vector<detail::ExtRow> rows_;
// Re-entrancy guard used while rebuilding the table to avoid feeding
// model-change signals back into toggleCell().
bool refreshing_ = false;
};
} // namespace qwfassoc
#endif // QWFASSOC_ASSOCIATION_WIDGET_H_
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AssociationWidget</class>
<widget class="QWidget" name="AssociationWidget">
<layout class="QVBoxLayout" name="mainLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="assocHeaderLabel">
<property name="text">
<string>File types associated with this application:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="selectButtonsLayout">
<item>
<widget class="QPushButton" name="selectUserButton">
<property name="toolTip">
<string>Select all for current user</string>
</property>
<property name="text">
<string notr="true">+</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="selectSystemButton">
<property name="toolTip">
<string>Select all for all users</string>
</property>
<property name="text">
<string notr="true">+</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="selectButtonsSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="assocTable">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectItems</enum>
</property>
<column>
<property name="text">
<string>Type</string>
</property>
</column>
<column>
<property name="text">
<string>User</string>
</property>
</column>
<column>
<property name="text">
<string>All Users</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="bottomButtonsLayout">
<item>
<spacer name="bottomButtonsSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="okButton">
<property name="text">
<string>OK</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
<property name="visible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="visible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="applyButton">
<property name="text">
<string>Apply</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,23 @@
#include "icon_utils.h"
#include <QImage>
// QImage::fromHICON is only available on Windows and requires the real Win32
// HICON typedef to be visible. The qt_windows.h wrapper (shipped with Qt 6 on
// Windows) takes care of including <windows.h> in the order Qt expects, so it
// is included after all Qt headers.
#include <qt_windows.h>
namespace qwfassoc {
namespace icon_utils {
QPixmap fromHicon(wfassocpp::HICON handle) {
if (handle == wfassocpp::INVALID_HICON) {
return QPixmap();
}
QImage image = QImage::fromHICON(static_cast<HICON>(handle));
return QPixmap::fromImage(std::move(image));
}
} // namespace icon_utils
} // namespace qwfassoc
@@ -0,0 +1,26 @@
#pragma once
#ifndef QWFASSOC_ICON_UTILS_H_
#define QWFASSOC_ICON_UTILS_H_
#include <QPixmap>
#include <wfassoc++.h>
#include "qwfassoc_global.h"
namespace qwfassoc {
namespace icon_utils {
// Convert a wfassocpp::HICON handle (the C++ wrapper around wfassoc's opaque
// icon handle) into a QPixmap suitable for use in Qt widgets.
//
// The conversion goes through QImage::fromHICON() (available since Qt 6.0 on
// Windows) and then QPixmap::fromImage() using its rvalue-reference overload
// so that no extra pixel buffer copy is performed. The returned pixmap is null
// if the input handle is null.
QWFASSOC_EXPORT QPixmap fromHicon(wfassocpp::HICON handle);
} // namespace icon_utils
} // namespace qwfassoc
#endif // QWFASSOC_ICON_UTILS_H_
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#ifndef QWFASSOC_MANIFEST_H_
#define QWFASSOC_MANIFEST_H_
#include <map>
#include <optional>
#include <string>
namespace qwfassoc {
// Description of a single extension declared in the manifest.
//
// For a given extension (e.g. "jpg"), the manifest references one entry from
// each of the `strs`, `icons` and `behaviors` tables by its token name.
struct ManifestExt {
std::string name;
std::string icon;
std::string behavior;
};
// In-memory representation of a wfassoc manifest TOML file.
//
// This struct mirrors the Rust `Manifest` type defined in
// `wfassoc-exec/src/manifest.rs`. It is a plain data struct: parsing from
// TOML and conversion into a wfassocpp::Schema are intentionally kept out of
// the library so that the library itself does not depend on a TOML parser.
// Consumers (such as qwfassoc-standalone) are responsible for filling the
// fields in.
struct Manifest {
std::string identifier;
std::string path;
std::string clsid;
std::optional<std::string> name;
std::optional<std::string> icon;
std::optional<std::string> behavior;
std::map<std::string, std::string> strs;
std::map<std::string, std::string> icons;
std::map<std::string, std::string> behaviors;
std::map<std::string, ManifestExt> exts;
};
} // namespace qwfassoc
#endif // QWFASSOC_MANIFEST_H_
@@ -0,0 +1,20 @@
#pragma once
#ifndef QWFASSOC_GLOBAL_H_
#define QWFASSOC_GLOBAL_H_
#include <qglobal.h>
// Standard Qt shared-library export/import macros.
//
// When the qwfassoc library itself is being built, QWFASSOC_LIBRARY is
// defined (see the library's CMakeLists.txt) and QWFASSOC_EXPORT expands to
// Q_DECL_EXPORT so that symbols are exported from the .dll. Consumers of the
// library leave QWFASSOC_LIBRARY undefined, so QWFASSOC_EXPORT expands to
// Q_DECL_IMPORT and the same symbols are imported.
#if defined(QWFASSOC_LIBRARY)
# define QWFASSOC_EXPORT Q_DECL_EXPORT
#else
# define QWFASSOC_EXPORT Q_DECL_IMPORT
#endif
#endif // QWFASSOC_GLOBAL_H_
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#ifndef QWFASSOC_SCOPE_H_
#define QWFASSOC_SCOPE_H_
namespace qwfassoc {
// The target scope an application is being managed for.
//
// This value typically comes from a `--for user` / `--for system` command
// line argument and decides which columns of the file-association table are
// interactive, as well as which scope install / unregister operations apply
// to.
enum class TargetScope {
User,
System,
};
} // namespace qwfassoc
#endif // QWFASSOC_SCOPE_H_
+1 -1
View File
@@ -1,4 +1,4 @@
# Wfassoc C/C++ Library
# WFAssoc C/C++ Binding
For how to utilize this library, please see our example located in `example/ppic`. It is a Qt project built with CMake and demonstrate basically all usage of exposed functions in this dynamic library.
+413 -10
View File
@@ -17,40 +17,108 @@
#include <string>
#include <utility>
/**
* @brief Windows File Association C++ API namespace
*
* Provides C++ RAII wrappers around the C-compatible wfassoc API.
*/
namespace wfassocpp {
using wfassoc::CStyleString;
using wfassoc::Token;
using wfassoc::HICON;
using wfassoc::INVALID_HICON;
using wfassoc::INVALID_INDEX;
using wfassoc::Scope;
using wfassoc::View;
/** @brief Type representing a null-terminated UTF-8 C-style string */
using CString = wfassoc::WFCString;
/**
* @brief Type representing a handle/token for managed objects
*
* This library uses an object pool to manage any objects created during calling.
* This type is exposed as an opaque handle for visiting your created object.
*/
using Token = wfassoc::WFToken;
/**
* @brief Type representing an icon handle (opaque pointer)
*
* This type is equivalent to the Win32 HICON type.
*/
using HICON = wfassoc::WFHICON;
/** @brief Invalid icon handle value */
using INVALID_HICON = wfassoc::WF_INVALID_HICON;
/** @brief Invalid index value used for error conditions */
using INVALID_INDEX = wfassoc::WF_INVALID_INDEX;
/**
* @brief Registration scope for file associations
*
* Determines whether a program is registered for the current user or system-wide.
*/
using Scope = wfassoc::WFScope;
/**
* @brief View mode for querying file association status
*
* Determines how the association status is viewed/queried.
*/
using View = wfassoc::WFView;
/// @private
/**
* @private
* @brief Check the result of a C API call and throw on failure
*
* @param[in] result Boolean result from a C API call
* @throws std::runtime_error if result is false, with the error message from WFGetLastError()
*/
inline void _Check(bool result) {
if (!result) {
throw std::runtime_error(wfassoc::WFGetLastError());
}
}
/// @private
/**
* @private
* @brief Get the invalid token value
*
* In theory, invalid token value should be a constant value.
* However, due to the library used in Rust side, this value can only be fetched at runtime.
* This function caches the value on first call for subsequent use.
*
* @return An invalid token value
*/
inline Token _INVALID_TOKEN() {
static Token v = wfassoc::WFInvalidToken();
return v;
}
/**
* @brief Schema object for defining a program's file association configuration
*
* A Schema is a sketchpad of a complete program.
* Create a Schema object first, then convert it to a Program object for following operations.
*
* This class is move-only (non-copyable).
*/
class Schema {
public:
/**
* @brief Construct a new Schema object
* @throws std::runtime_error if schema creation fails
*/
Schema() { _Check(wfassoc::WFSchemaCreate(&_token)); }
/**
* @brief Destroy the Schema object
*
* Releases resources associated with the Schema object.
*/
~Schema() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFSchemaDestroy(_token);
}
}
/** @brief Non-copyable */
Schema(const Schema&) = delete;
/** @brief Non-copyable */
Schema& operator=(const Schema&) = delete;
/** @brief Move constructor */
Schema(Schema&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
/** @brief Move assignment operator */
Schema& operator=(Schema&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
@@ -62,41 +130,159 @@ public:
return *this;
}
/**
* @brief Set the program identifier for this Schema
*
* @param[in] value Null-terminated UTF-8 string containing the identifier.
* This identifier should not be empty, must start with alphabet character,
* and follow with alphabet characters, digits, underline, or hyphens.
* @throws std::runtime_error if the operation fails
*/
void SetIdentifier(const char* value) { _Check(wfassoc::WFSchemaSetIdentifier(_token, value)); }
/**
* @brief Set the program path for this Schema
*
* @param[in] value Null-terminated UTF-8 string containing the program path.
* This path should be the fully qualified path to the application.
* @throws std::runtime_error if the operation fails
*/
void SetPath(const char* value) { _Check(wfassoc::WFSchemaSetPath(_token, value)); }
/**
* @brief Set the program CLSID for this Schema
*
* @param[in] value Null-terminated UTF-8 string containing the CLSID.
* This CLSID string should be in the format of @c {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}.
* Please note that curly braces are required.
* @throws std::runtime_error if the operation fails
*/
void SetClsid(const char* value) { _Check(wfassoc::WFSchemaSetClsid(_token, value)); }
/**
* @brief Set the program name for this Schema (optional)
*
* @param[in] value Null-terminated UTF-8 string containing the name, or nullptr to clear
* @throws std::runtime_error if the operation fails
*/
void SetName(const char* value) { _Check(wfassoc::WFSchemaSetName(_token, value)); }
/**
* @brief Set the program icon for this Schema (optional)
*
* @param[in] value Null-terminated UTF-8 string containing the icon path, or nullptr to clear
* @throws std::runtime_error if the operation fails
*/
void SetIcon(const char* value) { _Check(wfassoc::WFSchemaSetIcon(_token, value)); }
/**
* @brief Set the program behavior for this Schema (optional)
*
* @param[in] value Null-terminated UTF-8 string containing the behavior command, or nullptr to clear
* @throws std::runtime_error if the operation fails
*/
void SetBehavior(const char* value) { _Check(wfassoc::WFSchemaSetBehavior(_token, value)); }
/**
* @brief Add a string resource entry to this Schema
*
* @param[in] name Null-terminated UTF-8 string containing the name of this entry
* @param[in] value Null-terminated UTF-8 string containing the value of this entry.
* It can be a plain string or a reference string to resource.
* @throws std::runtime_error if the operation fails
*/
void AddStr(const char* name, const char* value) { _Check(wfassoc::WFSchemaAddStr(_token, name, value)); }
/**
* @brief Add an icon registry entry to this Schema
*
* @param[in] name Null-terminated UTF-8 string containing the name of this entry
* @param[in] value Null-terminated UTF-8 string containing the value of this entry.
* It can be a path to icon or a reference string to resource.
* @throws std::runtime_error if the operation fails
*/
void AddIcon(const char* name, const char* value) { _Check(wfassoc::WFSchemaAddIcon(_token, name, value)); }
/**
* @brief Add a behavior registry entry to this Schema
*
* @param[in] name Null-terminated UTF-8 string containing the name of this entry
* @param[in] value Null-terminated UTF-8 string containing the value of this entry.
* It should be a valid command line string which uses @c %1, @c %2, etc. to represent parameters.
* @throws std::runtime_error if the operation fails
*/
void AddBehavior(const char* name, const char* value) { _Check(wfassoc::WFSchemaAddBehavior(_token, name, value)); }
/**
* @brief Add a file extension to this Schema
*
* @param[in] ext Null-terminated UTF-8 string containing the file extension name (without leading dot)
* @param[in] ext_name Null-terminated UTF-8 string containing the name pointing to associated name for this extension.
* This name should be registered by calling AddStr().
* @param[in] ext_icon Null-terminated UTF-8 string containing the name pointing to associated icon for this extension.
* This name should be registered by calling AddIcon().
* @param[in] ext_behavior Null-terminated UTF-8 string containing the name pointing to associated behavior for this extension.
* This name should be registered by calling AddBehavior().
* @throws std::runtime_error if the operation fails
*/
void AddExt(const char* ext, const char* ext_name, const char* ext_icon, const char* ext_behavior) {
_Check(wfassoc::WFSchemaAddExt(_token, ext, ext_name, ext_icon, ext_behavior));
}
private:
friend class Program;
/// @private
/**
* @private
* @brief Release ownership of the token
*
* @return The released token
*/
Token Release() noexcept {
Token t = _token;
_token = _INVALID_TOKEN();
return t;
}
Token _token;
};
/**
* @brief Icon resource object for managing icon handles
*
* This class wraps an icon resource token and provides access to the icon handle.
* The icon handle will be freed when this object is destroyed.
*
* This class is move-only (non-copyable).
*/
class IconRc {
public:
/**
* @brief Construct an IconRc from an existing token
*
* @param[in] token The icon resource token to wrap
*/
explicit IconRc(Token token) : _token(token) {}
/**
* @brief Destroy the IconRc object
*
* Releases resources associated with the icon resource.
*/
~IconRc() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFIconRcDestroy(_token);
}
}
/** @brief Non-copyable */
IconRc(const IconRc&) = delete;
/** @brief Non-copyable */
IconRc& operator=(const IconRc&) = delete;
/** @brief Move constructor */
IconRc(IconRc&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
/** @brief Move assignment operator */
IconRc& operator=(IconRc&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
@@ -108,6 +294,13 @@ public:
return *this;
}
/**
* @brief Get the icon handle from this icon resource
*
* @return The icon handle. This handle will be freed when this object is destroyed.
* Please make a copy immediately if you need to use it longer.
* @throws std::runtime_error if the operation fails
*/
HICON GetIcon() {
HICON icon = nullptr;
_Check(wfassoc::WFIconRcGetIcon(_token, &icon));
@@ -118,17 +311,42 @@ private:
Token _token;
};
/**
* @brief Extension status object for querying file extension registration status
*
* This class wraps an extension status token and provides access to extension details.
* The extension status object will be freed when this object is destroyed.
*
* This class is move-only (non-copyable).
*/
class ExtStatus {
public:
/**
* @brief Construct an ExtStatus from an existing token
*
* @param[in] token The extension status token to wrap
*/
explicit ExtStatus(Token token) : _token(token) {}
/**
* @brief Destroy the ExtStatus object
*
* Releases resources associated with the extension status.
*/
~ExtStatus() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFExtStatusDestroy(_token);
}
}
/** @brief Non-copyable */
ExtStatus(const ExtStatus&) = delete;
/** @brief Non-copyable */
ExtStatus& operator=(const ExtStatus&) = delete;
/** @brief Move constructor */
ExtStatus(ExtStatus&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
/** @brief Move assignment operator */
ExtStatus& operator=(ExtStatus&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
@@ -140,12 +358,30 @@ public:
return *this;
}
/**
* @brief Get the display name from this extension status
*
* The display name will be user specified first, then fallback to its ProgId verbatim.
* The library will try to use localized name first, then use raw ProgId name if localized name is not available.
*
* @return The display name string. There is no possibility that this value is empty.
* @throws std::runtime_error if the operation fails
*/
std::string GetName() {
const char* name = nullptr;
_Check(wfassoc::WFExtStatusGetName(_token, &name));
return std::string(name);
}
/**
* @brief Get the icon from this extension status
*
* The icon will be user specified first, then fallback to the system default file icon.
*
* @return The icon handle. This handle will be freed when this object is destroyed.
* Please make a copy immediately if you need to use it longer.
* @throws std::runtime_error if the operation fails
*/
HICON GetIcon() {
HICON icon = nullptr;
_Check(wfassoc::WFExtStatusGetIcon(_token, &icon));
@@ -156,17 +392,42 @@ private:
Token _token;
};
/**
* @brief Self extension status object for accessing program-provided extension details
*
* This class wraps a self extension status token and provides access to extension details
* as defined by the program itself.
*
* This class is move-only (non-copyable).
*/
class SelfExtStatus {
public:
/**
* @brief Construct a SelfExtStatus from an existing token
*
* @param[in] token The self extension status token to wrap
*/
explicit SelfExtStatus(Token token) : _token(token) {}
/**
* @brief Destroy the SelfExtStatus object
*
* Releases resources associated with the self extension status.
*/
~SelfExtStatus() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFSelfExtStatusDestroy(_token);
}
}
/** @brief Non-copyable */
SelfExtStatus(const SelfExtStatus&) = delete;
/** @brief Non-copyable */
SelfExtStatus& operator=(const SelfExtStatus&) = delete;
/** @brief Move constructor */
SelfExtStatus(SelfExtStatus&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
/** @brief Move assignment operator */
SelfExtStatus& operator=(SelfExtStatus&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
@@ -178,24 +439,53 @@ public:
return *this;
}
/**
* @brief Get the display name from this self extension status
*
* The display name will be user specified first, then fallback to its ProgId verbatim.
*
* @return The display name string. There is no possibility that this value is empty.
* @throws std::runtime_error if the operation fails
*/
std::string GetName() {
const char* name = nullptr;
_Check(wfassoc::WFSelfExtStatusGetName(_token, &name));
return std::string(name);
}
/**
* @brief Get the icon from this self extension status
*
* The icon will be user specified first, then fallback to the system default file icon.
*
* @return The icon handle. This handle will be freed when this object is destroyed.
* Please make a copy immediately if you need to use it longer.
* @throws std::runtime_error if the operation fails
*/
HICON GetIcon() {
HICON icon = nullptr;
_Check(wfassoc::WFSelfExtStatusGetIcon(_token, &icon));
return icon;
}
/**
* @brief Get the extension string (without leading dot) from this self extension status
*
* @return The file extension name (without leading dot). There is no possibility that this value is empty.
* @throws std::runtime_error if the operation fails
*/
std::string GetExt() {
const char* inner = nullptr;
_Check(wfassoc::WFSelfExtStatusGetExt(_token, &inner));
return std::string(inner);
}
/**
* @brief Get the dotted extension string (with leading dot) from this self extension status
*
* @return The file extension string (with leading dot). There is no possibility that this value is empty.
* @throws std::runtime_error if the operation fails
*/
std::string GetDottedExt() {
const char* inner = nullptr;
_Check(wfassoc::WFSelfExtStatusGetDottedExt(_token, &inner));
@@ -206,19 +496,48 @@ private:
Token _token;
};
/**
* @brief Program object for managing file association registration
*
* A Program is created from a Schema and provides methods for registering,
* unregistering, and querying file associations.
*
* This class is move-only (non-copyable).
*/
class Program {
public:
/**
* @brief Construct a Program from a Schema
*
* This constructor consumes the Schema object.
* The Schema object cannot be used after this call.
*
* @param[in] schema The Schema to create the Program from (will be consumed)
* @throws std::runtime_error if program creation fails
*/
explicit Program(Schema&& schema) {
_Check(wfassoc::WFProgramCreate(schema.Release(), &_token));
}
/**
* @brief Destroy the Program object
*
* Releases resources associated with the Program.
*/
~Program() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFProgramDestroy(_token);
}
}
/** @brief Non-copyable */
Program(const Program&) = delete;
/** @brief Non-copyable */
Program& operator=(const Program&) = delete;
/** @brief Move constructor */
Program(Program&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
/** @brief Move assignment operator */
Program& operator=(Program&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
@@ -230,48 +549,132 @@ public:
return *this;
}
/**
* @brief Resolve the provided program name of this Program
*
* The name will be user specified first,
* then fallback to program manifest file specified name,
* and finally fallback to the file name of executable.
*
* @return The resolved program name. There is no possibility that this value is empty.
* @throws std::runtime_error if the operation fails
*/
std::string ResolveName() {
const char* name = nullptr;
_Check(wfassoc::WFProgramResolveName(_token, &name));
return std::string(name);
}
/**
* @brief Resolve the Program icon resource
*
* The icon will be user specified first,
* then fallback to the first icon of program,
* and finally fallback to the system default executable icon.
*
* @return An IconRc object containing the resolved icon
* @throws std::runtime_error if the operation fails
*/
IconRc ResolveIcon() {
Token token = _INVALID_TOKEN();
_Check(wfassoc::WFProgramResolveIcon(_token, &token));
return IconRc(token);
}
/**
* @brief Get the number of file extensions in the Program
*
* @return The number of extensions
* @throws std::runtime_error if the operation fails
*/
size_t ExtsLen() {
size_t len = 0;
_Check(wfassoc::WFProgramExtsLen(_token, &len));
return len;
}
/**
* @brief Find a file extension by its body (extension string)
*
* @param[in] body Null-terminated UTF-8 string containing the file extension name (without leading dot) to find
* @return The index of the extension, or INVALID_INDEX if not found
* @throws std::runtime_error if the operation fails
*/
size_t FindExt(const char* body) {
size_t index = INVALID_INDEX;
_Check(wfassoc::WFProgramFindExt(_token, body, &index));
return index;
}
/**
* @brief Resolve this program provided extension's details by index
*
* @param[in] index Index of the extension to resolve
* @return A SelfExtStatus object containing the extension details
* @throws std::runtime_error if the operation fails
*/
SelfExtStatus ResolveExt(size_t index) {
Token token = _INVALID_TOKEN();
_Check(wfassoc::WFProgramResolveExt(_token, index, &token));
return SelfExtStatus(token);
}
/**
* @brief Register the Program in the specified scope
*
* @param[in] scope Registration scope
* @throws std::runtime_error if the operation fails
*/
void Register(Scope scope) { _Check(wfassoc::WFProgramRegister(_token, scope)); }
/**
* @brief Unregister the Program from the specified scope
*
* @param[in] scope Registration scope
* @throws std::runtime_error if the operation fails
*/
void Unregister(Scope scope) { _Check(wfassoc::WFProgramUnregister(_token, scope)); }
/**
* @brief Check if the Program is registered in the specified scope
*
* @param[in] scope Registration scope for checking
* @return true if the Program is registered in the specified scope, false otherwise
* @throws std::runtime_error if the operation fails
*/
bool IsRegistered(Scope scope) {
bool result = false;
_Check(wfassoc::WFProgramIsRegistered(_token, scope, &result));
return result;
}
/**
* @brief Link a file extension in the specified scope
*
* @param[in] scope Registration scope
* @param[in] index Index of the extension to link
* @throws std::runtime_error if the operation fails
*/
void LinkExt(Scope scope, size_t index) { _Check(wfassoc::WFProgramLinkExt(_token, scope, index)); }
/**
* @brief Unlink a file extension in the specified scope
*
* @param[in] scope Registration scope
* @param[in] index Index of the extension to unlink
* @throws std::runtime_error if the operation fails
*/
void UnlinkExt(Scope scope, size_t index) { _Check(wfassoc::WFProgramUnlinkExt(_token, scope, index)); }
/**
* @brief Query the status of a file extension
*
* @param[in] view View viewpoint
* @param[in] index Index of the extension to query
* @return An optional ExtStatus object. Returns std::nullopt if the extension is not found
* (usually means that this extension is not registered in the specified scope).
* @throws std::runtime_error if the operation fails
*/
std::optional<ExtStatus> QueryExt(View view, size_t index) {
Token token = _INVALID_TOKEN();
_Check(wfassoc::WFProgramQueryExt(_token, view, index, &token));
+68 -62
View File
@@ -22,41 +22,47 @@
#ifdef __cplusplus
/**
* @brief Windows File Association C/C++ API namespace
*
* This namespace only provide the raw binding to C-compatible wfassoc API in C++.
* For the C++ RAII wrappers of this, please see @ref wfassocpp namespace.
*/
namespace wfassoc {
#endif // __cplusplus
#ifdef __cplusplus
/** Type representing a null-terminated UTF-8 C-style string */
using CStyleString = const char*;
using WFCString = const char*;
/**
* @brief Type representing a handle/token for managed objects
*
* This library use object pool to manage any objects created during calling.
* And we expose this type as an opaque handle for visiting your created object.
*/
using Token = uint64_t;
using WFToken = uint64_t;
/**
* @brief Type representing an icon handle (opaque pointer)
*
* This type is equivalent with Win32 HICON type.
*/
using HICON = void*;
using WFHICON = void*;
#else // __cplusplus
typedef const char *CStyleString;
typedef uint64_t Token;
typedef void *HICON;
typedef const char *WFCString;
typedef uint64_t WFToken;
typedef void *WFHICON;
#endif // __cplusplus
#ifdef __cplusplus
/** Invalid icon handle value */
constexpr HICON INVALID_HICON = nullptr;
constexpr WFHICON WF_INVALID_HICON = nullptr;
/** Invalid index value used for error conditions */
constexpr size_t INVALID_INDEX = static_cast<size_t>(-1);
constexpr size_t WF_INVALID_INDEX = static_cast<size_t>(-1);
#else // __cplusplus
static const HICON INVALID_HICON = NULL;
static const size_t INVALID_INDEX = ((size_t)-1);
static const WFHICON WF_INVALID_HICON = NULL;
static const size_t WF_INVALID_INDEX = ((size_t)-1);
#endif // __cplusplus
@@ -66,7 +72,7 @@ static const size_t INVALID_INDEX = ((size_t)-1);
*
* Determines whether a program is registered for the current user or system-wide.
*/
enum class Scope : uint32_t {
enum class WFScope : uint32_t {
/** Current user scope */
User = 0u,
/** System-wide scope */
@@ -77,7 +83,7 @@ enum class Scope : uint32_t {
*
* Determines how the association status is viewed/queried.
*/
enum class View : uint32_t {
enum class WFView : uint32_t {
/** User-level view */
User = 0u,
/** System-level view */
@@ -86,18 +92,18 @@ enum class View : uint32_t {
Hybrid = 2u
};
#else // __cplusplus
typedef uint32_t Scope;
typedef uint32_t WFScope;
/** Current user scope */
static const Scope SCOPE_USER = 0u;
static const WFScope WF_SCOPE_USER = 0u;
/** System-wide scope */
static const Scope SCOPE_SYSTEM = 1u;
typedef uint32_t View;
static const WFScope WF_SCOPE_SYSTEM = 1u;
typedef uint32_t WFView;
/** User-level view */
static const View VIEW_USER = 0u;
static const WFView WF_VIEW_USER = 0u;
/** System-level view */
static const View VIEW_SYSTEM = 1u;
static const WFView WF_VIEW_SYSTEM = 1u;
/** Combined hybrid view of both user and system */
static const View VIEW_HYBRID = 2u;
static const WFView WF_VIEW_HYBRID = 2u;
#endif // __cplusplus
@@ -143,7 +149,7 @@ bool WFShutdown(void);
* If no error has occurred, the string is empty.
* There is no possibility of a NULL return value.
*/
CStyleString WFGetLastError(void);
WFCString WFGetLastError(void);
/**
* @brief Check if the current process has administrative privileges
@@ -170,7 +176,7 @@ bool WFHasPrivilege(void);
*
* @return An invalid token value
*/
Token WFInvalidToken(void);
WFToken WFInvalidWFToken(void);
/**
* @brief Create a new Schema object
@@ -185,7 +191,7 @@ Token WFInvalidToken(void);
* or consumed by creating a Program object via WFProgramCreate().
* @return true on success, false on failure
*/
bool WFSchemaCreate(Token *out_schema);
bool WFSchemaCreate(WFToken *out_schema);
/**
* @brief Destroy a Schema object
@@ -198,7 +204,7 @@ bool WFSchemaCreate(Token *out_schema);
* @param[in] in_schema Schema token to destroy
* @return true on success, false on failure
*/
bool WFSchemaDestroy(Token in_schema);
bool WFSchemaDestroy(WFToken in_schema);
/**
* @brief Set the program identifier for a Schema
@@ -209,7 +215,7 @@ bool WFSchemaDestroy(Token in_schema);
* and follow with alphabet characters, digits, underline, or hyphens.
* @return true on success, false on failure
*/
bool WFSchemaSetIdentifier(Token in_schema, CStyleString in_value);
bool WFSchemaSetIdentifier(WFToken in_schema, WFCString in_value);
/**
* @brief Set the program path for a Schema
@@ -219,7 +225,7 @@ bool WFSchemaSetIdentifier(Token in_schema, CStyleString in_value);
* This path should be the fully qualified path to the application.
* @return true on success, false on failure
*/
bool WFSchemaSetPath(Token in_schema, CStyleString in_value);
bool WFSchemaSetPath(WFToken in_schema, WFCString in_value);
/**
* @brief Set the program CLSID for a Schema
@@ -230,7 +236,7 @@ bool WFSchemaSetPath(Token in_schema, CStyleString in_value);
* Please note that curly braces are required.
* @return true on success, false on failure
*/
bool WFSchemaSetClsid(Token in_schema, CStyleString in_value);
bool WFSchemaSetClsid(WFToken in_schema, WFCString in_value);
/**
* @brief Set the program name for a Schema (optional)
@@ -239,7 +245,7 @@ bool WFSchemaSetClsid(Token in_schema, CStyleString in_value);
* @param[in] in_value Null-terminated UTF-8 string containing the name, or NULL to clear
* @return true on success, false on failure
*/
bool WFSchemaSetName(Token in_schema, CStyleString in_value);
bool WFSchemaSetName(WFToken in_schema, WFCString in_value);
/**
* @brief Set the program icon for a Schema (optional)
@@ -248,7 +254,7 @@ bool WFSchemaSetName(Token in_schema, CStyleString in_value);
* @param[in] in_value Null-terminated UTF-8 string containing the icon path, or NULL to clear
* @return true on success, false on failure
*/
bool WFSchemaSetIcon(Token in_schema, CStyleString in_value);
bool WFSchemaSetIcon(WFToken in_schema, WFCString in_value);
/**
* @brief Set the program behavior for a Schema (optional)
@@ -257,7 +263,7 @@ bool WFSchemaSetIcon(Token in_schema, CStyleString in_value);
* @param[in] in_value Null-terminated UTF-8 string containing the behavior command, or NULL to clear
* @return true on success, false on failure
*/
bool WFSchemaSetBehavior(Token in_schema, CStyleString in_value);
bool WFSchemaSetBehavior(WFToken in_schema, WFCString in_value);
/**
* @brief Add a string resource entry to a Schema
@@ -268,7 +274,7 @@ bool WFSchemaSetBehavior(Token in_schema, CStyleString in_value);
* It can be a plain string or a reference string to resource.
* @return true on success, false on failure
*/
bool WFSchemaAddStr(Token in_schema, CStyleString in_name, CStyleString in_value);
bool WFSchemaAddStr(WFToken in_schema, WFCString in_name, WFCString in_value);
/**
* @brief Add an icon registry entry to a Schema
@@ -279,7 +285,7 @@ bool WFSchemaAddStr(Token in_schema, CStyleString in_name, CStyleString in_value
* It can be a path to icon or a reference string to resource.
* @return true on success, false on failure
*/
bool WFSchemaAddIcon(Token in_schema, CStyleString in_name, CStyleString in_value);
bool WFSchemaAddIcon(WFToken in_schema, WFCString in_name, WFCString in_value);
/**
* @brief Add a behavior registry entry to a Schema
@@ -290,7 +296,7 @@ bool WFSchemaAddIcon(Token in_schema, CStyleString in_name, CStyleString in_valu
* It should be a valid command line string which use \c %1, \c %2, etc. to represent parameters.
* @return true on success, false on failure
*/
bool WFSchemaAddBehavior(Token in_schema, CStyleString in_name, CStyleString in_value);
bool WFSchemaAddBehavior(WFToken in_schema, WFCString in_name, WFCString in_value);
/**
* @brief Add a file extension to a Schema
@@ -305,11 +311,11 @@ bool WFSchemaAddBehavior(Token in_schema, CStyleString in_name, CStyleString in_
* This name should be registered by calling WFSchemaAddBehavior().
* @return true on success, false on failure
*/
bool WFSchemaAddExt(Token in_schema,
CStyleString in_ext,
CStyleString in_ext_name,
CStyleString in_ext_icon,
CStyleString in_ext_behavior);
bool WFSchemaAddExt(WFToken in_schema,
WFCString in_ext,
WFCString in_ext_name,
WFCString in_ext_icon,
WFCString in_ext_behavior);
/**
* @brief Create a Program object from a Schema
@@ -327,7 +333,7 @@ bool WFSchemaAddExt(Token in_schema,
* And it should be freed by calling WFProgramDestroy() when it is no longer needed.
* @return true on success, false on failure
*/
bool WFProgramCreate(Token in_schema, Token *out_program);
bool WFProgramCreate(WFToken in_schema, WFToken *out_program);
/**
* @brief Destroy a Program object
@@ -337,7 +343,7 @@ bool WFProgramCreate(Token in_schema, Token *out_program);
* @param[in] in_program Program token to destroy
* @return true on success, false on failure
*/
bool WFProgramDestroy(Token in_program);
bool WFProgramDestroy(WFToken in_program);
/**
* @brief Resolve the provided program name of this Program
@@ -352,7 +358,7 @@ bool WFProgramDestroy(Token in_program);
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
* @return true on success, false on failure
*/
bool WFProgramResolveName(Token in_program, CStyleString *out_name);
bool WFProgramResolveName(WFToken in_program, WFCString *out_name);
/**
* @brief Resolve the Program icon resource
@@ -367,7 +373,7 @@ bool WFProgramResolveName(Token in_program, CStyleString *out_name);
* And it should be freed by calling WFIconRcDestroy() when it is no longer needed.
* @return true on success, false on failure
*/
bool WFProgramResolveIcon(Token in_program, Token *out_icon_rc);
bool WFProgramResolveIcon(WFToken in_program, WFToken *out_icon_rc);
/**
* @brief Get the number of file extensions in the Program
@@ -376,17 +382,17 @@ bool WFProgramResolveIcon(Token in_program, Token *out_icon_rc);
* @param[out] out_len Pointer to receive the number of extensions
* @return true on success, false on failure
*/
bool WFProgramExtsLen(Token in_program, size_t *out_len);
bool WFProgramExtsLen(WFToken in_program, size_t *out_len);
/**
* @brief Find a file extension by its body (extension string)
*
* @param[in] in_program Program token
* @param[in] in_body Null-terminated UTF-8 string containing the file extension name (without leading dot) to find.
* @param[out] out_index Pointer to receive the file extension index, or INVALID_INDEX if not found.
* @param[out] out_index Pointer to receive the file extension index, or WF_INVALID_INDEX if not found.
* @return true on success, false on failure
*/
bool WFProgramFindExt(Token in_program, CStyleString in_body, size_t *out_index);
bool WFProgramFindExt(WFToken in_program, WFCString in_body, size_t *out_index);
/**
* @brief Resolve this program provided extension's details by index
@@ -398,7 +404,7 @@ bool WFProgramFindExt(Token in_program, CStyleString in_body, size_t *out_index)
* And it should be freed by calling WFSelfExtStatusDestroy() when it is no longer needed.
* @return true on success, false on failure
*/
bool WFProgramResolveExt(Token in_program, size_t in_index, Token *out_self_ext_status);
bool WFProgramResolveExt(WFToken in_program, size_t in_index, WFToken *out_self_ext_status);
/**
* @brief Register the Program in the specified scope
@@ -407,7 +413,7 @@ bool WFProgramResolveExt(Token in_program, size_t in_index, Token *out_self_ext_
* @param[in] in_scope Registration scope
* @return true on success, false on failure
*/
bool WFProgramRegister(Token in_program, Scope in_scope);
bool WFProgramRegister(WFToken in_program, WFScope in_scope);
/**
* @brief Unregister the Program from the specified scope
@@ -416,7 +422,7 @@ bool WFProgramRegister(Token in_program, Scope in_scope);
* @param[in] in_scope Registration scope
* @return true on success, false on failure
*/
bool WFProgramUnregister(Token in_program, Scope in_scope);
bool WFProgramUnregister(WFToken in_program, WFScope in_scope);
/**
* @brief Check if the Program is registered in the specified scope
@@ -427,7 +433,7 @@ bool WFProgramUnregister(Token in_program, Scope in_scope);
* True if the Program is registered in the specified scope, false otherwise.
* @return true on success, false on failure
*/
bool WFProgramIsRegistered(Token in_program, Scope in_scope, bool *out_is_registered);
bool WFProgramIsRegistered(WFToken in_program, WFScope in_scope, bool *out_is_registered);
/**
* @brief Link a file extension in the specified scope
@@ -437,7 +443,7 @@ bool WFProgramIsRegistered(Token in_program, Scope in_scope, bool *out_is_regist
* @param[in] in_index Index of the extension to link
* @return true on success, false on failure
*/
bool WFProgramLinkExt(Token in_program, Scope in_scope, size_t in_index);
bool WFProgramLinkExt(WFToken in_program, WFScope in_scope, size_t in_index);
/**
* @brief Unlink a file extension in the specified scope
@@ -447,7 +453,7 @@ bool WFProgramLinkExt(Token in_program, Scope in_scope, size_t in_index);
* @param[in] in_index Index of the extension to unlink
* @return true on success, false on failure
*/
bool WFProgramUnlinkExt(Token in_program, Scope in_scope, size_t in_index);
bool WFProgramUnlinkExt(WFToken in_program, WFScope in_scope, size_t in_index);
/**
* @brief Query the status of a file extension
@@ -461,7 +467,7 @@ bool WFProgramUnlinkExt(Token in_program, Scope in_scope, size_t in_index);
* And it should be freed by calling WFExtStatusDestroy() when it is no longer needed.
* @return true on success, false on failure
*/
bool WFProgramQueryExt(Token in_program, View in_view, size_t in_index, Token *out_ext_status);
bool WFProgramQueryExt(WFToken in_program, WFView in_view, size_t in_index, WFToken *out_ext_status);
/**
* @brief Destroy an extension status object
@@ -469,7 +475,7 @@ bool WFProgramQueryExt(Token in_program, View in_view, size_t in_index, Token *o
* @param[in] in_ext_status Extension status token to destroy
* @return true on success, false on failure
*/
bool WFExtStatusDestroy(Token in_ext_status);
bool WFExtStatusDestroy(WFToken in_ext_status);
/**
* @brief Get the display name from an extension status object
@@ -484,7 +490,7 @@ bool WFExtStatusDestroy(Token in_ext_status);
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
* @return true on success, false on failure
*/
bool WFExtStatusGetName(Token in_ext_status, CStyleString *out_name);
bool WFExtStatusGetName(WFToken in_ext_status, WFCString *out_name);
/**
* @brief Get the icon from an extension status object
@@ -498,7 +504,7 @@ bool WFExtStatusGetName(Token in_ext_status, CStyleString *out_name);
* Please make a copy immediately if you need to use it longer.
* @return true on success, false on failure
*/
bool WFExtStatusGetIcon(Token in_ext_status, HICON *out_icon);
bool WFExtStatusGetIcon(WFToken in_ext_status, WFHICON *out_icon);
/**
* @brief Destroy a self extension status object
@@ -506,7 +512,7 @@ bool WFExtStatusGetIcon(Token in_ext_status, HICON *out_icon);
* @param[in] in_self_ext_status Self extension status token to destroy
* @return true on success, false on failure
*/
bool WFSelfExtStatusDestroy(Token in_self_ext_status);
bool WFSelfExtStatusDestroy(WFToken in_self_ext_status);
/**
* @brief Get the display name from a self extension status object
@@ -520,7 +526,7 @@ bool WFSelfExtStatusDestroy(Token in_self_ext_status);
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
* @return true on success, false on failure
*/
bool WFSelfExtStatusGetName(Token in_self_ext_status, CStyleString *out_name);
bool WFSelfExtStatusGetName(WFToken in_self_ext_status, WFCString *out_name);
/**
* @brief Get the icon from a self extension status object
@@ -534,7 +540,7 @@ bool WFSelfExtStatusGetName(Token in_self_ext_status, CStyleString *out_name);
* Please make a copy immediately if you need to use it longer.
* @return true on success, false on failure
*/
bool WFSelfExtStatusGetIcon(Token in_self_ext_status, HICON *out_icon);
bool WFSelfExtStatusGetIcon(WFToken in_self_ext_status, WFHICON *out_icon);
/**
* @brief Get the extension string (without leading dot) from a self extension status object
@@ -545,7 +551,7 @@ bool WFSelfExtStatusGetIcon(Token in_self_ext_status, HICON *out_icon);
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
* @return true on success, false on failure
*/
bool WFSelfExtStatusGetExt(Token in_self_ext_status, CStyleString *out_inner);
bool WFSelfExtStatusGetExt(WFToken in_self_ext_status, WFCString *out_inner);
/**
* @brief Get the dotted extension string (with leading dot) from a self extension status object
@@ -556,7 +562,7 @@ bool WFSelfExtStatusGetExt(Token in_self_ext_status, CStyleString *out_inner);
* This string will be freed at the next API call. Please make a copy immediately if you need to use it longer.
* @return true on success, false on failure
*/
bool WFSelfExtStatusGetDottedExt(Token in_self_ext_status, CStyleString *out_inner);
bool WFSelfExtStatusGetDottedExt(WFToken in_self_ext_status, WFCString *out_inner);
/**
* @brief Destroy an icon resource object
@@ -564,19 +570,19 @@ bool WFSelfExtStatusGetDottedExt(Token in_self_ext_status, CStyleString *out_inn
* @param[in] in_icon_rc Icon resource token to destroy
* @return true on success, false on failure
*/
bool WFIconRcDestroy(Token in_icon_rc);
bool WFIconRcDestroy(WFToken in_icon_rc);
/**
* @brief Get the icon handle from an icon resource object
*
* @param[in] in_icon_rc Icon resource token
* @param[out] out_icon Pointer to receive the icon handle.
* There is no possibility that this value is INVALID_HICON.
* There is no possibility that this value is WF_INVALID_HICON.
* This icon handle will be freed once this icon resource object is destroyed.
* Please make a copy immediately if you need to use it longer.
* @return true on success, false on failure
*/
bool WFIconRcGetIcon(Token in_icon_rc, HICON *out_icon);
bool WFIconRcGetIcon(WFToken in_icon_rc, WFHICON *out_icon);
#ifdef __cplusplus
} // extern "C"
+2 -4
View File
@@ -9,11 +9,9 @@
//! The only thing that outer programs should note is that this string is volatile,
//! once they get it, they must dupliate it immediately before any futher calling to this dynamic library.
use std::cell::RefCell;
use std::ffi::{CStr, CString, c_char};
use std::ffi::{CStr, CString};
use thiserror::Error as TeError;
/// The type representing the raw pointer to immutable C-style NUL-terminated string.
pub type CStyleString = *const c_char;
use crate::ffi_types::CStyleString;
// region: Error
+8 -4
View File
@@ -1,11 +1,15 @@
//! The module including all FFI types used by this crate, except string type.
//! For string type, see also [crate::cstr_ffi].
use std::ffi::c_void;
use std::ffi::{c_void, c_char};
use num_enum::TryFromPrimitive;
// region: File Extension Index
// region: Misc Types and Constants
/// The type representing the raw pointer to immutable C-style NUL-terminated string.
pub type CStyleString = *const c_char;
/// The invalid value of index.
pub const INVALID_INDEX: usize = usize::MAX;
// endregion
@@ -18,12 +22,12 @@ pub const INVALID_INDEX: usize = usize::MAX;
/// However, I don't want to add it as this crate's dependency,
/// because I don't use anything within it except this type.
/// So I check Microsoft document, re-define it in there for this crate.
/// Reference: https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
/// Reference: <https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types>
pub type HICON = *mut c_void;
/// The invalid value of Win32 HICON handle.
///
/// The same reason like [HICON] to re-define it in there.
/// The same reason like [WFHICON] to re-define it in there.
pub const INVALID_HICON: HICON = std::ptr::null_mut();
// endregion
+121 -119
View File
@@ -14,17 +14,17 @@ use wfassoc::highlevel::{Program, Schema};
#[derive(Debug, TeError)]
enum Error {
/// Error when operating Schema.
#[error("{0}")]
#[error("wfassoc error: {0}")]
Schema(#[from] wfassoc::highlevel::SchemaError),
/// Error when parsing Schema into Program.
#[error("{0}")]
#[error("wfassoc error: {0}")]
ParseProgram(#[from] wfassoc::highlevel::ParseProgramError),
/// Error when operating Program.
#[error("{0}")]
#[error("wfassoc error: {0}")]
Program(#[from] wfassoc::highlevel::ProgramError),
/// Error when manipulating with C-style string.
#[error("C-Style string FFI error:{0}")]
#[error("C-Style string FFI error: {0}")]
CStrFfi(#[from] cstr_ffi::Error),
/// Error when manipulating with object pool.
#[error("object pool error: {0}")]
@@ -202,9 +202,11 @@ static ICON_RC_POOL: LazyLock<RwLock<ObjectPool<wfassoc::win32::concept::IconRc>
// region: Exposed Types
pub use cstr_ffi::CStyleString;
pub use ffi_types::{HICON, Scope, View};
pub use object_pool::Token;
pub use ffi_types::{
CStyleString as WFCString, HICON as WFHICON, Scope as WFScope, View as WFView,
};
pub use ffi_types::{INVALID_HICON as WF_INVALID_HICON, INVALID_INDEX as WF_INVALID_INDEX};
pub use object_pool::Token as WFToken;
// endregion
@@ -244,7 +246,7 @@ pub extern "C" fn WFShutdown() -> bool {
}
#[unsafe(no_mangle)]
pub extern "C" fn WFGetLastError() -> CStyleString {
pub extern "C" fn WFGetLastError() -> WFCString {
last_error::get_last_error()
}
@@ -254,7 +256,7 @@ pub extern "C" fn WFHasPrivilege() -> bool {
}
#[unsafe(no_mangle)]
pub extern "C" fn WFInvalidToken() -> Token {
pub extern "C" fn WFInvalidToken() -> WFToken {
object_pool::invalid_token()
}
@@ -263,16 +265,16 @@ pub extern "C" fn WFInvalidToken() -> Token {
// region: Schema
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaCreate(out_schema: out_param_ty!(Token)) -> bool {
cffi_wrapper!(|| -> (out_schema: Token) {
pub extern "C" fn WFSchemaCreate(out_schema: out_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|| -> (out_schema: WFToken) {
let mut pool = pull_writer!(SCHEMA_POOL)?;
Ok(pool.allocate(Schema::new())?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaDestroy(in_schema: in_param_ty!(Token)) -> bool {
cffi_wrapper!(|in_schema: Token| {
pub extern "C" fn WFSchemaDestroy(in_schema: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_schema: WFToken| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
Ok(pool.free(in_schema)?)
})
@@ -280,10 +282,10 @@ pub extern "C" fn WFSchemaDestroy(in_schema: in_param_ty!(Token)) -> bool {
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetIdentifier(
in_schema: in_param_ty!(Token),
in_value: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.set_identifier(cstr_ffi::parse_ffi_string(in_value)?);
@@ -293,10 +295,10 @@ pub extern "C" fn WFSchemaSetIdentifier(
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetPath(
in_schema: in_param_ty!(Token),
in_value: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.set_path(cstr_ffi::parse_ffi_string(in_value)?);
@@ -306,10 +308,10 @@ pub extern "C" fn WFSchemaSetPath(
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetClsid(
in_schema: in_param_ty!(Token),
in_value: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.set_clsid(cstr_ffi::parse_ffi_string(in_value)?);
@@ -319,10 +321,10 @@ pub extern "C" fn WFSchemaSetClsid(
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetName(
in_schema: in_param_ty!(Token),
in_value: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
@@ -338,10 +340,10 @@ pub extern "C" fn WFSchemaSetName(
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetIcon(
in_schema: in_param_ty!(Token),
in_value: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
@@ -357,10 +359,10 @@ pub extern "C" fn WFSchemaSetIcon(
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetBehavior(
in_schema: in_param_ty!(Token),
in_value: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
@@ -376,12 +378,12 @@ pub extern "C" fn WFSchemaSetBehavior(
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddStr(
in_schema: in_param_ty!(Token),
in_name: in_param_ty!(CStyleString),
in_value: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_name: in_param_ty!(WFCString),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(
|in_schema: Token, in_name: CStyleString, in_value: CStyleString| {
|in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.add_str(
@@ -395,12 +397,12 @@ pub extern "C" fn WFSchemaAddStr(
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddIcon(
in_schema: in_param_ty!(Token),
in_name: in_param_ty!(CStyleString),
in_value: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_name: in_param_ty!(WFCString),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(
|in_schema: Token, in_name: CStyleString, in_value: CStyleString| {
|in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.add_icon(
@@ -414,12 +416,12 @@ pub extern "C" fn WFSchemaAddIcon(
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddBehavior(
in_schema: in_param_ty!(Token),
in_name: in_param_ty!(CStyleString),
in_value: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_name: in_param_ty!(WFCString),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(
|in_schema: Token, in_name: CStyleString, in_value: CStyleString| {
|in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.add_behavior(
@@ -433,17 +435,17 @@ pub extern "C" fn WFSchemaAddBehavior(
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddExt(
in_schema: in_param_ty!(Token),
in_ext: in_param_ty!(CStyleString),
in_ext_name: in_param_ty!(CStyleString),
in_ext_icon: in_param_ty!(CStyleString),
in_ext_behavior: in_param_ty!(CStyleString),
in_schema: in_param_ty!(WFToken),
in_ext: in_param_ty!(WFCString),
in_ext_name: in_param_ty!(WFCString),
in_ext_icon: in_param_ty!(WFCString),
in_ext_behavior: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: Token,
in_ext: CStyleString,
in_ext_name: CStyleString,
in_ext_icon: CStyleString,
in_ext_behavior: CStyleString| {
cffi_wrapper!(|in_schema: WFToken,
in_ext: WFCString,
in_ext_name: WFCString,
in_ext_icon: WFCString,
in_ext_behavior: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.add_ext(
@@ -462,10 +464,10 @@ pub extern "C" fn WFSchemaAddExt(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramCreate(
in_schema: in_param_ty!(Token),
out_program: out_param_ty!(Token),
in_schema: in_param_ty!(WFToken),
out_program: out_param_ty!(WFToken),
) -> bool {
cffi_wrapper!(|in_schema: Token| -> (out_program: Token) {
cffi_wrapper!(|in_schema: WFToken| -> (out_program: WFToken) {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.pop(in_schema)?;
@@ -476,8 +478,8 @@ pub extern "C" fn WFProgramCreate(
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramDestroy(in_program: in_param_ty!(Token)) -> bool {
cffi_wrapper!(|in_program: Token| {
pub extern "C" fn WFProgramDestroy(in_program: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_program: WFToken| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
Ok(pool.free(in_program)?)
})
@@ -485,10 +487,10 @@ pub extern "C" fn WFProgramDestroy(in_program: in_param_ty!(Token)) -> bool {
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveName(
in_program: in_param_ty!(Token),
out_name: out_param_ty!(CStyleString),
in_program: in_param_ty!(WFToken),
out_name: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_program: Token| -> (out_name: CStyleString) {
cffi_wrapper!(|in_program: WFToken| -> (out_name: WFCString) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
@@ -500,10 +502,10 @@ pub extern "C" fn WFProgramResolveName(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveIcon(
in_program: in_param_ty!(Token),
out_icon_rc: out_param_ty!(Token),
in_program: in_param_ty!(WFToken),
out_icon_rc: out_param_ty!(WFToken),
) -> bool {
cffi_wrapper!(|in_program: Token| -> (out_icon_rc: Token) {
cffi_wrapper!(|in_program: WFToken| -> (out_icon_rc: WFToken) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
@@ -515,10 +517,10 @@ pub extern "C" fn WFProgramResolveIcon(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramExtsLen(
in_program: in_param_ty!(Token),
in_program: in_param_ty!(WFToken),
out_len: out_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: Token| -> (out_len: usize) {
cffi_wrapper!(|in_program: WFToken| -> (out_len: usize) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
Ok(program.exts_len())
@@ -527,18 +529,18 @@ pub extern "C" fn WFProgramExtsLen(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramFindExt(
in_program: in_param_ty!(Token),
in_body: in_param_ty!(CStyleString),
in_program: in_param_ty!(WFToken),
in_body: in_param_ty!(WFCString),
out_index: out_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: Token, in_body: CStyleString| -> (out_index: usize) {
cffi_wrapper!(|in_program: WFToken, in_body: WFCString| -> (out_index: usize) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let body = cstr_ffi::parse_ffi_string(in_body)?;
let index = match program.find_ext(body) {
Some(index) => index,
None => ffi_types::INVALID_INDEX,
None => WF_INVALID_INDEX,
};
Ok(index)
})
@@ -546,11 +548,11 @@ pub extern "C" fn WFProgramFindExt(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveExt(
in_program: in_param_ty!(Token),
in_program: in_param_ty!(WFToken),
in_index: in_param_ty!(usize),
out_self_ext_status: out_param_ty!(Token),
out_self_ext_status: out_param_ty!(WFToken),
) -> bool {
cffi_wrapper!(|in_program: Token, in_index: usize| -> (out_self_ext_status: Token) {
cffi_wrapper!(|in_program: WFToken, in_index: usize| -> (out_self_ext_status: WFToken) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
@@ -563,13 +565,13 @@ pub extern "C" fn WFProgramResolveExt(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramRegister(
in_program: in_param_ty!(Token),
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32| {
cffi_wrapper!(|in_program: WFToken, in_scope: u32| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?;
let scope = resolve_enum!(WFScope, in_scope)?;
program.register(scope.into())?;
Ok(())
})
@@ -577,13 +579,13 @@ pub extern "C" fn WFProgramRegister(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramUnregister(
in_program: in_param_ty!(Token),
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32| {
cffi_wrapper!(|in_program: WFToken, in_scope: u32| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?;
let scope = resolve_enum!(WFScope, in_scope)?;
program.unregister(scope.into())?;
Ok(())
})
@@ -591,28 +593,28 @@ pub extern "C" fn WFProgramUnregister(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramIsRegistered(
in_program: in_param_ty!(Token),
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
out_is_registered: out_param_ty!(bool),
) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32| -> (out_is_registered: bool) {
cffi_wrapper!(|in_program: WFToken, in_scope: u32| -> (out_is_registered: bool) {
let pool = pull_reader!(PROGRAM_POOL)?;
let program = pool.get(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?;
let scope = resolve_enum!(WFScope, in_scope)?;
Ok(program.is_registered(scope.into())?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramLinkExt(
in_program: in_param_ty!(Token),
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
in_index: in_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32, in_index: usize| {
cffi_wrapper!(|in_program: WFToken, in_scope: u32, in_index: usize| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?;
let scope = resolve_enum!(WFScope, in_scope)?;
program.link_ext(scope.into(), in_index)?;
Ok(())
})
@@ -620,14 +622,14 @@ pub extern "C" fn WFProgramLinkExt(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramUnlinkExt(
in_program: in_param_ty!(Token),
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
in_index: in_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32, in_index: usize| {
cffi_wrapper!(|in_program: WFToken, in_scope: u32, in_index: usize| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?;
let scope = resolve_enum!(WFScope, in_scope)?;
program.unlink_ext(scope.into(), in_index)?;
Ok(())
})
@@ -635,15 +637,15 @@ pub extern "C" fn WFProgramUnlinkExt(
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramQueryExt(
in_program: in_param_ty!(Token),
in_program: in_param_ty!(WFToken),
in_view: in_param_ty!(u32),
in_index: in_param_ty!(usize),
out_ext_status: out_param_ty!(Token),
out_ext_status: out_param_ty!(WFToken),
) -> bool {
cffi_wrapper!(|in_program: Token, in_view: u32, in_index: usize| -> (out_ext_status: Token) {
cffi_wrapper!(|in_program: WFToken, in_view: u32, in_index: usize| -> (out_ext_status: WFToken) {
let pool = pull_reader!(PROGRAM_POOL)?;
let program = pool.get(in_program)?;
let view = resolve_enum!(View, in_view)?;
let view = resolve_enum!(WFView, in_view)?;
let ext_status = program.query_ext(view.into(), in_index)?;
let token = match ext_status {
@@ -662,8 +664,8 @@ pub extern "C" fn WFProgramQueryExt(
// region: Extension Status
#[unsafe(no_mangle)]
pub extern "C" fn WFExtStatusDestroy(in_ext_status: in_param_ty!(Token)) -> bool {
cffi_wrapper!(|in_ext_status: Token| {
pub extern "C" fn WFExtStatusDestroy(in_ext_status: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_ext_status: WFToken| {
let mut pool = pull_writer!(EXT_STATUS_POOL)?;
Ok(pool.free(in_ext_status)?)
})
@@ -671,10 +673,10 @@ pub extern "C" fn WFExtStatusDestroy(in_ext_status: in_param_ty!(Token)) -> bool
#[unsafe(no_mangle)]
pub extern "C" fn WFExtStatusGetName(
in_ext_status: in_param_ty!(Token),
out_name: out_param_ty!(CStyleString),
in_ext_status: in_param_ty!(WFToken),
out_name: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_ext_status: Token| -> (out_name: CStyleString) {
cffi_wrapper!(|in_ext_status: WFToken| -> (out_name: WFCString) {
let pool = pull_reader!(EXT_STATUS_POOL)?;
let ext_status = pool.get(in_ext_status)?;
@@ -685,10 +687,10 @@ pub extern "C" fn WFExtStatusGetName(
#[unsafe(no_mangle)]
pub extern "C" fn WFExtStatusGetIcon(
in_ext_status: in_param_ty!(Token),
out_icon: out_param_ty!(HICON),
in_ext_status: in_param_ty!(WFToken),
out_icon: out_param_ty!(WFHICON),
) -> bool {
cffi_wrapper!(|in_ext_status: Token| -> (out_icon: HICON) {
cffi_wrapper!(|in_ext_status: WFToken| -> (out_icon: WFHICON) {
let pool = pull_reader!(EXT_STATUS_POOL)?;
let ext_status = pool.get(in_ext_status)?;
@@ -702,8 +704,8 @@ pub extern "C" fn WFExtStatusGetIcon(
// region: Self Extension Status
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusDestroy(in_self_ext_status: in_param_ty!(Token)) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| {
pub extern "C" fn WFSelfExtStatusDestroy(in_self_ext_status: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_self_ext_status: WFToken| {
let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
Ok(pool.free(in_self_ext_status)?)
})
@@ -711,10 +713,10 @@ pub extern "C" fn WFSelfExtStatusDestroy(in_self_ext_status: in_param_ty!(Token)
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusGetName(
in_self_ext_status: in_param_ty!(Token),
out_name: out_param_ty!(CStyleString),
in_self_ext_status: in_param_ty!(WFToken),
out_name: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| -> (out_name: CStyleString) {
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_name: WFCString) {
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
let self_ext_status = pool.get(in_self_ext_status)?;
@@ -725,10 +727,10 @@ pub extern "C" fn WFSelfExtStatusGetName(
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusGetIcon(
in_self_ext_status: in_param_ty!(Token),
out_icon: out_param_ty!(HICON),
in_self_ext_status: in_param_ty!(WFToken),
out_icon: out_param_ty!(WFHICON),
) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| -> (out_icon: HICON) {
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_icon: WFHICON) {
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
let self_ext_status = pool.get(in_self_ext_status)?;
@@ -739,10 +741,10 @@ pub extern "C" fn WFSelfExtStatusGetIcon(
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusGetExt(
in_self_ext_status: in_param_ty!(Token),
out_inner: out_param_ty!(CStyleString),
in_self_ext_status: in_param_ty!(WFToken),
out_inner: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| -> (out_inner: CStyleString) {
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_inner: WFCString) {
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
let self_ext_status = pool.get(in_self_ext_status)?;
@@ -753,10 +755,10 @@ pub extern "C" fn WFSelfExtStatusGetExt(
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusGetDottedExt(
in_self_ext_status: in_param_ty!(Token),
out_inner: out_param_ty!(CStyleString),
in_self_ext_status: in_param_ty!(WFToken),
out_inner: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| -> (out_inner: CStyleString) {
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_inner: WFCString) {
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
let self_ext_status = pool.get(in_self_ext_status)?;
@@ -770,8 +772,8 @@ pub extern "C" fn WFSelfExtStatusGetDottedExt(
// region: Icon Resource
#[unsafe(no_mangle)]
pub extern "C" fn WFIconRcDestroy(in_icon_rc: in_param_ty!(Token)) -> bool {
cffi_wrapper!(|in_icon_rc: Token| {
pub extern "C" fn WFIconRcDestroy(in_icon_rc: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_icon_rc: WFToken| {
let mut pool = pull_writer!(ICON_RC_POOL)?;
Ok(pool.free(in_icon_rc)?)
})
@@ -779,10 +781,10 @@ pub extern "C" fn WFIconRcDestroy(in_icon_rc: in_param_ty!(Token)) -> bool {
#[unsafe(no_mangle)]
pub extern "C" fn WFIconRcGetIcon(
in_icon_rc: in_param_ty!(Token),
out_icon: out_param_ty!(HICON),
in_icon_rc: in_param_ty!(WFToken),
out_icon: out_param_ty!(WFHICON),
) -> bool {
cffi_wrapper!(|in_icon_rc: Token| -> (out_icon: HICON) {
cffi_wrapper!(|in_icon_rc: WFToken| -> (out_icon: WFHICON) {
let pool = pull_reader!(ICON_RC_POOL)?;
let icon_rc = pool.get(in_icon_rc)?;
+1 -1
View File
@@ -1,4 +1,4 @@
# Wfassoc Executable
# WFAssoc Executable
For the syntax of this executable's parameters, execute it with `--help` for more.
+1 -1
View File
@@ -63,7 +63,7 @@ impl Manifest {
#[derive(Debug, TeError)]
pub enum ParseSchemaError {
/// Error when operating with schema.
#[error("{0}")]
#[error("failed on parsing manifest file to Schema: {0}")]
Schema(#[from] wfassoc::highlevel::SchemaError)
}
+12 -11
View File
@@ -12,20 +12,21 @@ use toml;
#[derive(Debug, TeError)]
pub enum Error {
/// Error when parsing Manifest TOML file.
#[error("{0}")]
#[error("initialization error: {0}")]
ParseManifest(#[from] manifest::ParseManifestError),
/// Error when parsing Manifest into Schema.
#[error("{0}")]
#[error("initialization error: {0}")]
ParseSchema(#[from] manifest::ParseSchemaError),
/// Error when serializing TOML
#[error("initialization error: {0}")]
SerializeToml(#[from] toml::ser::Error),
/// Error when parsing Schema into Program.
#[error("{0}")]
#[error("wfassoc error: {0}")]
ParseProgram(#[from] wfassoc::highlevel::ParseProgramError),
/// Error when operating Program.
#[error("{0}")]
#[error("wfassoc error: {0}")]
Program(#[from] wfassoc::highlevel::ProgramError),
/// Error when serializing TOML
#[error("{0}")]
SerializeToml(#[from] toml::ser::Error),
/// Find duplicated name when converting extension name to index
#[error("given extension name {0} has been specified more than one time")]
@@ -34,8 +35,8 @@ pub enum Error {
#[error("given extension name {0} is not presented in application")]
BadExtName(String),
/// Find star (*) extension name with other extension names when converting extension name to index
#[error("wildcard extension name \"*\" is not allowed to be used with other extension names")]
ExclusiveStarExtName(String),
#[error(r#"wildcard extension name "*" is not allowed to be used with other extension names"#)]
ExclusiveStarExtName,
}
/// Result type used in this module.
@@ -60,7 +61,7 @@ fn stringified_exts_to_indices(
// Check for star (*) with other extensions
let has_star = exts.iter().any(|ext| ext == "*");
if has_star && exts.len() > 1 {
return Err(Error::ExclusiveStarExtName("*".to_string()));
return Err(Error::ExclusiveStarExtName);
}
// If star is present alone, return fixed list from zero to the maximum ext index.
@@ -124,7 +125,7 @@ fn run_ext_unlink(
) -> Result<()> {
let exts = stringified_exts_to_indices(&program, exts)?;
for index in exts {
program.link_ext(scope, index)?;
program.unlink_ext(scope, index)?;
}
println!("File extension now is unlinked.");
+21 -1
View File
@@ -1,4 +1,6 @@
# WFassoc Core
# WFAssoc Core
## Usage
This crate provides low level API and high level API for manipulating Windows file associations at the same time.
For the convenient use of this project, the root module of this crate re-expose high level API.
@@ -10,3 +12,21 @@ Oppositely, for visiting low level API, please use `lowlevel` module.
If you are a programmer who want to take a deep into the internal implementations,
see `win32` module and its submodules for detail.
## Test Notes
Some tests of this crate may be dangerous because they need to manipulate Windows Registry.
So it is highly recommend that run these tests in sandbox environment.
In detailed words, you should run `cargo test --no-run` to build all test first,
then fetch the path to executable tests according to this command shown on console.
Then execute these executable tests in your sandbox for testing this crate.
Additionally, some tests also need Administration permission for testing,
because it requires write permission in HKLM.
If you do not test it with sandbox and administrative environment,
test program will assert paniked and tell you how to resolve these issues.
The reason why do not run `cargo test` in sandbox environment directly,
is that `cargo` can not find built tests located in host machine.
It will try to fetch all dependencies again and rebuild test in sandbox entirely.
So we use this complex way for testing.
+2
View File
@@ -1,3 +1,5 @@
//! The module gives a convenient highlevel wrapper for lowlevel file association operations.
use crate::lowlevel;
// region: Utilities
+76 -31
View File
@@ -17,37 +17,38 @@ use thiserror::Error as TeError;
/// Error occurs when trying converting [Schema] into [Program].
#[derive(Debug, TeError)]
pub enum ParseProgramError {
#[error("{0}")]
#[error("parsing into Program error: {0}")]
BadExtBody(#[from] concept::BadExtBodyError),
#[error("{0}")]
#[error("parsing into Program error: {0}")]
BadProgIdPart(#[from] concept::BadProgIdPartError),
#[error("{0}")]
#[error("parsing into Program error: {0}")]
BadFileName(#[from] concept::BadFileNameError),
#[error("{0}")]
#[error("parsing into Program error: {0}")]
ParseClsid(#[from] concept::ParseClsidError),
#[error("parsing into Program error: {0}")]
ParseCmdLine(#[from] concept::ParseCmdLineError),
#[error("{0}")]
#[error("parsing into Program error: {0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("given path doesn't has legal file name part")]
#[error("given path doesn't has legal file name part when parsing into Program")]
NoFileNamePart,
#[error("given path doesn't has legal directory part")]
#[error("given path doesn't has legal directory part when parsing into Program")]
NoDirNamePart,
#[error("given identifier is not presented in dict")]
NoSuchIdentifier,
#[error("extension name should not be empty")]
EmptyExtension,
#[error("given program identifier is not allowed")]
#[error("identifier {0} is unexpected not presented in HashMap when parsing into Program")]
NoSuchIdentifier(String),
#[error("bad program identifier found when parsing into Program")]
BadIdentifier,
}
/// Error occurs when operating with [Program].
#[derive(Debug, TeError)]
pub enum ProgramError {
#[error("{0}")]
#[error("Program operation error: {0}")]
Lowlevel(#[from] lowlevel::Error),
#[error("{0}")]
#[error("Program operation error: {0}")]
LoadIconRc(#[from] concept::LoadIconRcError),
#[error("given index is invalid")]
BadIndex,
#[error("given file extension index {0} is invalid when visiting Program")]
BadIndex(usize),
}
// endregion
@@ -62,6 +63,11 @@ pub struct Program {
app_path: String,
app_file_name: String,
app_dir_path: String,
// TODO: Remove this dead_code attribute once we start to use CLSID.
#[allow(dead_code)]
clsid: concept::Clsid,
name: Option<Arc<ProgramStr>>,
icon: Option<Arc<ProgramIcon>>,
behavior: Option<Arc<ProgramBehavior>>,
@@ -103,6 +109,8 @@ impl Program {
.ok_or(ParseProgramError::NoDirNamePart)
}
/// Flatten a `HashMap<String, V>` into a `Vec<U>` with a reverse index map,
/// by applying the given transformation function `f` to each value.
fn flat_hashmap<V, U, F>(
hashmap: &HashMap<String, V>,
f: F,
@@ -119,6 +127,8 @@ impl Program {
Ok((vector, indexmap))
}
/// Resolve a key string to an `Arc<T>` by looking it up in the index map,
/// then indexing into the vector.
fn resolve_index<T>(
key: &str,
vector: &Vec<Arc<T>>,
@@ -129,7 +139,7 @@ impl Program {
.get(*index)
.expect("unexpected invalid index")
.clone()),
None => Err(ParseProgramError::NoSuchIdentifier),
None => Err(ParseProgramError::NoSuchIdentifier(key.to_string())),
}
}
@@ -176,6 +186,9 @@ impl Program {
let app_paths_key = lowlevel::AppPathsKey::new(key.clone());
let applications_key = lowlevel::ApplicationsKey::new(key.clone());
// Parse CLSID
let clsid = schema.get_clsid().parse::<concept::Clsid>()?;
// Build string, icon and behavior list,
// and build mapper at the same time.
let (strs, strs_index_map) = Self::flat_hashmap(schema.get_strs(), |entry| {
@@ -260,6 +273,7 @@ impl Program {
app_path,
app_file_name,
app_dir_path,
clsid,
name,
icon,
behavior,
@@ -273,6 +287,10 @@ impl Program {
}
impl Program {
/// Resolve the display name of this application.
///
/// Returns the user-specified display name first.
/// Falls back to the executable file name if no display name is configured.
pub fn resolve_name(&self) -> Result<String, ProgramError> {
// Fecch from user specified name first
let name = self
@@ -291,6 +309,10 @@ impl Program {
Ok(self.app_file_name.clone())
}
/// Resolve the icon of this application.
///
/// Returns the user-specified icon first, then falls back to the first icon from the executable,
/// and finally to the system default executable icon.
pub fn resolve_icon(&self) -> Result<concept::IconRc, ProgramError> {
// Fetch from user specified icon first
let icon = self
@@ -312,17 +334,29 @@ impl Program {
)?)
}
/// Return the number of file extensions associated with this program.
pub fn exts_len(&self) -> usize {
self.ext_keys.len()
}
/// Find the index of a file extension by its body (without leading dot).
///
/// Returns `None` if the given extension is not associated with this program.
pub fn find_ext(&self, body: &str) -> Option<usize> {
self.ext_keys_map.get(body).copied()
}
/// Resolve the extension status for the extension at the given index.
/// This status including the ProgId associated with this file extension,
/// and the display name and icon for this file extension.
///
/// Returns the fetched status, or the error occurs when fetching.
pub fn resolve_ext(&self, index: usize) -> Result<ProgramSelfExtStatus, ProgramError> {
// Fetch data
let progid_ext_key = self.ext_keys.get(index).ok_or(ProgramError::BadIndex)?;
let progid_ext_key = self
.ext_keys
.get(index)
.ok_or(ProgramError::BadIndex(index))?;
// Try resolving name with string resource first,
// and fallback to ProgId verbatim.
@@ -387,7 +421,7 @@ impl Program {
for program_key in &mut self.ext_keys {
let progid_key = &mut program_key.progid_key;
debug_println!(
"Adding ProgId \"{0}\" subkey...",
r#"Adding ProgId "{0}" subkey..."#,
progid_key.inner().to_string()
);
@@ -402,6 +436,7 @@ impl Program {
// Add this progid to file extension "open with" list.
let ext_key = &mut program_key.ext_key;
ext_key.ensure(scope)?;
ext_key.add_into_open_with_progids(scope, progid_key.inner())?;
}
@@ -429,7 +464,7 @@ impl Program {
for program_key in &mut self.ext_keys {
let progid_key = &mut program_key.progid_key;
debug_println!(
"Deleting ProgId \"{0}\" subkey...",
r#"Deleting ProgId "{0}" subkey..."#,
progid_key.inner().to_string()
);
@@ -438,9 +473,12 @@ impl Program {
// there is no need to reset the default open way of file extension.
// So we simply remove it from "open with" list.
// Remove this ProgId from file extension "open with" list.
// Remove this ProgId from file extension "open with" list,
// if this file extension is existing
let ext_key = &mut program_key.ext_key;
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?;
if ext_key.is_exist(scope.into())? {
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?;
}
// Delete ProgId subkey
progid_key.delete(scope)?;
@@ -455,8 +493,7 @@ impl Program {
/// Check whether this application has been registered in given view.
///
/// Please note that this is a rough check and do not validate any data.
///
/// The return value only ensures the pre-requirement of `register` and `unregister`.
/// The return value only ensures the pre-requirements of [Self::register] and [Self::unregister].
pub fn is_registered(&self, scope: Scope) -> Result<bool, ProgramError> {
// Check App Paths subkey.
debug_println!("Checking App Paths subkey...");
@@ -475,7 +512,7 @@ impl Program {
for program_key in &self.ext_keys {
let progid_key = &program_key.progid_key;
debug_println!(
"Checking ProgId \"{0}\" subkey...",
r#"Checking ProgId "{0}" subkey..."#,
progid_key.inner().to_string()
);
@@ -488,13 +525,14 @@ impl Program {
Ok(true)
}
/// Set this program as the default handler for the file extension at the given index.
pub fn link_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
match self.ext_keys.get_mut(index) {
Some(program_key) => {
let ext_key = &mut program_key.ext_key;
let progid_key = &program_key.progid_key;
debug_println!(
"Linking ProgId \"{0}\" to extension \"{1}\" subkey...",
r#"Linking ProgId "{0}" to extension "{1}" subkey..."#,
progid_key.inner().to_string(),
ext_key.inner().to_string()
);
@@ -503,7 +541,7 @@ impl Program {
ext_key.ensure(scope)?;
ext_key.set_default(scope, Some(progid_key.inner()))?;
}
None => return Err(ProgramError::BadIndex),
None => return Err(ProgramError::BadIndex(index)),
};
// Everything is okey.
@@ -512,12 +550,13 @@ impl Program {
Ok(())
}
/// Remove this program as the default handler for the file extension at the given index.
pub fn unlink_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
match self.ext_keys.get_mut(index) {
Some(program_key) => {
let ext_key = &mut program_key.ext_key;
debug_println!(
"Unlinking for extension \"{0}\" subkey...",
r#"Unlinking for extension "{0}" subkey..."#,
ext_key.inner().to_string()
);
@@ -525,7 +564,7 @@ impl Program {
ext_key.ensure(scope)?;
ext_key.set_default(scope, None)?;
}
None => return Err(ProgramError::BadIndex),
None => return Err(ProgramError::BadIndex(index)),
}
// Everything is okey.
@@ -534,6 +573,10 @@ impl Program {
Ok(())
}
/// Query the current default association for the file extension at the given index.
///
/// Returns `Ok(None)` if the extension is not associated with any program,
/// or if the associated program's registration data is missing.
pub fn query_ext(
&self,
view: View,
@@ -543,7 +586,7 @@ impl Program {
Some(program_key) => {
let ext_key = &program_key.ext_key;
debug_println!(
"Querying for extension \"{0}\"subkey...",
r#"Querying for extension "{0}"subkey..."#,
ext_key.inner().to_string()
);
@@ -593,7 +636,7 @@ impl Program {
// Okey, return it.
Ok(Some(ProgramExtStatus::new(name, icon)))
}
None => Err(ProgramError::BadIndex),
None => Err(ProgramError::BadIndex(index)),
}
}
}
@@ -649,6 +692,7 @@ pub struct ProgramSelfExtStatus {
}
impl ProgramSelfExtStatus {
/// Create a new `ProgramSelfExtStatus`.
fn new(ext: concept::Ext, name: String, icon: concept::IconRc) -> Self {
Self { ext, name, icon }
}
@@ -689,6 +733,7 @@ pub struct ProgramExtStatus {
}
impl ProgramExtStatus {
/// Create a new `ProgramExtStatus`.
fn new(name: String, icon: concept::IconRc) -> Self {
Self { name, icon }
}
+46 -2
View File
@@ -7,7 +7,7 @@ use super::{Program, ParseProgramError};
/// Error occurs when operating with [Schema].
#[derive(Debug, TeError)]
pub enum SchemaError {
#[error("duplicate key: {0}")]
#[error("duplicate key in Schema operation: {0}")]
DuplicateKey(String),
}
@@ -38,6 +38,7 @@ pub struct Schema {
}
impl Schema {
/// Create an empty `Schema` with all fields set to their defaults.
pub fn new() -> Self {
Self {
identifier: String::new(),
@@ -76,22 +77,40 @@ impl Schema {
self.path = exe_path.to_string();
}
/// Set the CLSID (Class Identifier) of the application.
///
/// # Todo
///
/// Currently this field is not used by this crate,
/// but you still need to fill it with a legal CLSID for future expanding.
pub fn set_clsid(&mut self, clsid: &str) -> () {
self.clsid = clsid.to_string();
}
/// Set the key referencing a named string resource for the application display name.
///
/// This field is optional and can pass `None` to clear it.
pub fn set_name(&mut self, name: Option<&str>) -> () {
self.name = name.map(|n| n.to_string());
}
/// Set the key referencing a named icon resource for the application default icon.
///
/// This field is optional and can pass `None` to clear it.
pub fn set_icon(&mut self, icon: Option<&str>) -> () {
self.icon = icon.map(|i| i.to_string());
}
/// Set the key referencing a named behavior resource for the application default execution behavior.
///
/// This field is optional and can pass `None` to clear it.
pub fn set_behavior(&mut self, behavior: Option<&str>) -> () {
self.behavior = behavior.map(|b| b.to_string());
}
/// Add a named string resource entry.
///
/// Returns error if the name already exists.
pub fn add_str(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.strs.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
@@ -99,6 +118,9 @@ impl Schema {
}
}
/// Add a named icon resource entry.
///
/// Returns error if the name already exists.
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.icons.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
@@ -106,6 +128,9 @@ impl Schema {
}
}
/// Add a named behavior (command line) entry.
///
/// Returns error if the name already exists.
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.behaviors.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
@@ -116,6 +141,8 @@ impl Schema {
/// Add a file extension to the schema.
///
/// The parameter `ext` is the file extension without leading dot `.`.
/// And the parameter `ext_name`, `ext_icon` and `ext_behavior` are the key
/// referencing a named string resource, icon resource and behavior resource respectively.
pub fn add_ext(
&mut self,
ext: &str,
@@ -135,42 +162,55 @@ impl Schema {
}
impl Schema {
/// Get the identifier of the schema.
pub(super) fn get_identifier(&self) -> &str {
&self.identifier
}
/// Get the absolute path to the executable file.
pub(super) fn get_path(&self) -> &str {
&self.path
}
/// Get the CLSID (Class Identifier) of the application.
pub(super) fn get_clsid(&self) -> &str {
&self.clsid
}
/// Get the key referencing the named string resource for the display name.
/// Return `None` if user doesn't specify this.
pub(super) fn get_name(&self) -> Option<&str> {
self.name.as_ref().map(|v| v.as_str())
}
/// Get the key referencing the named icon resource for the default icon.
/// Return `None` if user doesn't specify this.
pub(super) fn get_icon(&self) -> Option<&str> {
self.icon.as_ref().map(|v| v.as_str())
}
/// Get the key referencing the named behavior resource for the default verb.
/// Return `None` if user doesn't specify this.
pub(super) fn get_behavior(&self) -> Option<&str> {
self.icon.as_ref().map(|v| v.as_str())
self.behavior.as_ref().map(|v| v.as_str())
}
/// Get a reference to the string resources map.
pub(super) fn get_strs(&self) -> &HashMap<String, String> {
&self.strs
}
/// Get a reference to the icon resources map.
pub(super) fn get_icons(&self) -> &HashMap<String, String> {
&self.icons
}
/// Get a reference to the behavior resources map.
pub(super) fn get_behaviors(&self) -> &HashMap<String, String> {
&self.behaviors
}
/// Get a reference to the file extensions map.
pub(super) fn get_exts(&self) -> &HashMap<String, SchemaExt> {
&self.exts
}
@@ -189,6 +229,7 @@ pub(crate) struct SchemaExt {
}
impl SchemaExt {
/// Create a new `SchemaExt` with the given name, icon key and behavior key.
fn new(name: &str, icon: &str, behavior: &str) -> Self {
Self {
name: name.to_string(),
@@ -199,14 +240,17 @@ impl SchemaExt {
}
impl SchemaExt {
/// Get the key referencing the named string resource for display name.
pub(super) fn get_name(&self) -> &str {
&self.name
}
/// Get the key referencing the named icon resource for file icon.
pub(super) fn get_icon(&self) -> &str {
&self.icon
}
/// Get the key referencing the named behavior resource.
pub(super) fn get_behavior(&self) -> &str {
&self.behavior
}
+1 -1
View File
@@ -1,5 +1,5 @@
//! This crate provide utilities fetching and manilupating Windows file association.
//! All code under crate are following Microsoft document: https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper
//! All code under crate are following Microsoft document: <https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper>
#[cfg(not(target_os = "windows"))]
compile_error!("Crate wfassoc is only supported on Windows.");
+66 -18
View File
@@ -1,3 +1,5 @@
//! The module containing lowlevel operation involving file association.
use crate::win32::{concept, regext, utilities};
use std::fmt::Display;
use std::str::FromStr;
@@ -10,27 +12,27 @@ use winreg::enums::{KEY_READ, KEY_WRITE};
/// Error occurs in this module.
#[derive(Debug, TeError)]
pub enum Error {
#[error("can not perform this operation because lack essential privilege")]
#[error("some lowlevel operations require escalating privilege")]
NoPrivilege,
#[error("given registry key is inexistant")]
#[error("given lowlevel registry key is not presented")]
InexistantKey,
#[error("registry operation error: {0}")]
#[error("lowlevel registry operation error: {0}")]
BadRegOp(#[from] std::io::Error),
#[error("{0}")]
#[error("unexpected blank registry key in lowlevel registry operation")]
UnexpectedBlankKey(#[from] regext::BlankPathError),
#[error("{0}")]
#[error("lowlevel operation error: {0}")]
ExpandEnvVar(#[from] concept::ExpandEnvVarError),
#[error("{0}")]
#[error("lowlevel operation error: {0}")]
LoadIconRc(#[from] concept::LoadIconRcError),
#[error("{0}")]
#[error("lowlevel operation error: {0}")]
LoadStrRc(#[from] concept::LoadStrRcError),
#[error("{0}")]
#[error("lowlevel operation error: {0}")]
ParseVerb(#[from] concept::ParseVerbError),
#[error("{0}")]
#[error("lowlevel operation error: {0}")]
ParseCmdLine(#[from] concept::ParseCmdLineError),
#[error("{0}")]
#[error("lowlevel operation error: {0}")]
ParseExt(#[from] concept::ParseExtError),
}
@@ -58,6 +60,7 @@ pub enum Scope {
pub struct TryFromViewError {}
impl TryFromViewError {
/// Creates a new `TryFromViewError`.
fn new() -> Self {
Self {}
}
@@ -106,9 +109,13 @@ impl From<Scope> for View {
/// The enum representing a losse Programmatic Identifiers (ProgId).
///
/// In real world, not all software developers are willing to following Microsoft suggestions to use ProgId.
/// They use string which do not have any regulation as ProgId.
/// In real world, not all software developers are willing
/// to following Microsoft suggestions to use ProgId.
/// They frequently use string which do not have any regulation as ProgId.
/// This enum is designed for handling this scenario.
///
/// You can utilize [FromStr] trait to build this struct.
/// Standard ProgId and any casual string are both acceptable.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LosseProgId {
Plain(String),
@@ -151,6 +158,11 @@ impl From<concept::ProgId> for LosseProgId {
/// In real usage, programmer can use Icon Reference String,
/// or a plain string pointing to a icon file as the icon setting value.
/// This enum is designed for handling this scenario.
///
/// You can utilize [FromStr] trait to build this struct.
/// You can pass a standard Icon Reference String or a path to specific icon file.
/// However, if you choose the last one, you must make sure
/// that your path is must be unquoted, but expand string is allowed.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum IconResVariant {
Plain(String),
@@ -158,7 +170,12 @@ pub enum IconResVariant {
}
impl IconResVariant {
/// Extracts the icon resource with the given size kind.
pub fn extract(&self, kind: concept::IconSizeKind) -> Result<concept::IconRc> {
// TODO:
// Once we implement quote mechanism inside IconRefStr,
// there is no need to preserve this unquote code in there.
// However the expand string mechanism should be kept.
let rc = match self {
IconResVariant::Plain(v) => concept::IconRc::with_ico_file(v.as_str(), kind)?,
IconResVariant::RefStr(v) => {
@@ -214,6 +231,9 @@ impl From<concept::IconRefStr> for IconResVariant {
/// In real usage, programmer can use String Reference String,
/// or a plain string as the string setting value.
/// This enum is designed for handling this scenario.
///
/// You can utilize [FromStr] trait to build this struct.
/// You can pass a standard String Reference String or a plain string.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum StrResVariant {
Plain(String),
@@ -221,7 +241,12 @@ pub enum StrResVariant {
}
impl StrResVariant {
/// Resolves this string resource variant into a plain string.
pub fn extract(&self) -> Result<String> {
// TODO:
// Once we implement quote mechanism inside StrRefStr,
// there is no need to preserve this unquote code in there.
// However the expand string mechanism should be kept.
let rv = match self {
// For plain string, we just simply clone it.
StrResVariant::Plain(v) => v.clone(),
@@ -276,6 +301,12 @@ impl From<concept::StrRefStr> for StrResVariant {
// region: Shell Verb
/// The struct representing a shell verb pair.
///
/// Shell verb is frequently used in Windows to describe how to use an application.
/// It can be used to define the default behavior of application,
/// or the way to use this application for opening a specific file.
///
/// You can use [ShellVerb::new] with given verb and command line infos to build this struct.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ShellVerb {
verb: concept::Verb,
@@ -283,14 +314,17 @@ pub struct ShellVerb {
}
impl ShellVerb {
/// Creates a new `ShellVerb` from a verb and a command.
pub fn new(verb: concept::Verb, command: concept::CmdLine) -> Self {
Self { verb, command }
}
/// Returns the verb of this shell verb pair.
pub fn get_verb(&self) -> &concept::Verb {
&self.verb
}
/// Returns the command of this shell verb pair.
pub fn get_command(&self) -> &concept::CmdLine {
&self.command
}
@@ -304,7 +338,7 @@ impl ShellVerb {
// region: Opened Key
/// Internal used struct representing the result of opening scope or view.
/// Internal used struct representing the result about opening registry key in given scope or view.
#[derive(Debug)]
struct OpenedKey {
/// The parent key of opened key which must be presented.
@@ -314,6 +348,7 @@ struct OpenedKey {
}
impl OpenedKey {
/// Creates a new `OpenedKey` with the given parent key and optional opened key.
fn new(parent_key: RegKey, this_key: Option<RegKey>) -> Self {
Self {
parent_key,
@@ -326,10 +361,10 @@ impl OpenedKey {
// region: Open Key Territory
/// The territory of opening key.
/// Internal used enum representing the territory of opening registry key.
///
/// Scope and View will finally be converted into this type,
/// and delivered to an uniform function to open key.
/// [Scope] and [View] will finally be converted into this type,
/// and delivered to an uniform function to open registry key.
#[derive(Debug, Copy, Clone)]
enum OpenKeyTerritory {
System,
@@ -360,7 +395,7 @@ impl From<View> for OpenKeyTerritory {
// region: Open Key Purpose
/// The purpose of opening this key.
/// The purpose of opening given registry key.
#[derive(Debug, Copy, Clone)]
enum OpenKeyPurpose {
/// Only read something.
@@ -370,6 +405,7 @@ enum OpenKeyPurpose {
}
impl OpenKeyPurpose {
/// Converts this purpose to the corresponding registry permission flags.
fn to_permission(&self) -> u32 {
match self {
OpenKeyPurpose::Read => PERM_R,
@@ -411,8 +447,17 @@ fn check_privilege(territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Resu
}
}
/// Remove quote pair if possible.
// TODO:
// This function should be eliminated or moved to another place,
// once we finish the quote mechanism in StrRefStr and IconRefStr.
/// Internal function removing quote pair if possible.
///
/// # Todo
///
/// This function actually is the compromise about the current design of this crate.
/// This function will be optimized in future.
///
/// In some cases, the path part of [concept::IconRefStr] or [concept::StrRefStr] is quoted by quote.
/// This can no be recognized by Win32 functions.
/// So in this case, we should remove this quote pair.
@@ -431,6 +476,9 @@ fn strip_quote<'a>(s: &'a str) -> &'a str {
// region: Registry Keys
// YYC MARK:
// Load submodules and re-export them.
mod app_path_key;
mod applications_key;
mod ext_key;
+51 -23
View File
@@ -5,16 +5,30 @@ use crate::win32::{concept, regext};
use winreg::RegKey;
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
/// A registry key wrapper for `Software\Microsoft\Windows\CurrentVersion\App Paths\<APP>`.
///
/// This key can only be opened under a Scope (not a View), since
/// "App Paths" only exists under HKCU and HKLM, not under a merged HKCR view.
///
/// This struct provides [Self::is_exist], [Self::ensure] and [Self::delete] to
/// check whether this key is in Registry, make sure this key is presneted in Registry,
/// and delete self from Registry respectively.
///
/// And there are some getter and setter in this struct,
/// before calling them, you must use [Self::ensure] make sure that this key is presented in Registry,
/// otherwise these functions will return errors.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AppPathsKey {
key_name: concept::FileName,
}
impl AppPathsKey {
/// Creates a new `AppPathsKey` with the given file name.
pub fn new(inner: concept::FileName) -> Self {
Self { key_name: inner }
}
/// Returns the inner file name of this application path key.
pub fn inner(&self) -> &concept::FileName {
&self.key_name
}
@@ -23,6 +37,7 @@ impl AppPathsKey {
impl AppPathsKey {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
/// Attempts to open the registry key for this application path with the given territory and purpose.
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege
check_privilege(territory, purpose)?;
@@ -49,20 +64,24 @@ impl AppPathsKey {
Ok(OpenedKey::new(app_paths, this_app))
}
/// Opens the registry key for read-only access under the given scope.
fn open_scope_for_read(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::Read)
}
/// Opens the registry key for read-write access under the given scope.
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
/// Checks whether this application path key exists in the registry under the given scope.
/// Return true if it is presented in registry, otherwise false.
pub fn is_exist(&self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_read(scope)?.this_key;
Ok(key.is_some())
}
/// Ensure this application key is presented in App Paths.
/// Ensure this application key is presented in App Paths key.
///
/// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key.
@@ -79,66 +98,75 @@ impl AppPathsKey {
}
}
/// Delete this application key from App Paths.
/// Delete this application key from App Paths key.
///
/// If there is no such key in App Paths,
/// this function does nothing.
pub fn delete(&mut self, scope: Scope) -> Result<()> {
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
key.parent_key
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?;
Ok(())
Ok(regext::arbitrarily_delete_subkey_all(
&key.parent_key,
regext::blank_path_guard(self.key_name.inner())?,
)?)
}
// YYC MARK:
// Reference: <https://learn.microsoft.com/en-us/windows/win32/shell/app-registration#using-the-app-paths-subkey>
// TODO:
// We only support these keys in there because current interface are enough to use.
// We may expand these in future.
/// Opens the registry key for getter reading, returning an error if the key does not exist.
fn open_scope_for_getter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_read(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
/// Opens the registry key for setter writing, returning an error if the key does not exist.
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
// YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/app-registration#using-the-app-paths-subkey
const NAMEOF_DEFAULT: &str = "";
/// Gets the default value of this application path key.
///
///
/// This field point to the fully qualified path to the application.
/// The content of this key should point to the fully qualified path of the application.
pub fn get_default(&self, scope: Scope) -> Result<String> {
let key = self.open_scope_for_getter(scope)?;
Ok(key.get_value(Self::NAMEOF_DEFAULT)?)
}
///
///
/// This field should be filled with fully qualified path to the application.
/// Sets the default value of this application path key.
pub fn set_default(&mut self, scope: Scope, value: &str) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
key.set_value(Self::NAMEOF_DEFAULT, &value)?;
Ok(())
}
// TODO:
// This key may be REG_SZ or REG_EXPAND_SZ.
// Currently we see them as one type.
// This should be improved in future.
const NAMEOF_PATH: &str = "Path";
/// Gets the `Path` value of this application path key.
///
///
/// This field point to the added path for PATH environment variable.
/// Usually it is the path to application directory.
/// The content of this key will be added to PATH environment variable,
/// to enable that application can find its dependencies conveniently.
/// If it contains multiple pathes, semicolon-separated form is required.
/// Usually this is the path to application directory.
pub fn get_path(&self, scope: Scope) -> Result<String> {
let key = self.open_scope_for_getter(scope)?;
Ok(key.get_value(Self::NAMEOF_PATH)?)
}
///
///
/// This field should be the added path for PATH environment variable.
/// Usually it is the path to application directory.
/// Sets the `Path` value of this application path key.
pub fn set_path(&mut self, scope: Scope, value: &str) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
key.set_value(Self::NAMEOF_PATH, &value)?;
+85 -10
View File
@@ -6,16 +6,27 @@ use crate::win32::{concept, regext};
use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
/// A registry key wrapper for `Software\Classes\Applications\<APP>`.
///
/// This struct provides [Self::is_exist], [Self::ensure] and [Self::delete] to
/// check whether this key is in Registry, make sure this key is presneted in Registry,
/// and delete self from Registry respectively.
///
/// And there are some getter and setter in this struct,
/// before calling them, you must use [Self::ensure] make sure that this key is presented in Registry,
/// otherwise these functions will return errors.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ApplicationsKey {
key_name: concept::FileName,
}
impl ApplicationsKey {
/// Creates a new `ApplicationsKey` with the given file name.
pub fn new(inner: concept::FileName) -> Self {
Self { key_name: inner }
}
/// Returns the inner file name of this applications key.
pub fn inner(&self) -> &concept::FileName {
&self.key_name
}
@@ -25,6 +36,7 @@ impl ApplicationsKey {
const FULL_APPLICATIONS: &str = "Software\\Classes\\Applications";
const PARTIAL_APPLICATIONS: &str = "Applications";
/// Attempts to open the registry key for this application with the given territory and purpose.
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege
check_privilege(territory, purpose)?;
@@ -55,19 +67,27 @@ impl ApplicationsKey {
Ok(OpenedKey::new(applications, this_app))
}
/// Opens the registry key for read-only access under the given view.
fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
self.open_key(view.into(), OpenKeyPurpose::Read)
}
/// Opens the registry key for read-write access under the given scope.
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
/// Checks whether this applications key exists in the registry under the given view.
/// Return true if it is presented in registry, otherwise false.
pub fn is_exist(&self, view: View) -> Result<bool> {
let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some())
}
/// Ensure this application key is presented in Applications key.
///
/// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key.
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
if let None = key.this_key {
@@ -81,32 +101,54 @@ impl ApplicationsKey {
}
}
pub fn delete(&mut self, scope: Scope) -> Result<()> {
/// Delete this application key from Applications key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
key.parent_key
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?;
Ok(())
Ok(regext::arbitrarily_delete_subkey_all(
&key.parent_key,
regext::blank_path_guard(self.key_name.inner())?,
)?)
}
// YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/app-registration#using-the-applications-subkey
// Reference: <https://learn.microsoft.com/en-us/windows/win32/shell/app-registration#using-the-applications-subkey>
// TODO:
// We only support these keys in there because current interface are enough to use.
// We may expand these in future.
/// Opens the registry key for getter reading, returning an error if the key does not exist.
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
self.open_view_for_read(view)?
.this_key
.ok_or(Error::InexistantKey)
}
/// Opens the registry key for setter writing, returning an error if the key does not exist.
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
// TODO:
// Support mutiple ShellVerb in future.
const NAMEOF_SHELL_VERB_PART1: &str = "shell";
const NAMEOF_SHELL_VERB_PART3: &str = "command";
const NAMEOF_SHELL_VERB_PART4: &str = "";
/// Gets the shell verb registered for this application under the given view.
/// Return `None` if there is no any shell verb.
///
/// # Todo
///
/// Currently we only support single shell verb.
/// So if there is multiple shell verb, this function still return `None`.
/// This issue will be resolved in future.
pub fn get_shell_verb(&self, view: View) -> Result<Option<ShellVerb>> {
let key = self.open_view_for_getter(view)?;
@@ -157,6 +199,15 @@ impl ApplicationsKey {
)))
}
/// Sets the shell verb for this application under the given scope.
/// Pass `None` as shell verb to remove the shell verb.
///
/// # Todo
///
/// Currently we only support single shell verb.
/// So if there is multiple shell verb, all of them will be deleted at first,
/// and then write your given shell verb inside it.
/// This issue will be resolved in future.
pub fn set_shell_verb(&mut self, scope: Scope, sv: Option<&ShellVerb>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
@@ -176,7 +227,7 @@ impl ApplicationsKey {
}
None => {
// Delete shell and its all subkey.
key.delete_subkey_all(Self::NAMEOF_SHELL_VERB_PART1)?;
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
}
}
@@ -186,6 +237,10 @@ impl ApplicationsKey {
const NAMEOF_DEFAULT_ICON_PART1: &str = "DefaultIcon";
const NAMEOF_DEFAULT_ICON_PART2: &str = "";
/// Gets the default icon registered for this application under the given view.
/// Return `None` if there is no such set default icon.
///
/// This icon will be used to represent the application instead of the first icon stored in the .exe file.
pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get default icon subkey
@@ -204,6 +259,8 @@ impl ApplicationsKey {
Ok(default_icon_default_value.map(|v| IconResVariant::from(v.as_str())))
}
/// Sets the default icon for this application under the given scope.
/// Pass `None` as icon to remove the default icon.
pub fn set_default_icon(&mut self, scope: Scope, icon: Option<&IconResVariant>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
@@ -217,7 +274,7 @@ impl ApplicationsKey {
}
None => {
// Delete shell and its all subkey.
key.delete_subkey_all(Self::NAMEOF_DEFAULT_ICON_PART1)?;
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
}
}
@@ -226,6 +283,10 @@ impl ApplicationsKey {
const NAMEOF_FRIENDLY_APP_NAME: &str = "FriendlyAppName";
/// Gets the friendly app name registered for this application under the given view.
/// Return `None` if there is no such set friendly app name.
///
/// This name will be used to display for an application instead of just the version information appearing.
pub fn get_friendly_app_name(&self, view: View) -> Result<Option<StrResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get value of it
@@ -234,6 +295,8 @@ impl ApplicationsKey {
Ok(value.map(|v| StrResVariant::from(v.as_str())))
}
/// Sets the friendly app name for this application under the given scope.
/// Pass `None` to remove the friendly app name value.
pub fn set_friendly_app_name(
&mut self,
scope: Scope,
@@ -248,7 +311,7 @@ impl ApplicationsKey {
}
None => {
// Delete this key
key.delete_value(Self::NAMEOF_FRIENDLY_APP_NAME)?;
regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_APP_NAME)?;
}
}
@@ -257,6 +320,10 @@ impl ApplicationsKey {
const NAMEOF_SUPPORTED_TYPES: &str = "SupportedTypes";
/// Gets the supported file types registered for this application under the given view.
/// Return `None` if application doesn't set this (because this is suggested but not forced).
///
/// This is a list defining the file types that the application supports.
pub fn get_supported_types(&self, view: View) -> Result<Option<Vec<concept::Ext>>> {
let key = self.open_view_for_getter(view)?;
// Get supported types subkey
@@ -276,6 +343,8 @@ impl ApplicationsKey {
Ok(Some(exts))
}
/// Sets the supported file types for this application under the given scope.
/// Pass `None` to remove this list.
pub fn set_supported_types(
&mut self,
scope: Scope,
@@ -298,7 +367,7 @@ impl ApplicationsKey {
}
None => {
// Delete this subkey.
key.delete_subkey_all(Self::NAMEOF_SUPPORTED_TYPES)?;
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SUPPORTED_TYPES)?;
}
}
@@ -307,6 +376,7 @@ impl ApplicationsKey {
const NAMEOF_NO_OPEN_WITH: &str = "NoOpenWith";
/// Checks whether the "NoOpenWith" flag is set for this application under the given view.
pub fn get_no_open_with(&self, view: View) -> Result<bool> {
let key = self.open_view_for_getter(view)?;
match regext::try_get_value::<String, _>(&key, Self::NAMEOF_NO_OPEN_WITH)? {
@@ -315,12 +385,17 @@ impl ApplicationsKey {
}
}
/// Sets or clears the "NoOpenWith" flag for this application under the given scope.
///
/// Microsoft document said that this key indicates that no application is specified for opening this file type.
/// But I totally doesn't understand this. However, most of application set this.
/// So let we set it together.
pub fn set_no_open_with(&mut self, scope: Scope, flag: bool) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
if flag {
key.set_value(Self::NAMEOF_NO_OPEN_WITH, &"")?;
} else {
key.delete_value(Self::NAMEOF_NO_OPEN_WITH)?;
regext::arbitrarily_delete_value(&key, Self::NAMEOF_NO_OPEN_WITH)?;
}
Ok(())
}
+55 -15
View File
@@ -6,16 +6,27 @@ use crate::win32::{concept, regext};
use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
/// A registry key wrapper for `Software\Classes\<EXTENSION>`.
///
/// This struct provides [Self::is_exist], [Self::ensure] and [Self::delete] to
/// check whether this key is in Registry, make sure this key is presneted in Registry,
/// and delete self from Registry respectively.
///
/// And there are some getter and setter in this struct,
/// before calling them, you must use [Self::ensure] make sure that this key is presented in Registry,
/// otherwise these functions will return errors.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExtKey {
ext: concept::Ext,
}
impl ExtKey {
/// Creates a new `ExtKey` with the given file extension.
pub fn new(inner: concept::Ext) -> Self {
Self { ext: inner }
}
/// Returns the inner file extension of this extension key.
pub fn inner(&self) -> &concept::Ext {
&self.ext
}
@@ -25,6 +36,7 @@ impl ExtKey {
const FULL_CLASSES: &str = "Software\\Classes";
const PARTIAL_CLASSES: &str = "";
/// Attempts to open the registry key for this file extension with the given territory and purpose.
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege
check_privilege(territory, purpose)?;
@@ -53,19 +65,27 @@ impl ExtKey {
Ok(OpenedKey::new(classes, this_ext))
}
/// Opens the registry key for read-only access under the given view.
fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
self.open_key(view.into(), OpenKeyPurpose::Read)
}
/// Opens the registry key for read-write access under the given scope.
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
/// Checks whether this file extension key exists in the registry under the given view.
/// Return true if it is presented in registry, otherwise false.
pub fn is_exist(&self, view: View) -> Result<bool> {
let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some())
}
/// Ensure this file extension key is presented in Classes key.
///
/// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key.
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
if let None = key.this_key {
@@ -79,28 +99,34 @@ impl ExtKey {
}
}
pub fn delete(&mut self, scope: Scope) -> Result<()> {
/// Delete this file extension key from Classes key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
key.parent_key
.delete_subkey_all(regext::blank_path_guard(self.ext.dotted_inner())?)?;
Ok(())
Ok(regext::arbitrarily_delete_subkey_all(
&key.parent_key,
regext::blank_path_guard(self.ext.dotted_inner())?,
)?)
}
// YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#setting-optional-subkeys-and-file-type-extension-attributes
// Reference: <https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#setting-optional-subkeys-and-file-type-extension-attributes>
// And we explicitly do not support "OpenWithList", because it is obsolete in modern Windows.
// TODO:
// We do not support "Content Type" and "PerceivedType"
// because current interface are enough to use,
// and these types has not been made as concept struct in Rust.
// We only support these keys in there because current interface are enough to use.
// We may expand these in future.
/// Opens the registry key for getter reading, returning an error if the key does not exist.
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
self.open_view_for_read(view)?
.this_key
.ok_or(Error::InexistantKey)
}
/// Opens the registry key for setter writing, returning an error if the key does not exist.
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)?
.this_key
@@ -109,6 +135,10 @@ impl ExtKey {
const NAMEOF_DEFAULT: &str = "";
/// Gets the default value of this file extension under the given view.
/// Return `None` if there is no association for this file extension.
///
/// The content of this key is this file extension associated ProgId.
pub fn get_default(&self, view: View) -> Result<Option<LosseProgId>> {
let key = self.open_view_for_getter(view)?;
// Get value of it
@@ -117,6 +147,8 @@ impl ExtKey {
Ok(value.map(|v| LosseProgId::from(v.as_str())))
}
/// Sets the default value (ProgId association) for this file extension under the given scope.
/// Pass `None` to remove the association.
pub fn set_default(&mut self, scope: Scope, pid: Option<&LosseProgId>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
@@ -127,7 +159,7 @@ impl ExtKey {
}
None => {
// Delete this key
key.delete_value(Self::NAMEOF_DEFAULT)?;
regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
}
}
@@ -136,6 +168,11 @@ impl ExtKey {
const NAMEOF_OPEN_WITH_PROGIDS: &str = "OpenWithProgIds";
/// Gets the list of ProgIds registered in the "OpenWithProgIds" subkey under the given view.
/// Return `None` is there is no this subkey.
///
/// This subkey contains a list of alternate ProgId for this file type.
/// In Windows Explorer, these ProgId should appear in the Open with menu in right click menu of this file extension.
pub fn get_open_with_progids(&self, view: View) -> Result<Option<Vec<LosseProgId>>> {
let key = self.open_view_for_getter(view)?;
// Get OpenWithProgIds subkey
@@ -156,7 +193,7 @@ impl ExtKey {
Ok(Some(progids))
}
///
/// Checks whether the given ProgId is in the "OpenWithProgIds" subkey under the given view.
///
/// If there is no "OpenWithProgIds" subkey, this function return false.
pub fn is_in_open_with_progids(&self, view: View, pid: &LosseProgId) -> Result<bool> {
@@ -176,7 +213,7 @@ impl ExtKey {
.is_some())
}
///
/// Adds the given ProgId into the "OpenWithProgIds" subkey under the given scope.
///
/// If there is no "OpenWithProgIds" subkey, this function will create it first,
/// then add your given ProgId into it.
@@ -190,9 +227,9 @@ impl ExtKey {
Ok(())
}
/// Removes the given ProgId from the "OpenWithProgIds" subkey under the given scope.
///
///
/// If there is no "OpenWithProgIds" subkey, this function do nothing.
/// If there is no "OpenWithProgIds" subkey, this function does nothing.
pub fn remove_from_open_with_progids(&mut self, scope: Scope, pid: &LosseProgId) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
// Try get subkey
@@ -205,7 +242,10 @@ impl ExtKey {
None => return Ok(()),
};
// Remove given key
open_with_progids_key.delete_value(pid.to_string())?;
regext::arbitrarily_delete_value(
&open_with_progids_key,
regext::blank_path_guard(pid.to_string())?,
)?;
Ok(())
}
}
+88 -17
View File
@@ -6,16 +6,27 @@ use crate::win32::regext;
use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
/// A registry key wrapper for `Software\Classes\<PROGID>`.
///
/// This struct provides [Self::is_exist], [Self::ensure] and [Self::delete] to
/// check whether this key is in Registry, make sure this key is presneted in Registry,
/// and delete self from Registry respectively.
///
/// And there are some getter and setter in this struct,
/// before calling them, you must use [Self::ensure] make sure that this key is presented in Registry,
/// otherwise these functions will return errors.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgIdKey {
progid: LosseProgId,
}
impl ProgIdKey {
/// Creates a new `ProgIdKey` with the given ProgId.
pub fn new(inner: LosseProgId) -> Self {
Self { progid: inner }
}
/// Returns the inner ProgId of this ProgId key.
pub fn inner(&self) -> &LosseProgId {
&self.progid
}
@@ -25,6 +36,7 @@ impl ProgIdKey {
const FULL_CLASSES: &str = "Software\\Classes";
const PARTIAL_CLASSES: &str = "";
/// Attempts to open the registry key for this ProgId with the given territory and purpose.
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege
check_privilege(territory, purpose)?;
@@ -53,19 +65,27 @@ impl ProgIdKey {
Ok(OpenedKey::new(classes, this_progid))
}
/// Opens the registry key for read-only access under the given view.
fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
self.open_key(view.into(), OpenKeyPurpose::Read)
}
/// Opens the registry key for read-write access under the given scope.
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
/// Checks whether this ProgId key exists in the registry under the given view.
/// Return true if it is presented in registry, otherwise false.
pub fn is_exist(&self, view: View) -> Result<bool> {
let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some())
}
/// Ensure this ProgId key is presented in Classes key.
///
/// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key.
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
if let None = key.this_key {
@@ -79,27 +99,33 @@ impl ProgIdKey {
}
}
pub fn delete(&mut self, scope: Scope) -> Result<()> {
/// Delete this ProgId key from Classes key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
key.parent_key
.delete_subkey_all(regext::blank_path_guard(self.progid.to_string())?)?;
Ok(())
Ok(regext::arbitrarily_delete_subkey_all(
&key.parent_key,
regext::blank_path_guard(self.progid.to_string())?,
)?)
}
// YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids#programmatic-identifier-elements-used-by-file-associations
// Reference: <https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids#programmatic-identifier-elements-used-by-file-associations>
// TODO:
// Currently we only support (Default), FriendlyTypeName and DefaultIcon
// to just cover the basic usage.
// We only support these keys in there because current interface are enough to use.
// We may expand these in future.
/// Opens the registry key for getter reading, returning an error if the key does not exist.
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
self.open_view_for_read(view)?
.this_key
.ok_or(Error::InexistantKey)
}
/// Opens the registry key for setter writing, returning an error if the key does not exist.
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)?
.this_key
@@ -108,6 +134,11 @@ impl ProgIdKey {
const NAMEOF_DEFAULT: &str = "";
/// Gets the default value of this ProgId under the given view.
///
/// The content of this key is the legacy way to introduce the friendly name of application.
/// If you are fetching friendly name, please choose "FriendlyTypeName" at first, then fallback to this.
/// If you are setting friendly name, please set this together.
pub fn get_default(&self, view: View) -> Result<Option<StrResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get value of it
@@ -116,9 +147,7 @@ impl ProgIdKey {
Ok(value.map(|v| StrResVariant::from(v.as_str())))
}
///
///
/// The legacy way to set friendly name for this ProgId.
/// Sets the default value (friendly name) for this ProgId under the given scope.
pub fn set_default(&mut self, scope: Scope, name: Option<&StrResVariant>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
@@ -129,17 +158,28 @@ impl ProgIdKey {
}
None => {
// Delete this key
key.delete_value(Self::NAMEOF_DEFAULT)?;
regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
}
}
Ok(())
}
// TODO:
// Support mutiple ShellVerb in future.
const NAMEOF_SHELL_VERB_PART1: &str = "shell";
const NAMEOF_SHELL_VERB_PART3: &str = "command";
const NAMEOF_SHELL_VERB_PART4: &str = "";
/// Gets the shell verb registered for this ProgId under the given view.
/// Return `None` if there is no any shell verb.
///
/// # Todo
///
/// Currently we only support single shell verb.
/// So if there is multiple shell verb, this function still return `None`.
/// This issue will be resolved in future.
pub fn get_shell_verb(&self, view: View) -> Result<Option<ShellVerb>> {
let key = self.open_view_for_getter(view)?;
@@ -190,6 +230,15 @@ impl ProgIdKey {
)))
}
/// Sets the shell verb for this ProgId under the given scope.
/// Pass `None` as shell verb to remove the shell verb.
///
/// # Todo
///
/// Currently we only support single shell verb.
/// So if there is multiple shell verb, all of them will be deleted at first,
/// and then write your given shell verb inside it.
/// This issue will be resolved in future.
pub fn set_shell_verb(&mut self, scope: Scope, sv: Option<&ShellVerb>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
@@ -209,15 +258,24 @@ impl ProgIdKey {
}
None => {
// Delete shell and its all subkey.
key.delete_subkey_all(Self::NAMEOF_SHELL_VERB_PART1)?;
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
}
}
Ok(())
}
// TODO:
// This key may be REG_SZ or REG_EXPAND_SZ.
// Currently we see them as one type.
// This should be improved in future.
const NAMEOF_FRIENDLY_TYPE_NAME: &str = "FriendlyTypeName";
/// Gets the friendly type name registered for this ProgId under the given view.
/// Return `None` if there is no such key.
///
/// The content of this key is the file type name which is suitable to display to the user.
pub fn get_friendly_type_name(&self, view: View) -> Result<Option<StrResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get value of it
@@ -226,9 +284,8 @@ impl ProgIdKey {
Ok(value.map(|v| StrResVariant::from(v.as_str())))
}
///
///
/// Set this entry to a friendly name for the ProgID.
/// Sets the friendly type name for this ProgId under the given scope.
/// Pass `None` to remove friendly type name.
pub fn set_friendly_type_name(
&mut self,
scope: Scope,
@@ -243,16 +300,25 @@ impl ProgIdKey {
}
None => {
// Delete this key
key.delete_value(Self::NAMEOF_FRIENDLY_TYPE_NAME)?;
regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_TYPE_NAME)?;
}
}
Ok(())
}
// TODO:
// This key may be REG_SZ or REG_EXPAND_SZ.
// Currently we see them as one type.
// This should be improved in future.
const NAMEOF_DEFAULT_ICON_PART1: &str = "DefaultIcon";
const NAMEOF_DEFAULT_ICON_PART2: &str = "";
/// Gets the default icon registered for this ProgId under the given view.
/// Return `None` if there is no default icon.
///
/// The content of this key will be displayed for file types associated with this ProgId.
pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get default icon subkey
@@ -271,6 +337,11 @@ impl ProgIdKey {
Ok(default_icon_default_value.map(|v| IconResVariant::from(v.as_str())))
}
/// Sets the default icon for this ProgId under the given scope.
/// Pass `None` to remove the default icon subkey.
///
/// Although this function give the ability that do not set this key,
/// however, I strongly suggest that set this to let file associated with this ProgId have an icon.
pub fn set_default_icon(&mut self, scope: Scope, icon: Option<&IconResVariant>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
@@ -284,7 +355,7 @@ impl ProgIdKey {
}
None => {
// Delete shell and its all subkey.
key.delete_subkey_all(Self::NAMEOF_DEFAULT_ICON_PART1)?;
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
}
}
+6 -3
View File
@@ -5,11 +5,11 @@ use std::iter::FusedIterator;
use std::path::Path;
use thiserror::Error as TeError;
// region: OS String Related
// region: OS String Casting
/// The error occurs when casting `OsStr` into `str`.
/// The error occurs when casting [OsStr] into [str].
#[derive(Debug, TeError)]
#[error("fail to cast OS string into string")]
#[error("fail to cast OS string into Rust string")]
pub struct CastOsStrError {}
impl CastOsStrError {
@@ -32,6 +32,8 @@ pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> {
// region: Capitalize First ASCII Letter
/// The struct transforming accept an iterator and perform as an iterator
/// whose transform the first character of accepted iterator into capital form (ASCII char only).
struct CapitalizeFirstAscii<T>
where
T: Iterator<Item = char>,
@@ -72,6 +74,7 @@ where
impl<T> FusedIterator for CapitalizeFirstAscii<T> where T: Iterator<Item = char> + FusedIterator {}
/// Accept an string slice, transform its first char (ASCII only) as capital form and output.
pub fn capitalize_first_ascii(s: &str) -> String {
CapitalizeFirstAscii::new(s.chars()).collect()
}
+3
View File
@@ -1,3 +1,6 @@
//! The module containing Win32 stuff related to file association operation,
//! including Win32 concept, Registry operation helper and etc.
pub mod concept;
pub mod utilities;
pub mod regext;
+198 -28
View File
@@ -15,7 +15,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::HICON;
/// The error occurs when constructing Ext with bad body.
#[derive(Debug, TeError)]
#[error("given file extension body \"{inner}\" is invalid")]
#[error(r#"given file extension body "{inner}" is invalid"#)]
pub struct BadExtBodyError {
/// The clone of string which is not a valid file extension body.
inner: String,
@@ -32,6 +32,12 @@ impl BadExtBodyError {
/// The struct representing an file extension which must start with dot (`.`)
/// and followed by at least one arbitrary characters.
///
/// If you having a file extension expressed in string form with leading dot,
/// please utilize [FromStr] trait to build this struct.
/// Otherwise, use [Ext::new] with file extension without leading dot.
///
/// The [Display] trait this struct implemented always outputs file extension with leading dot.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ext {
/// The body of file extension (excluding dot).
@@ -42,6 +48,8 @@ impl Ext {
/// Create an new file extension.
///
/// `body` is the body of file extension (excluding dot, such as `jpg`).
/// Empty body is not allowed.
///
/// If you want to create this struct with ordinary extension string like `.jpg`,
/// please use `Ext::from_str()` or `parse::<Ext>()` instead.
pub fn new(body: &str) -> Result<Self, BadExtBodyError> {
@@ -56,11 +64,17 @@ impl Ext {
}
/// Get the body part of file extension (excluding dot)
///
/// This struct promise that there is no possibility that return value is blank
/// (i.e. the body of file extension is empty).
pub fn inner(&self) -> &str {
&self.body
}
/// Get the body part of file extension (with leading dot)
///
/// This struct promise that there is no possibility that return value is dot only
/// (i.e. the body of file extension is empty).
pub fn dotted_inner(&self) -> String {
// Reuse Display trait result
self.to_string()
@@ -69,7 +83,7 @@ impl Ext {
/// The error occurs when try parsing string into FileExt.
#[derive(Debug, TeError)]
#[error("given file extension name \"{inner}\" is invalid")]
#[error(r#"given file extension name "{inner}" is invalid"#)]
pub struct ParseExtError {
/// The clone of string which is not a valid file extension.
inner: String,
@@ -110,7 +124,7 @@ impl FromStr for Ext {
/// The error occurs when constructing ProgId.
#[derive(Debug, TeError)]
#[error("given ProgId part \"{inner}\" is invalid")]
#[error(r#"given ProgId part "{inner}" is invalid"#)]
pub struct BadProgIdPartError {
/// The clone of string which is not a valid ProgId part.
inner: String,
@@ -127,16 +141,21 @@ impl BadProgIdPartError {
/// The ProgId exactly follows Microsoft suggested
/// `[Vendor or Application].[Component].[Version]` format.
/// And additionally, `[Version]` part is optional.
///
/// Additionally, `[Version]` part is optional.
///
/// However, most of applications do no follow this standard,
/// this scenario is not convered by this struct in there.
/// In reality world, most of applications do no follow this standard.
/// However, this scenario is not convered by this struct in there.
/// It should be done by other structs in other places.
///
/// If you have a ProgId expressed in string form, please utilize [FromStr] trait to parse it.
/// Or use [ProgId::new] with each parts to construct this struct.
///
/// The [Display] trait this struct implemented always
/// outputs legal ProgId in string form.
///
/// Reference:
/// - https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids
/// - https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
/// - <https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids>
/// - <https://learn.microsoft.com/en-us/windows/win32/com/-progid--key>
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgId {
/// The vendor part of ProgId.
@@ -149,6 +168,9 @@ pub struct ProgId {
impl ProgId {
/// Create a new ProgId with given parts.
///
/// Empty string or string with embedded dot is not allowed
/// for vendor and component parts.
pub fn new(
vendor: &str,
component: &str,
@@ -186,7 +208,7 @@ impl ProgId {
/// The error occurs when parsing ProgId.
#[derive(Debug, TeError)]
#[error("given ProgId \"{inner}\" is invalid")]
#[error(r#"given ProgId "{inner}" is invalid"#)]
pub struct ParseProgIdError {
/// The clone of string which is not a valid ProgId.
inner: String,
@@ -245,6 +267,14 @@ impl FromStr for ProgId {
/// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive).
///
/// Please note that the curly brace is the essential part of CLSID.
///
/// If you have a CLSID expressed in string form,
/// please utilize [FromStr] trait to parse it.
/// You also can utilize [Clsid::new] with [uuid] crate to create a custom CLSID,
/// or directly use [Clsid::with_random] to create a random new one.
///
/// The [Display] trait this struct implemented always
/// output CLSID in lower-case with curly brace.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Clsid {
inner: Uuid,
@@ -252,7 +282,7 @@ pub struct Clsid {
impl Clsid {
/// Create new CLSID from underlying UUID.
fn new(uuid: &Uuid) -> Self {
pub fn new(uuid: &Uuid) -> Self {
Self { inner: *uuid }
}
@@ -264,7 +294,7 @@ impl Clsid {
/// The error occurs when parsing CLSID.
#[derive(Debug, TeError)]
#[error("given string \"{inner}\" is invalid for uuid")]
#[error(r#"given string "{inner}" is invalid for CLSID"#)]
pub struct ParseClsidError {
/// The clone of string which is not a valid CLSID.
inner: String,
@@ -301,7 +331,7 @@ impl Display for Clsid {
/// Error occurs when given string is not a valid Icon Reference String.
#[derive(Debug, TeError)]
#[error("given string \"{inner}\" is not a valid Icon Reference String")]
#[error(r#"given string "{inner}" is not a valid Icon Reference String"#)]
pub struct ParseIconRefStrError {
/// The clone of string which is not a valid Icon Reference String
inner: String,
@@ -316,11 +346,22 @@ impl ParseIconRefStrError {
}
}
// TODO:
// IconRefStr should process the quote issue for path part in itself,
// rather than fetching its path part and resolve quote issue in another place.
/// The struct representing an Icon Reference String
/// looks like `%SystemRoot%\System32\imageres.dll,-72`.
///
/// As far as I know, the minus token `-` does nothing in this string.
/// The following number is just the index.
///
/// If you have a Icon Reference String,
/// please utilize [FromStr] trait to parse it into this struct.
/// You also can use [IconRefStr::new] with path and index parts to build this struct.
///
/// The [Display] trait this struct implemented always
/// outputs Icon Reference String in string form.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IconRefStr {
/// The path part of this reference string.
@@ -352,6 +393,12 @@ impl IconRefStr {
/// Get the path part of this reference string.
///
/// This path can be absolute path, relative path or expandable path.
///
/// # Todo
///
/// Currently this return value may be quoted path.
/// You may need manually unquote it.
/// This issue will be resolved in furture.
pub fn get_path(&self) -> &str {
&self.path
}
@@ -395,7 +442,7 @@ impl FromStr for IconRefStr {
/// Error occurs when given string is not a valid String Reference String.
#[derive(Debug, TeError)]
#[error("given string \"{inner}\" is not a valid String Reference String")]
#[error(r#"given string "{inner}" is not a valid String Reference String"#)]
pub struct ParseStrRefStrError {
/// The clone of string which is not a valid String Reference String
inner: String,
@@ -410,11 +457,22 @@ impl ParseStrRefStrError {
}
}
// TODO:
// StrRefStr should process the quote issue for path part in itself,
// rather than fetching its path part and resolve quote issue in another place.
/// The struct representing an String Reference String
/// looks like `@%SystemRoot%\System32\shell32.dll,-30596`.
///
/// As far as I know, the minus token `-` does nothing in this string.
/// The following number is just the index.
///
/// If you have a String Reference String,
/// please utilize [FromStr] trait to parse it into this struct.
/// You also can use [StrRefStr::new] with path and index parts to build this struct.
///
/// The [Display] trait this struct implemented always
/// outputs String Reference String in string form.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StrRefStr {
/// The path part of this reference string.
@@ -440,6 +498,12 @@ impl StrRefStr {
/// Get the path part of this reference string.
///
/// This path can be absolute path, relative path or expandable path.
///
/// # Todo
///
/// Currently this return value may be quoted path.
/// You may need manually unquote it.
/// This issue will be resolved in furture.
pub fn get_path(&self) -> &str {
&self.path
}
@@ -504,7 +568,16 @@ pub enum IconSizeKind {
Large,
}
/// The struct representing a loaded icon resource.
/// The struct representing a loaded Win32 icon resource.
///
/// You can use [IconRc::new] or [IconRc::with_ico_file] to create this resource.
///
/// The ownership of loaded Win32 icon resource is managed by this struct,
/// and will be free once this struct was destroyed.
///
/// According to Microsoft manual,
/// Win32 icon resource is program scope resource,
/// so it can be safely used between different threads.
#[derive(Debug)]
pub struct IconRc {
icon: HICON,
@@ -526,6 +599,9 @@ impl IconRc {
impl IconRc {
/// Load icon from executable or `.ico` file.
///
/// Please note that the content of `file` parameter must NOT be
/// expandable string or quoted string.
///
/// If you want to extract icon from `.ico` file, please pass `0` to `index` parameter.
/// Otherwise `index` is the icon resource index located in executable.
@@ -562,16 +638,28 @@ impl IconRc {
Self::new(file, 0, kind)
}
/// Construct this struct in raw Win32 icon handle.
///
/// Please note that after calling this function,
/// the ownership of given Win32 icon handle are managed by this struct.
pub unsafe fn from_raw(hicon: HICON) -> Self {
Self { icon: hicon }
}
pub fn into_raw(self) -> HICON {
self.icon
/// Consume and leak this struct held Win32 icon handle.
///
/// The ownership of this Win32 icon handle will be transfered to the caller of this function.
pub fn into_raw(mut self) -> HICON {
let rv = self.icon;
self.icon = std::ptr::null_mut();
rv
}
}
impl IconRc {
/// Get underlying Win32 icon handle.
///
/// Please note that the ownership of this handle are still in this struct.
pub fn get_icon(&self) -> HICON {
self.icon
}
@@ -622,12 +710,18 @@ pub enum LoadStrRcError {
}
/// The struct representing a loaded string resource.
///
/// You can use [StrRc::new] to create this resource.
#[derive(Debug)]
pub struct StrRc {
inner: String,
}
impl StrRc {
/// Load icon from executable file.
///
/// Please note that the content of `file` parameter must NOT be
/// expandable string or quoted string.
pub fn new(file: &str, index: u32) -> Result<Self, LoadStrRcError> {
use windows_sys::Win32::Foundation::FreeLibrary;
use windows_sys::Win32::System::LibraryLoader::{
@@ -673,10 +767,12 @@ impl StrRc {
}
impl StrRc {
/// Get fetched string resource.
pub fn get_string(&self) -> &str {
&self.inner
}
/// Consume and return fetched string resource.
pub fn into_string(self) -> String {
self.inner
}
@@ -717,9 +813,15 @@ pub enum ExpandEnvVarError {
NoEnvVar,
}
/// The struct representing an Expand String,
/// The struct representing an expandable string,
/// which contain environment variable in string,
/// like `%LOCALAPPDATA%\SomeApp.exe`.
///
/// You can use [ExpandString::new] or [FromStr] trait to create an expandable string,
/// and use [ExpandString::expand] to try expanding it.
///
/// The [Display] trait this struct implemented always
/// outputs original expandable string.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExpandString {
inner: String,
@@ -737,7 +839,7 @@ impl ExpandString {
}
/// Expand the variables located in this string
/// and produce the final usable string.
/// and produce the final string without having any embedded variable syntax.
pub fn expand(&self) -> Result<String, ExpandEnvVarError> {
use windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW;
@@ -798,9 +900,15 @@ impl FromStr for ExpandString {
// region: File Name
/// The error occurs when constructing FileName with bad file name.
pub type BadFileNameError = ParseFileNameError;
/// The struct representing a legal Windows file name.
///
/// You can utilize [FileName::new] or [FromStr] trait to create this struct.
///
/// The [Display] trait this struct implemented always
/// outputs the file name in string form.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileName {
/// The validated legal Windows file name.
@@ -824,7 +932,7 @@ impl FileName {
}
}
/// The error occurs when constructing FileName with bad file name.
/// The error occurs when parsing FileName with bad file name.
#[derive(Debug, TeError)]
#[error("given file name is illegal in Windows")]
pub enum ParseFileNameError {
@@ -871,9 +979,15 @@ impl FromStr for FileName {
// region: Verb
/// The error occurs when constructing Verb with bad verb name.
pub type BadVerbError = ParseVerbError;
/// The struct representing a verb when manipulating file
///
/// You can utilize [Verb::new] or [FromStr] trait to create this struct.
///
/// The [Display] trait this struct implemented always
/// outputs the verb in string form.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Verb {
/// The validated verb name.
@@ -915,9 +1029,9 @@ impl Verb {
}
}
/// The error occurs when constructing Verb with bad verb name.
/// The error occurs when parsing Verb with bad verb name.
#[derive(Debug, TeError)]
#[error("given verb \"{inner}\" is illegal")]
#[error(r#"given verb "{inner}" is illegal"#)]
pub struct ParseVerbError {
inner: String,
}
@@ -956,52 +1070,106 @@ impl FromStr for Verb {
// region: Command Line
// TODO:
// Introduce CmdArg in future to describe single Windows command line argument.
// And use CmdArg to finish the full functionality of CmdLine.
/// The error occurs when constructing CmdLine with bad arguments.
#[derive(Debug, TeError)]
#[error("given file extension body \"{inner}\" is invalid")]
#[error(r#"given command line argument "{inner}" is invalid"#)]
pub struct BadCmdLineError {
inner: String,
}
impl BadCmdLineError {
// TODO: Remove this dead_code attribute once we need use this error type.
#[allow(dead_code)]
fn new(inner: &str) -> Self {
Self {
inner: inner.to_string(),
}
}
}
/// The struct representing a Windows command line.
///
/// If you have a complete Windows command line expressed in string form,
/// you can utilize [FromStr] trait to create this struct.
/// Or, you can utilize [CmdLine::new] to create this struct with command arguments one by one.
///
/// The [Display] trait this struct implemented always
/// outputs a legal Windows command line in string form,
/// which combine all arguments together.
///
/// # Note
/// # Todo
///
/// This struct currently does nothing for validation for given command line.
/// Because there is no standard for validating this.
/// So I just write this struct as a placeholder for future extension.
///
/// Currently if you are using [FromStr] trait to parse command line,
/// it will simply split it with space as separator.
/// This may cause unexpected behavior such as empty argument or unexpected break in argument.
/// This issue will be resolved in future.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CmdLine {
inner: Vec<String>,
}
impl CmdLine {
/// Create a new command line.
/// Create a new command line with arguments one by one.
///
/// Please note that passed command line argument must be unquoted.
///
/// # Todo
///
/// Currently, there is no unquote restrictions for those passed arguments.
/// These passed argument will be honestly used in full command line.
/// So if your argument require quote, please quote them at first.
/// This issue will be resolved in future.
pub fn new<S: AsRef<str>>(args: &[S]) -> Result<Self, BadCmdLineError> {
Ok(Self {
inner: args.iter().map(|s| s.as_ref().to_string()).collect(),
})
}
/// Get the full command line.
/// Get the full command line in string form with proper quote.
///
/// # Todo
///
/// Currently this function only simply returns
/// the joined command line arguments with space as separator.
/// There is no guarantee about safe quote.
/// This issue will be resolved in future.
pub fn full(&self) -> String {
self.inner.join(" ")
}
/// Get the iterator of command line arguments.
///
/// Each returned argument are unquoted.
///
/// # Todo
///
/// Currently there is no guarantee about the unquote behavior
/// for returned command line arguments.
/// It simply returns the content of arguments list passed to constructor.
/// This issue will be resolved in future.
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.inner.iter().map(|s| s.as_str())
}
}
/// The error occurs when constructing CmdLine with bad syntax.
/// The error occurs when parsing CmdLine with bad syntax.
#[derive(Debug, TeError)]
#[error("given command line \"{inner}\" is invalid")]
#[error(r#"given command line "{inner}" is invalid"#)]
pub struct ParseCmdLineError {
inner: String,
}
impl ParseCmdLineError {
// TODO: Remove this dead_code attribute once we need use this error type.
#[allow(dead_code)]
fn new(inner: &str) -> Self {
Self {
inner: inner.to_string(),
@@ -1025,3 +1193,5 @@ impl FromStr for CmdLine {
})
}
}
// endregion
+80 -20
View File
@@ -1,8 +1,6 @@
//! This module extend `winreg` crate to make it more suit for the usage of this crate.
use std::ffi::OsStr;
use std::ops::Deref;
use std::ops::DerefMut;
use thiserror::Error as TeError;
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows_sys::Win32::System::Registry::REG_SAM_FLAGS;
@@ -12,13 +10,13 @@ use winreg::types::FromRegValue;
// region: Extra Operations
/// Get the subkey with given name.
/// Try getting the subkey with given name.
///
/// If error occurs when fetching given subkey, it return `Err(...)`,
/// otherwise, it will return `Ok(Some(...))` if subkey is existing,
/// or `Ok(None)` if there is no suchsub key.
///
/// Comparing with the function provided by winreg,
/// Comparing with the function provided by [winreg],
/// it differ "no such subkey" error and other access error.
pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
regkey: &RegKey,
@@ -37,13 +35,13 @@ pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
}
}
/// Get the value by given key.
/// Try getting the value by given key.
///
/// If error occurs when fetching given key, it return `Err(...)`,
/// otherwise, it will return `Ok(Some(...))` if key is existing,
/// or `Ok(None)` if there is no such key.
///
/// Comparing with the function provided by winreg,
/// Comparing with the function provided by [winreg],
/// it differ "no such key" error and other access error.
pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
regkey: &RegKey,
@@ -61,13 +59,65 @@ pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
}
}
/// Delete all tree of given path of given key anyway.
///
/// This function was invented to fix the shortcoming of [RegKey::delete_subkey_all].
/// This function always delete given path of given key no matter it is existing.
/// Oppositely, [RegKey::delete_subkey_all] will return error if there is no such path.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn arbitrarily_delete_subkey_all<P: AsRef<OsStr>>(
regkey: &RegKey,
path: P,
) -> std::io::Result<bool> {
match regkey.delete_subkey_all(path) {
Ok(()) => Ok(true),
Err(e) => match e.raw_os_error() {
Some(errno) => match errno as u32 {
ERROR_FILE_NOT_FOUND => Ok(false),
_ => Err(e),
},
_ => Err(e),
},
}
}
/// Delete given value key of given key anyway.
///
/// This function was invented to fix the shortcoming of [RegKey::delete_value].
/// This function always delete given value key of given key no matter it is existing.
/// Oppositely, [RegKey::delete_value] will return error if there is no such value key.
///
/// Return true if we successfully delete this value key,
/// otherwise false indicating there is no such value key (already deleted).
pub fn arbitrarily_delete_value<N: AsRef<OsStr>>(
regkey: &RegKey,
name: N,
) -> std::io::Result<bool> {
match regkey.delete_value(name) {
Ok(()) => Ok(true),
Err(e) => match e.raw_os_error() {
Some(errno) => match errno as u32 {
ERROR_FILE_NOT_FOUND => Ok(false),
_ => Err(e),
},
_ => Err(e),
},
}
}
// TODO:
// Once we support multiple ShellVerb,
// it seems that there is no need to preserve this function.
/// Get the name of only subkey in given key.
///
/// If there is only one subkey in given key, the return value is its name.
/// If there is no any subkey, or has multiple subkeys, return None instead.
/// If error occurs when fetching data, return Err(_).
///
/// This is usually used for ShellVerb fetching.
/// If error occurs when fetching data, return `Err(_)`.
///
/// This is used as fetching only one ShellVerb.
pub fn get_sole_subkey_name(regkey: &RegKey) -> std::io::Result<Option<String>> {
let mut subkey_enumerator = regkey.enum_keys();
@@ -85,8 +135,8 @@ pub fn get_sole_subkey_name(regkey: &RegKey) -> std::io::Result<Option<String>>
}
/// Get the name list of all "string" subkeys in given key.
///
/// This is usually used for "OpenWithProgIds" subkey.
///
/// This is used in "OpenWithProgIds" subkey.
pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<String>> {
regkey
.enum_values()
@@ -105,11 +155,14 @@ pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<Strin
}
/// Delete all contents, including values and subkeys of given key.
///
///
/// Deleting all contents of given key rely on giving a special parameter to [RegKey::delete_subkey_all].
/// This is very dangerous and may be used by accident.
/// So I create this to explicitly indicate this behavior and avoid any mis-type in code.
pub fn clean_all_contents(regkey: &RegKey) -> std::io::Result<()> {
// There is no possibility that this key do not existing,
// because what we are cleaning is self content.
// So directly use delete_subkey_all is okey.
regkey.delete_subkey_all("")
}
@@ -129,15 +182,15 @@ impl BlankPathError {
}
/// Check whether given registry path is empty.
/// If it is, return error, otherwise the given path.
/// If it is, return error, otherwise the given path itself.
///
/// Passing empty path for some registry functions is dangerous.
/// Because it will cause unexpected behavior that returning key self, rather than subkey.
/// This is VERY dangerous especially for those registry delete functions.
/// So I create this function to prevent any harmful blank path was passed into registry function.
///
/// This function MUST be used for the value, whose content can not be confirmed at compile time,
/// and it will be passed to get/set value, or create/delete key functions.
/// Passing empty path for some [winreg] functions is dangerous, expecially for those registry delete functions.
/// Passing empty path to registry creation function will return themselves,
/// and passing them to registry delete functions will delete all contents of them!
/// So I create this function to prevent any harmful blank path was passed into [winreg] function.
///
/// This function MUST be used for wrapping the value, whose content can not be confirmed at compile time,
/// if that value will be passed to get/set value, or create/delete key functions.
pub fn blank_path_guard<P: AsRef<OsStr>>(path: P) -> std::result::Result<P, BlankPathError> {
if path.as_ref().is_empty() {
Err(BlankPathError::new())
@@ -150,6 +203,11 @@ pub fn blank_path_guard<P: AsRef<OsStr>>(path: P) -> std::result::Result<P, Blan
// region: Expand String
// TODO:
// Re-enable following code when we need to distinguish between REG_SZ and REG_EXPAND_SZ.
/*
/// The struct basically is the alias of String, but make a slight difference with it,
/// to make they are different when use it with String as generic argument.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -217,4 +275,6 @@ impl From<&str> for ExpandString {
}
}
*/
// endregion
+6 -1
View File
@@ -7,7 +7,12 @@
/// It usually means that checking whether current process is running as Administrator.
/// Return true if it is, otherwise false.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
/// Reference: <https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership>
///
/// # Panics
///
/// This function will panic if any Windows functions failed unexpectedly.
/// This is a very common check and in theory there is no any possibility that this check will fail.
pub fn has_privilege() -> bool {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::{
+13 -2
View File
@@ -9,8 +9,19 @@ pub fn check_sandbox() {
std::env::var("SANDBOXIE").is_ok(),
concat!(
"Non-sandbox environment detected. ",
"Executing these test in non-sandbox environment is VERY dangerous. ",
"Please set \"SANDBOXIE\" environment variable to explicitly indicate you are running these test in sandbox environment."
"Executing these tests in non-sandbox environment is VERY dangerous. ",
r#"Please set "SANDBOXIE" environment variable to explicitly indicate you are running these tests in sandbox environment."#
)
)
}
pub fn check_privilege() {
assert!(
wfassoc::win32::utilities::has_privilege(),
concat!(
"You are running test without privilege. ",
"These tests must be run with some privilege because it need to manipulate Windows Registry. ",
"Please give it privilege in your sandbox environment."
)
)
}
+5
View File
@@ -318,12 +318,17 @@ fn test_verb() {
#[test]
fn test_cmd_line() {
// TODO:
// Remove error tester dead_code attribute,
// improve ok tester and add more error tests
// once we finish a complete CmdLine.
fn ok_tester(s: &str) {
let rv = CmdLine::from_str(s);
assert!(rv.is_ok());
// let rv = rv.unwrap();
// assert_eq!(s, rv.full());
}
#[allow(dead_code)]
fn err_tester(s: &str) {
let rv = CmdLine::from_str(s);
assert!(rv.is_err());
+7 -7
View File
@@ -13,8 +13,8 @@ fn make_valid_schema() -> Schema {
schema.set_clsid(CLSID);
schema.add_str("main_name", "Passoc Application").unwrap();
schema.add_str("ext_name", "Pacfg File").unwrap();
schema.add_icon("main_icon", r"notepad.exe,0").unwrap();
schema.add_icon("ext_icon", r"notepad.exe,0").unwrap();
schema.add_icon("main_icon", "notepad.exe,0").unwrap();
schema.add_icon("ext_icon", "notepad.exe,0").unwrap();
schema.add_behavior("main_behavior", "notepad.exe %1").unwrap();
schema.add_behavior("ext_behavior", "notepad.exe %1").unwrap();
schema.set_name(Some("main_name"));
@@ -29,21 +29,20 @@ fn make_valid_schema() -> Schema {
#[test]
fn test_schema() {
common::check_sandbox();
common::check_privilege();
// valid schema -> valid program
let schema = make_valid_schema();
let rv = schema.into_program();
assert!(rv.is_ok());
// missing identifier
let mut schema = Schema::new();
schema.set_path(APP_PATH);
// missing essential parts (schema, path and etc)
let schema = Schema::new();
let rv = schema.into_program();
assert!(rv.is_err());
// invalid path
let mut schema = Schema::new();
schema.set_identifier(IDENTIFIER);
let mut schema = make_valid_schema();
schema.set_path(r"C:\");
let rv = schema.into_program();
assert!(rv.is_err());
@@ -93,6 +92,7 @@ fn test_schema() {
#[test]
fn test_program() {
common::check_sandbox();
common::check_privilege();
fn tester(scope: Scope, view: View) {
// build program
+4
View File
@@ -22,6 +22,7 @@ static VERB: LazyLock<ShellVerb> = LazyLock::new(|| {
#[test]
fn test_app_paths_key() {
common::check_sandbox();
common::check_privilege();
static APP_PATH: &str = r"C:\Program Files\Passoc\passoc.exe";
static APP_DIR: &str = r"C:\Program Files\Passoc";
@@ -75,6 +76,7 @@ fn test_app_paths_key() {
#[test]
fn test_applications_key() {
common::check_sandbox();
common::check_privilege();
static FRIENDLY_APP_NAME: LazyLock<StrResVariant> =
LazyLock::new(|| "Passoc Application".into());
@@ -177,6 +179,7 @@ fn test_applications_key() {
#[test]
fn test_ext_key() {
common::check_sandbox();
common::check_privilege();
fn tester(scope: Scope, view: View) {
let mut key = ExtKey::new(EXT.clone());
@@ -246,6 +249,7 @@ fn test_ext_key() {
#[test]
fn test_prog_id_key() {
common::check_sandbox();
common::check_privilege();
static LEGACY_NAME: LazyLock<StrResVariant> = LazyLock::new(|| "Passoc Pacfg File".into());
static FRIENDLY_TYPE_NAME: LazyLock<StrResVariant> =