Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
- uses: actions/checkout@v6
with:
submodules: true
# git-cliff reads the tags and commits since the previous release.
fetch-depth: 0
- uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
Expand All @@ -39,8 +41,15 @@ jobs:
# Git Bash on Windows prints "<hash> *<file>"; write the text-mode form the app verifies.
printf '%s %s\n' "$(sha256sum "$asset" | cut -d ' ' -f 1)" "$asset" > "$asset.sha256"
cat "$asset.sha256"
- name: Write the release notes
uses: orhun/git-cliff-action@v4
with:
args: --current --strip header
env:
OUTPUT: release-notes.md
- uses: softprops/action-gh-release@v3
with:
body_path: release-notes.md
files: |
enableLoopback-${{ matrix.target }}.exe
enableLoopback-${{ matrix.target }}.exe.sha256
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Changelog

Generated from the commit history by [git-cliff](https://git-cliff.org).

## Unreleased

- License the project under MIT ([a68bbb1](https://github.com/libnyanpasu/enable-loopback/commit/a68bbb15b9667dafabcacd728e62996cd1bd205f))
- Center the window on the monitor under the cursor ([be9e97f](https://github.com/libnyanpasu/enable-loopback/commit/be9e97fcbeb869f0f9e74b667c289c597a48f07d))
- Start the Binaries column at 200 DIP ([4a23be3](https://github.com/libnyanpasu/enable-loopback/commit/4a23be33a9e6fca898f25331ddfd3bc816498582))
- Scroll the table in its list so both scroll bars stay reachable ([b676214](https://github.com/libnyanpasu/enable-loopback/commit/b6762145a0785cc526fbec5aa8c47d56266d2369))
- Let rows follow a column resize while dragging ([3191842](https://github.com/libnyanpasu/enable-loopback/commit/31918424f02562b8366b7bd50302fc4ad601f31d))
- Keep the table's scroll position when its rows are replaced ([f6adeec](https://github.com/libnyanpasu/enable-loopback/commit/f6adeec373fb328cc191c83ea0c22b76e2afa822))
- Build table rows only near the list's viewport ([5174e44](https://github.com/libnyanpasu/enable-loopback/commit/5174e443aa44b3f7ca69e3d7048ae98fef82f354))
- Create cell tooltips only for the hovered row ([21a257a](https://github.com/libnyanpasu/enable-loopback/commit/21a257af34f4552f15d1eb9d42be4bc3a741311b))

## [0.1.3](https://github.com/libnyanpasu/enable-loopback/releases/tag/v0.1.3) - 2026-10-21

- Link the C runtime statically ([7fdcd62](https://github.com/libnyanpasu/enable-loopback/commit/7fdcd62d260f51c008e3df630f9d377d69144522))
- Offer to install Windows App Runtime from the Microsoft Store ([ab92f4e](https://github.com/libnyanpasu/enable-loopback/commit/ab92f4e88df86b4d3b28c8508129611f8bd57c92))
- Opt the executable into the segment heap ([1a8de54](https://github.com/libnyanpasu/enable-loopback/commit/1a8de54613f35964f942563510c49f3048f75878))

## [0.1.2](https://github.com/libnyanpasu/enable-loopback/releases/tag/v0.1.2) - 2026-10-21

- Perf reduce release executable size ([#3](https://github.com/libnyanpasu/enable-loopback/pull/3)) ([71f9cf3](https://github.com/libnyanpasu/enable-loopback/commit/71f9cf3ecf8c6bf93029ad0a0b4b1bb54fb54729))

## [0.1.1](https://github.com/libnyanpasu/enable-loopback/releases/tag/v0.1.1) - 2026-10-20

- Open the row context menu on right-button release ([71c28de](https://github.com/libnyanpasu/enable-loopback/commit/71c28de866e1eb7869ccf9bad3ebde943d86363b))

## [0.1.0](https://github.com/libnyanpasu/enable-loopback/releases/tag/v0.1.0) - 2026-10-20

- Scaffold the localized WinUI loopback tool ([4ddb6d0](https://github.com/libnyanpasu/enable-loopback/commit/4ddb6d08b428ef2c9367c6c79fa19f2adb1c958e))
- Check formatting, Clippy, and tests on Windows CI ([f463b63](https://github.com/libnyanpasu/enable-loopback/commit/f463b637202807d146f2e796389fcaed9c3713b2))
- Implement owned NetIso FFI and registry enumeration ([b2e28d3](https://github.com/libnyanpasu/enable-loopback/commit/b2e28d30c796afe708d7615030f1a13fccaaed5e))
- Implement pure list rendering and exemption rules ([dfb38bd](https://github.com/libnyanpasu/enable-loopback/commit/dfb38bd8af0d4b02b2201ea2ea325ca6226dbc80))
- Add SID-based list selection state ([a727edb](https://github.com/libnyanpasu/enable-loopback/commit/a727edb8bed4418d581b7db7ad5c9d4afecd8487))
- Execute CLI exemption commands without WinAppSDK ([69d3c7b](https://github.com/libnyanpasu/enable-loopback/commit/69d3c7be905fbbf0904d9e37fee97b0ce219eb38))
- Enforce GUI single instance and publish its window handle ([b5e6603](https://github.com/libnyanpasu/enable-loopback/commit/b5e66035a075d0c777467c550e5a9e4fc0c4cccd))
- Build the AppContainer list and asynchronous refresh flow ([89cb09f](https://github.com/libnyanpasu/enable-loopback/commit/89cb09f47789987635bd44fe4171bc1b3b05cfc4))
- Wire row selection and context menu clipboard actions ([92c0e69](https://github.com/libnyanpasu/enable-loopback/commit/92c0e69ad6e1ce3abb4820c988dc6bec8929913b))
- Save checked exemptions without losing failed edits ([8a30beb](https://github.com/libnyanpasu/enable-loopback/commit/8a30beb81576e7737ac3ff990d86c3ffdd89f95a))
- Finish close confirmation, keyboard shortcuts and help ([f408ba7](https://github.com/libnyanpasu/enable-loopback/commit/f408ba7b34d4fc6b142bf7bf7b016d54aa38048b))
- Publish x64 and arm64 executables with checksums on tags ([88633e9](https://github.com/libnyanpasu/enable-loopback/commit/88633e9d85b320a8671dfc57aa95a5f2567af093))
- Build against the vendored libnyanpasu windows-rs fork ([725bd80](https://github.com/libnyanpasu/enable-loopback/commit/725bd80be02c1520b29f90aec54fa00d6fb5df80))
- Rework the window around a TableView component with search ([94fad55](https://github.com/libnyanpasu/enable-loopback/commit/94fad55c5e1734e8d64515f03448f4dc9a361d49))
- Mark the design as implemented ([9f87f6a](https://github.com/libnyanpasu/enable-loopback/commit/9f87f6aa86cf76b61d311ccae19c67f90821857e))

6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ Run tests with `cargo test`; only the GUI executable embeds the elevation manife
The vendored windows-rs fork is a separate workspace. Check application formatting
with `cargo fmt -p enable-loopback -- --check`; `cargo fmt --all` recursively visits
the fork too and can exceed Windows' command-line limit.

To release, set the version in `crates/enable-loopback/Cargo.toml`, regenerate
[CHANGELOG.md](CHANGELOG.md) with [git-cliff](https://git-cliff.org) as
`git cliff --tag vX.Y.Z -o CHANGELOG.md`, commit both with `Cargo.lock` as
`Release X.Y.Z`, and push the `vX.Y.Z` tag. The release workflow builds the
executables and uses the version's changelog section as the release notes.
37 changes: 37 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# git-cliff configuration: https://git-cliff.org/docs/configuration
# Regenerate CHANGELOG.md with `git cliff -o CHANGELOG.md` (add `--tag vX.Y.Z` before tagging).

[changelog]
header = """
# Changelog

Generated from the commit history by [git-cliff](https://git-cliff.org).
"""
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}](https://github.com/libnyanpasu/enable-loopback/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## Unreleased
{% endif %}
{% for commit in commits %}\
- {{ commit.message | split(pat="\n") | first | trim }} \
([{{ commit.id | truncate(length=7, end="") }}](https://github.com/libnyanpasu/enable-loopback/commit/{{ commit.id }}))
{% endfor %}
"""
trim = false
postprocessors = [
{ pattern = '\(#([0-9]+)\)', replace = "([#${1}](https://github.com/libnyanpasu/enable-loopback/pull/${1}))" },
]

[git]
# Subjects are imperative sentences, not Conventional Commits.
conventional_commits = false
filter_unconventional = false
commit_parsers = [
{ message = "^Release \\d", skip = true },
{ message = "^Merge (pull request|branch)", skip = true },
# The application commits that use a fork change describe it.
{ message = "^Bump windows-rs fork", skip = true },
]
tag_pattern = "v[0-9].*"
sort_commits = "oldest"
2 changes: 1 addition & 1 deletion crates/enable-loopback/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ nyanpasu-windows-dependencies = { git = "https://github.com/libnyanpasu/nyanpasu
snafu = "0.8"
sys-locale = "0.3"
windows-reactor = { path = "../../vendor/windows-rs/crates/libs/reactor" }
windows = { path = "../../vendor/windows-rs/crates/libs/windows", features = ["libloaderapi", "debugapi", "winuser", "shellapi", "commctrl", "winerror", "winnt", "netfw", "sddl", "secext", "securitybaseapi", "shlwapi", "winbase", "heapapi", "winreg", "errhandlingapi", "processenv", "synchapi", "memoryapi", "handleapi", "processthreadsapi", "datetimeapi", "sysinfoapi"] }
windows = { path = "../../vendor/windows-rs/crates/libs/windows", features = ["libloaderapi", "debugapi", "winuser", "shellscalingapi", "shellapi", "commctrl", "winerror", "winnt", "netfw", "sddl", "secext", "securitybaseapi", "shlwapi", "winbase", "heapapi", "winreg", "errhandlingapi", "processenv", "synchapi", "memoryapi", "handleapi", "processthreadsapi", "datetimeapi", "sysinfoapi"] }

[build-dependencies]
resvg = { version = "0.45", default-features = false }
Expand Down
8 changes: 4 additions & 4 deletions crates/enable-loopback/src/model/columns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub struct ColumnWidths {
impl Default for ColumnWidths {
fn default() -> Self {
Self::new(
[220.0, 220.0, 200.0, 160.0, 140.0, 140.0, MIN_COLUMN_WIDTH],
[220.0, 220.0, 200.0, 160.0, 140.0, 140.0, 200.0],
[MIN_COLUMN_WIDTH; 7],
)
}
Expand Down Expand Up @@ -112,12 +112,12 @@ mod tests {
}

#[test]
fn total_and_fill_preserve_fixed_columns_and_binaries_minimum() {
fn total_and_fill_preserve_stored_widths() {
let widths = ColumnWidths::default();
assert_eq!(widths.total(), 1168.0);
assert_eq!(widths.total(), 1320.0);
assert_eq!(
widths.resolved(900.0),
[40.0, 220.0, 220.0, 200.0, 160.0, 140.0, 140.0, 48.0]
[40.0, 220.0, 220.0, 200.0, 160.0, 140.0, 140.0, 200.0]
);
assert_eq!(widths.resolved(1400.0)[7], 280.0);
let mut resized = widths;
Expand Down
33 changes: 21 additions & 12 deletions crates/enable-loopback/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ use windows::{
use windows_reactor::*;

const PAGE_MARGIN: f64 = 24.0;
const WINDOW_WIDTH: f64 = 960.0;
const WINDOW_HEIGHT: f64 = 600.0;
const SECTION_SPACING: f64 = 12.0;
const SEARCH_WIDTH: f64 = 280.0;
const TOOLBAR_WRAP_WIDTH: f64 = 900.0;
Expand Down Expand Up @@ -83,6 +85,8 @@ struct MainWindow {
search_focused: bool,
selection: Selection,
hwnd: usize,
/// Read only from the first declaration; None keeps the system default.
initial_position: Option<ScreenPoint>,
close: native::CloseState,
hovering_help: bool,
version: String,
Expand Down Expand Up @@ -312,7 +316,7 @@ impl MainWindow {
"column.users",
"column.binaries",
];
let widths = [220.0, 220.0, 200.0, 160.0, 140.0, 140.0, 48.0];
let widths = [220.0, 220.0, 200.0, 160.0, 140.0, 140.0, 200.0];
component::<table::TableView>(
"table",
table::Input {
Expand Down Expand Up @@ -534,7 +538,7 @@ impl Component for MainWindow {
dialog_timer: None,
menu_timer: None,
list_generation: 0,
viewport_width: 960.0 - 2.0 * PAGE_MARGIN,
viewport_width: WINDOW_WIDTH - 2.0 * PAGE_MARGIN,
table_rows: Rc::new(Vec::new()),
keyboard_focus: false,
app: input.app.clone(),
Expand All @@ -543,6 +547,9 @@ impl Component for MainWindow {
search_focused: false,
selection: Selection::default(),
hwnd: 0,
initial_position: native::centered_position(WINDOW_WIDTH, WINDOW_HEIGHT)
.inspect_err(|error| crate::debug(&error.to_string()))
.ok(),
close,
hovering_help: false,
version: native::version().unwrap_or_else(|error| {
Expand Down Expand Up @@ -824,16 +831,18 @@ impl Component for MainWindow {
}
fn view(&self, _: &Startup, cx: &mut ViewContext<Self>) -> View {
cx.window_title(rust_i18n::t!("window.title"));
cx.window_visuals(
WindowVisuals::new()
.backdrop(WindowBackdrop::Mica)
.client_size(960.0, 600.0)
.constraints(WindowConstraints {
min_width: Some(640.0),
min_height: Some(400.0),
..Default::default()
}),
);
let mut visuals = WindowVisuals::new()
.backdrop(WindowBackdrop::Mica)
.client_size(WINDOW_WIDTH, WINDOW_HEIGHT)
.constraints(WindowConstraints {
min_width: Some(640.0),
min_height: Some(400.0),
..Default::default()
});
if let Some(position) = self.initial_position {
visuals = visuals.initial_position(position);
}
cx.window_visuals(visuals);
cx.on_window_size(cx.callback(Message::Size));
let mut status: Vec<View> = Vec::new();
if self.list.refreshing {
Expand Down
60 changes: 60 additions & 0 deletions crates/enable-loopback/src/ui/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,66 @@ pub(super) fn screen_point(
Ok(windows_reactor::ScreenPoint::new(point.x, point.y))
}

/// Top-left that centers a window with this DIP client size in the work area of the
/// monitor under the cursor, where the user launched it. ScreenPoint is in physical pixels.
pub(super) fn centered_position(
client_width: f64,
client_height: f64,
) -> Result<windows_reactor::ScreenPoint, Error> {
use windows::Win32::{
POINT, RECT,
shellscalingapi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI},
winuser::{
AdjustWindowRectExForDpi, GetCursorPos, GetMonitorInfoW, MONITOR_DEFAULTTONEAREST,
MONITORINFO, MonitorFromPoint, WS_OVERLAPPEDWINDOW,
},
};
let mut cursor = POINT::default();
// SAFETY: writable POINT.
if !unsafe { GetCursorPos(&mut cursor) }.as_bool() {
return Err(failure("GetCursorPos"));
}
// SAFETY: NEAREST always returns a monitor.
let monitor = unsafe { MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST as u32) };
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
// SAFETY: live monitor handle and a sized MONITORINFO.
if !unsafe { GetMonitorInfoW(monitor, &mut info) }.as_bool() {
return Err(failure("GetMonitorInfoW"));
}
let (mut dpi, mut unused) = (0, 0);
// SAFETY: live monitor handle and writable outputs.
unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi, &mut unused) }
.ok()
.map_err(|source| Error::Native {
operation: "GetDpiForMonitor",
source,
})?;
let scale = f64::from(dpi) / 96.0;
let mut outer = RECT {
left: 0,
top: 0,
right: (client_width * scale).round() as i32,
bottom: (client_height * scale).round() as i32,
};
// SAFETY: writable RECT; the style matches WinUI's overlapped window frame.
if !unsafe { AdjustWindowRectExForDpi(&mut outer, WS_OVERLAPPEDWINDOW as u32, false, 0, dpi) }
.as_bool()
{
return Err(failure("AdjustWindowRectExForDpi"));
}
let work = info.rcWork;
let width = outer.right - outer.left;
let height = outer.bottom - outer.top;
Ok(windows_reactor::ScreenPoint::new(
work.left + (work.right - work.left - width) / 2,
// A window taller than the work area keeps its caption reachable.
(work.top + (work.bottom - work.top - height) / 2).max(work.top),
))
}

/// All four flags are shared only by UI-thread code. Rc makes accidental Send impossible.
#[derive(Clone, Default)]
pub(super) struct CloseState {
Expand Down
Loading