0x2B|~0x2B

My broken wings still strong enough to cross the ocean with.

It seems vscode shortened its release cycle starting March 2026,in version 1.111. Two notable enhancements regarding markdown:

  • In 1.121(May 2026), it added preview for mermaid diagrams.
  • In 1.131(Jul 2026), it added an experimental hybrid markdown editor. It is a in-place WYSIWYG markdown editor.

With these changes, vscode now has comparable basic edit experience to typora, and it is free.

If you found yourself ever missed some hidden items or equipment/inventory, you probably do not want to start a new game. Moreover, one missable is only available during H'aanit chapter 1. It’s the very start of the game, and it’s in my case unfortunately. But I was able to fix the saves to complete both Eagle-Eyed and Collector achievements.

1. Fixing Missable Hidden Items

The save files are located in %USERPROFILE%\Documents\My Games\Octopath_Traveler\<user-id>\SaveGames\, they are in UE4 GVAS format. I have no idea what exactly the format is structured, but it seems to be a plain, binary, casscading container format. The data is not compressed, which means I can use a binary editor like imhex to modify it directly.

Now open the save you want to modify using imhex, search by string with BitFlag. Scroll down to find a binary sequence starting with DE. It is a 32-byte sequence, several hundreds of bytes before you can see EventCheckFlag section. It’s not a fixed offset, but around offset 00205B80 in most cases.

First, the Inspiriting Plum (M) only available during H'aanit chapter 1, in S'warkii of Woodlands. Modify the 29th byte to value|0x20. For me, I modified it from 0x5F to 0x7F.

Second, the Heavy Coin Pouch only available after Olberic chapter 3 but before chapter4, in Wellspring of Sunlands. Modify the 19th byte to value|0x04. For me, I modified it from 0xF8 to 0xFC.

Third, the Inspiriting Plum (M) only available during quest Arianna Again (I) & Arianna Again (II) in Trail to the Whitewood of Frostlands. Modify the 4th byte to value|0x80. For me, I modified it from 0x03 to 0x83.

octopath_imhex1

Now, open the modified save, travel to these maps. Hidden items are revealed directly, no more information gleaning.

octopath_woodlands

octopath_sunlands

octopath_frostlands

I did collect Sleepweed and Olive Bloom in Clearbrook of Riverlands. If you have missed them, continue reading the 3rd part of the article.

2. Fixing Missable Equipment/Inventory

Open the save you want to modify using imhex, search by string with ItemFlag. Scroll down to find last 2 byte sequences before you can see FreeFlag section. It’s not a fixed offset, but around offset 002082B0 in most cases.

First, Obsidian Garb only can be stolen in boss fight of Primrose chaper 3. Modify the 4th byte in the 2nd last byte sequence to value|0x04. For me, I modified it from 0xFB to 0xFF.

Second, Physical Belt and Mental Belt can be stolen in boss fight of Primrose chaper 4. Though they can also be collected via challenge and provoke. Modify the 2nd byte in the last byte sequence to value|0xC0(0x40 for Physical Belt and 0x80 for Mental Belt). For me, I modified it from 0xBF to 0xFF. See the screenshot for reference(red marked).

octopath_imhex2

The byte changes only affect achievement. You do not actually own them in your inventory. You can add them to your inventory by using OctopathTraveler-SaveDataEditor.

3. How to Find Those Bytes

Well, that’s simple. Load the save which is the last chance to collect an item, collect it, and save to another one. Now compare these 2 saves and find the diff bytes in the BitFlag and ItemFlag sections.

0. Environment

  • Hardware
    • System: 2023 version of Lenovo XiaoXin(IdeaPad) Pro 14
    • CPU: AMD 7840HS (Zen4)
    • GPU: AMD 780M iGPU (RDNA3) with 8G vRAM setup in UEFI
    • Memory: 24G (32G total, 8G is assigned to iGPU in UEFI)
  • Windows 11 25H2
    • AMD graphic driver 2025.9.1
  • Room Temperature
    • ~25°C

1. ITS Settings

Do install Lenovo Vantage app or some equivalents to adjust ITS(Intelligent Thermal System) settings. It is the most important. It has higher priority than Windows settings. I’m using ck3 and victoria3 for benchmarking.

Game CPU Voltage (v) CPU Power (w) CPU Max Frequency (MHz) CPU Temperature (°C)
ck3 (original) 1.1-1.2 35-40 ~4.6 80-90
victoria3 (original) 1.1-1.2 35-40 ~4.6 80-90
ck3 (after) 0.6-0.8 15-20 ~4.6 55-65
victoria3 (after) 0.6-0.8 15-20 ~4.6 55-65

The CPU power and voltage are dramatically reduced. Thus, the temperature is much lower. The max frequency appears only when launching these two games. You see, even with strategy games from Paradox, they are not CPU bound.

Alternatively, you can use my homemade app ideapad-keyboard-backlight to adjust ITS settings. It’s much more lightweight than the official one.

2. Windows Power Mode

Go to Settings app –> System –> Power & Battery, set Power Mode to Best Power Efficiency. This helps a bit, but not much.

Game CPU Voltage (v) CPU Power (w) CPU Max Frequency (MHz) CPU Temperature (°C)
ck3 (after) 0.6-0.8 ~15 ~4.6 55-60
victoria3 (after) 0.6-0.8 ~15 ~4.6 55-60

3. Windows Power Plan

It’s really annoying there are separate settings, and what is the fxxking difference between power mode and power plan?

First, enable options via registry:

1
# REG ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\PowerSettings\54533251-82be-4824-96c1-47b60b740d00\be337238-0d82-4146-a960-4f3749d470c7 /v Attributes /t REG_DWORD /d 2 /f

The default version is 1. This enables the Processor performance boost mode option.

Now go to Control Panel –> Hardware and Sound –> Power Options –> Edit Plan Settings, click Change advanced power settings –> Processor power management –> Processor performance boost mode, set to Disabled(or maybe Efficient Enabled).

Now, CPU boost is completely disabled.

Game CPU Voltage (v) CPU Power (w) CPU Max Frequency (MHz) CPU Temperature (°C)
ck3 (after) 0.6-0.8 ~15 ~3.7 55-60
victoria3 (after) 0.6-0.8 ~15 ~3.7 55-60

Your laptop is cool. Your fan makes much less noise. The setting also prevents system services or 3rd-party applications from making a sudden and unintended CPU boost.

Modern CPUs are just capable to run games at their base frequency, with almost no performance loss. All is done.

4. Add Windows Defender Exclusion

See my previous post.

Cheat Engine 7.7 was just release several days ago. The new version gains additional support for both Linux and MacOS.

The 7.7 installer on the website is just a downloader of the actual installer. It is a InnoSetup one. After some process monitoring work, the actual installer is downloaded into %LocalAppData%\Temp\is-XXXXX.tmp directory. The directory name is randrom. So I copied the actual installer as an offline installer. But it only installs 32-bit files.

With the help of InnoUnpacker-Windows-GUI and some reverse-engineering work. I found that the key is to add two command line options. This can be cross-verified here on Reddit.

1
$ CheatEngine77.exe /VERYSILENT /ZBDIST

You can also change the default installation directory with /DIR="some_custom_directory" option.

0. Install Ghostty

ghostty is a fast, feature-rich, and cross-platform terminal emulator that uses platform-native UI and GPU acceleration. kitty is not chosen, since it is not as easy as ghostty by default. ghostty provides out-of-box support for features that require configuration in kitty. wezterm can be an alternative, but the project is less active these days, since the author is busy moving countries, see #7451. ghostty also provides better native experience on Linux and MacOS.

For Ubuntu, install it using a community binary here.

1
$ sudo dpkg -i ghostty_1.2.3-0.ppa1_amd64_24.04.deb

For AlmaLinux, there is a AppImage here. Nightly build in flatpak is also provided here. Extract and install it with:

1
2
3
4
$ unzip com.mitchellh.ghostty-x86_64.flatpak.zip
$ mv com.mitchellh.ghostty com.mitchellh.ghostty.flatpak
$ sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
$ sudo flatpak install com.mitchellh.ghostty.flatpak

1. Mise

mise:

1.1 Install Mise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ubuntu 26.04+
$ sudo add-apt-repository -y ppa:jdxcode/mise
$ sudo apt update -y
$ sudo apt install -y mise
# ubuntu 22.04+
$ sudo apt update -y && sudo apt install -y curl
$ sudo install -dm 755 /etc/apt/keyrings
$ curl -fSs https://mise.jdx.dev/gpg-key.pub | sudo tee /etc/apt/keyrings/mise-archive-keyring.asc 1> /dev/null
$ echo "deb [signed-by=/etc/apt/keyrings/mise-archive-keyring.asc] https://mise.jdx.dev/deb stable main" | sudo tee /etc/apt/sources.list.d/mise.list
$ sudo apt update -y
$ sudo apt install -y mise
# almalinux 9
$ sudo dnf copr enable jdxcode/mise
$ sudo dnf install mise

mise is chosen as an alternative to homebrew, since homebrew has bugs running patchelf, see #163826 and #258146. But there is a workaround. nix is also not chosen, since it is complex and I do not want to learn its config DSL.

mise also helps to manage different versions of node, python etc..

1.2 Activate Mise

1
2
3
4
5
6
7
8
9
# bash activation
$ echo 'eval "$(mise activate bash)"' >> ~/.bashrc
# bash completion
$ mise use -g usage
$ mkdir -p ~/.local/share/bash-completion/completions
# ubuntu 26.04+
$ mise completion bash > ~/.local/share/bash-completion/completions/mise
# ubuntu 22.04+ & almalinux 9
$ mise completion bash --include-bash-completion-lib > ~/.local/share/bash-completion/completions/mise

mise use installs a tool and adds the version to mise.toml. -g means it will use the global config file ~/.config/mise/config.toml. The additional --include-bash-completion-lib parameter ensures that it works in bash-completion versions earlier than 2.12.

1.3 Install Tools

1
2
3
4
$ mise use -g ripgrep[exe=rg] fd fzf jq resvg zoxide yazi
$ mise use -g [email protected]
$ mise use -g oh-my-posh@28
$ echo 'eval "$(oh-my-posh init bash)"' >> ~/.bashrc

Some useful command lines:

1
2
3
4
5
6
$ mise ls
$ mise ls-remote neovim
$ mise latest neovim
$ mise upgrade
$ mise upgrade neovim
$ mise registry

All supported command line tools can be found in their registry.

For yazi to be full functional, install imagemagick and ffmpeg:

1
2
3
4
5
6
7
8
9
# ubuntu 22.04+
$ sudo apt-get install p7zip p7zip-full ffmpeg imagemagick
$ sudo ln -sf /usr/bin/convert /usr/bin/magick
# almalinux 9
# enable crb repository
# see: https://almalinux.org/blog/2025-09-08-enabling-crb-by-default-for-almalinux10/
$ sudo dnf config-manager --set-enabled crb
$ sudo dnf install p7zip p7zip-plugins ffmpeg-free ImageMagick
$ sudo ln -sf /usr/bin/convert /usr/bin/magick

ghostty supports image preview in yazi out of box, while gnome-terminal does not. Windows Terminal also works by default.

1.4 Install Nerd Fonts

1
2
3
4
5
$ wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/JetBrainsMono.zip \
&& mkdir -p ~/.local/share/fonts \
&& unzip JetBrainsMono.zip -d ~/.local/share/fonts \
&& rm JetBrainsMono.zip \
&& fc-cache -fv

AlmaLinux 9 selects Source Code Pro Regular 10 as its default monospace font, while LinuxMint selects DejaVu Sans Mono. Change it to just monospace in gnome-tweaks or cinnamon-settings. We will use fontconfig to config system fonts.

0. Install Ghostty

ghostty is a fast, feature-rich, and cross-platform terminal emulator that uses platform-native UI and GPU acceleration. kitty is not chosen, since it is not as easy as ghostty by default. ghostty provides out-of-box support for features that require configuration in kitty. wezterm can be an alternative, but the project is less active these days, since the author is busy moving countries, see #7451. ghostty also provides better native experience on Linux and MacOS.

For Ubuntu, install it using a community binary here.

1
$ sudo dpkg -i ghostty_1.2.3-0.ppa1_amd64_24.04.deb

For AlmaLinux, there is a AppImage here. Nightly build in flatpak is also provided here. Extract and install it with:

1
2
3
4
$ unzip com.mitchellh.ghostty-x86_64.flatpak.zip
$ mv com.mitchellh.ghostty com.mitchellh.ghostty.flatpak
$ sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
$ sudo flatpak install com.mitchellh.ghostty.flatpak

1. Homebrew

1.1 Prerequisites

1
2
3
4
5
# ubuntu 22.04+
$ sudo apt install build-essential procps curl file git
# almalinux 9
$ sudo dnf group install 'Development Tools'
$ sudo dnf install procps-ng curl file

1.2 Install Tools

1
2
3
4
5
6
$ bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
$ echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc
$ source ~/.bashrc
$ brew install ripgrep fd fzf jq resvg zoxide yazi
$ brew install neovim oh-my-posh
$ echo 'eval "$(oh-my-posh init bash)"' >> ~/.bashrc

For yazi to be full functional, install imagemagick and ffmpeg:

1
2
3
4
5
6
7
8
9
# ubuntu 22.04+
$ sudo apt-get install p7zip p7zip-full ffmpeg imagemagick
$ sudo ln -sf /usr/bin/convert /usr/bin/magick
# almalinux 9
# enable crb repository
# see: https://almalinux.org/blog/2025-09-08-enabling-crb-by-default-for-almalinux10/
$ sudo dnf config-manager --set-enabled crb
$ sudo dnf install p7zip p7zip-plugins ffmpeg-free ImageMagick
$ sudo ln -sf /usr/bin/convert /usr/bin/magick

ghostty supports image preview in yazi out of box, while gnome-terminal does not. Windows Terminal also works by default.

1.3 Install Nerd Fonts

1
2
3
4
5
$ wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/JetBrainsMono.zip \
&& mkdir -p ~/.local/share/fonts \
&& unzip JetBrainsMono.zip -d ~/.local/share/fonts \
&& rm JetBrainsMono.zip \
&& fc-cache -fv

AlmaLinux 9 selects Source Code Pro Regular 10 as its default monospace font, while LinuxMint selects DejaVu Sans Mono. Change it to just monospace in gnome-tweaks or cinnamon-settings. We will use fontconfig to config system fonts.

1.4 Workaround Bugs in patchelf

homebrew has bugs running patchelf, see #163826 and #258146. For gtk+3 and libadwaita, we can simply fix them by not running patchelf at all. Find bottles in the cache and extract them directly into the homebrew installation directory:

1
2
3
4
5
$ ls ~/.cache/Homebrew/downloads/*bottle*.tar.gz | grep -iE 'gtk|adwaita'
/home/gonwan/.cache/Homebrew/downloads/0a00657a6b24c00ca122404a34a7ed7af7b88ba48cea9e192d6f7ec101675f51--gtk+3--3.24.51.x86_64_linux.bottle.tar.gz
/home/gonwan/.cache/Homebrew/downloads/0d02f139507799d4915c97def5e0a717b25ee3e56704b318da8d8aa0e76fa45b--libadwaita--1.8.4.x86_64_linux.bottle.tar.gz
$ tar xzvf /home/gonwan/.cache/Homebrew/downloads/0a00657a6b24c00ca122404a34a7ed7af7b88ba48cea9e192d6f7ec101675f51--gtk+3--3.24.51.x86_64_linux.bottle.tar.gz -C /home/linuxbrew/.linuxbrew/Cellar/ gtk+3/3.24.51/lib/
$ tar xzvf /home/gonwan/.cache/Homebrew/downloads/0d02f139507799d4915c97def5e0a717b25ee3e56704b318da8d8aa0e76fa45b--libadwaita--1.8.4.x86_64_linux.bottle.tar.gz -C /home/linuxbrew/.linuxbrew/Cellar/ libadwaita/1.8.4/lib/

Only *.so files are restored. patchelf can be mandatory for binary files in bin directories, they have a @@HOMEBREW_PREFIX@@ placeholder hardcoded in the ldd dependency list, which to be replaced by patchelf. The workaround does work, because we are using the default installation prefix /home/linuxbrew/.linuxbrew.

Otherwise, you may want to build gtk+3 and libadwaita from source, which leads to tons of dependencies to be installed as well as much time to build them.

1
$ brew reinstall -s gtk+3 libadwaita

Now, gtk3/gtk+4/libadwaita apps work. Verified with evince and gnome-papers:

1
2
3
4
$ brew install evince gnome-papers
$ export XDG_DATA_DIRS=/home/linuxbrew/.linuxbrew/share:$XDG_DATA_DIRS
$ evince &
$ papers &

1. Install Microsoft Store

If you are using Enterprise or IoT Enterprise version of Windows 11, there is no Microsoft Store preinstalled. Install it with:

1
$ wsreset -i

1.1 winget

Also winget is not preinstalled. You can not find it directly in the store. Go to App Installer and redirect from there.

1.2 Windows Terminal

Just search and install it inside the store.

2. Install Development Tools

Before installing any tools using winget, open cmd with administrative privilege:

1
$ winget settings --enable ProxyCommandLineOptions

This will allow proxy usage for winget in command line. Verify it with:

1
2
3
4
5
6
7
8
9
10
11
$ winget info
C:\Users\gonwan>winget --info
...
Admin Setting State
--------------------------------------------------
LocalManifestFiles Disabled
BypassCertificatePinningForMicrosoftStore Disabled
InstallerHashOverride Disabled
LocalArchiveMalwareScanOverride Disabled
ProxyCommandLineOptions Enabled
DefaultProxy Disabled

Clink adds command completion, history, and line-editing capabilities for cmd. Install with:

1
$ winget install --id chrisant996.Clink [--proxy http://127.0.0.1:10809]

Use a proxy if necessary. Clink takes effect automatically every time you start cmd.

2.2 Oh-My-Posh

Oh-My-Posh is a prompt theme, like powerline. It is chosen because it works in almost all shells. Install with:

1
$ winget install --id JanDeDobbeleer.OhMyPosh [--proxy http://127.0.0.1:10809]

Clink has builtin support for Oh My Posh. It allows you to set the prompt using the clink command:

1
$ clink config prompt use oh-my-posh

For powershell to work, follow their documents.

2.3 Nerd Fonts

Nerd Fonts patches developer targeted fonts with a high number of glyphs (icons). Install with:

1
$ winget install --id DEVCOM.JetBrainsMonoNerdFont [--proxy http://127.0.0.1:10809]

You can find 6 fonts from Jetbrains, with and without ligatures.

  • JetBrainsMono Nerd Font, JetBrainsMono Nerd Font Mono, JetBrainsMono Nerd Font Propo.
  • JetBrainsMonoNL Nerd Font, JetBrainsMonoNL Nerd Font Mono, JetBrainsMonoNL Nerd Font Propo.

Fonts without Mono suffix have bigger icons (usually around 1.5 normal letters wide). It works better with oh-my-posh. Just choose JetBrainsMonoNL Nerd Font in the settings of Windows Terminal.

Now your terminal looks more productive. It also shows git and language info in its prompt.

win11_terminal

2.4 Neovim

Install with:

1
$ winget install Neovim [--proxy http://127.0.0.1:10809]

3. Upgrade Apps

Upgrade with:

1
$ winget upgrade --id chrisant996.Clink [--proxy http://127.0.0.1:10809]

Refer to the project page. Also see my previous articles: 1, 2.

See Techempower. This repository contains homemade benchmarks for Http servers. Only a single text response is considered, since some servers do not have their builtin Json implementation. Basically, I am benchmarking event loops, with Http implementation.

All server applications are running in a 4C docker environment. After packaging, a server can be launched using docker compose:

1
$ docker compose -f docker-compose.yml up actix

wrk is used as the client. Http keep-alive is enabled in the benchmark.

1
2
# benchmark plaintext
$ wrk -c 1000 -t 10 -d 30s http://127.0.0.1:90xx/text

Dependencies

If you are using Ubuntu 24.04 or later, all packages can be installed via apt-get:

1
2
3
$ sudo apt-get install libssl-dev libevent-dev libevhtp-dev libjsoncpp-dev \
libtrantor-dev libdrogon-dev libboost-dev wrk \
golang openjdk-21-jdk maven dotnet-sdk-10.0 nodejs rust-all

Environment

  • Server: 4C docker
  • Client: 12C host machine, AMD 7840HS, LinuxMint 22.3 / Ubuntu 24.04.
  • All applications are tweaked to work with 4C.
  • All Java & .NET applications are properly warmed up before benchmark.
  • The userland proxy of docker is disabled to eliminate the networking bottleneck. In /etc/docker/daemon.json, set the value and restart docker.
1
2
3
{
"userland-proxy": false
}

Benchmark Result

Server Language Version Throughput
nginx C 1.28.2 336k /s
libevhtp (libevent) C 1.2.18 / 2.1.12 254k /s
drogon C++ 1.8.7 320k /s
beast C++ 1.83 220k /s
gin Go 1.22 1.10.1 86k /s
gin (GOMAXPROCS=4) Go 1.22 1.10.1 194k /s
gofiber Go 1.22 2.52.12 98k /s
gofiber (GOMAXPROCS=4) Go 1.22 2.52.12 272k /s
hertz Go 1.22 0.10.4 138k /s
hertz (GOMAXPROCS=4) Go 1.22 0.10.4 268k /s
spring-webflux (netty) Java 21 3.5.10 / 4.1.130 170k /s
spring-webmvc (tomcat) Java 21 3.5.10 / 10.1.50 47k /s
spring-webmvc (tomcat with virtual thread) Java 21 3.5.10 / 10.1.50 68k /s
spring-webmvc-undertow Java 21 3.5.10 / 2.3.22 61k /s
aspnetcore .NET 10 10.0 125k /s
node Javascript 24.14 178k /s
bun Javascript 24.14 / 1.3.10 254k /s
actix Rust 1.75 4.12.1 334k /s
---
config:
  xyChart:
    showDataLabel: true
  themeVariables:
    xyChart:
      plotColorPalette: '#5294e2'
---
xychart-beta horizontal
title "Http Server Benchmark"
x-axis [nginx, libevhtp, drogon, beast, gin, gofiber, hertz, spring-flux, spring-mvc, spring-mvc-vt, spring-mvc-udt, aspnetcore, node, bun, actix]
y-axis "Throughput (k/s)" 0 --> 350
bar [336, 254, 320, 220, 194, 272, 268, 170, 47, 68, 61, 125, 178, 254, 334]

It is amazing that actix in rust wins the first price. It runs even faster than any C/C++ servers. It does gain a bit more QPS when enabling LTO optimization. But I choose to not have it. Since all C/C++ servers do not have it, and the optimization can introduce bugs in a production environment. nginx is here for reference.

Then, drogon is the second. It is also an application level framework. libevhtp is not actively maintained and somehow hard to use. beast from boost is just another choice in C++. In my previous benchmark, I can remember it did not scale well and got a much lower throughput. It got almost same throughput running in a 4C and 24C Linux machine. Not knowing why, maybe It was running on an outdated CPU and OS(CentOS 7).

In the Go world, all 3 frameworks give competitive throughput. Setting GOMAXPROCS makes large difference in performance. Starting 1.25, Go automatically detects and respects docker CPU limits to set GOMAXPROCS.

Java servers all have bad performance. spring-webmvc gives the worst performance by default. It does get better when switching to use undertow or the virtual thread feature introduced in Java 21, but no so much. There is almost no difference when running under Java 21 and Java 25 in this case, regarding JEP 491(Synchronize Virtual Threads without Pinning). spring-webflux gives the best performance. The drawback is that, it is hard to write and debug reactive code. The Java world seems to have chosen virtual thread, say stackful coroutine, as their future of non-blocking programming. Even Spring has deprecated their reactor-kafka component. By the way, Java detects the docker environment with limited CPU resource. It not necessary to set IO or worker count manually for these servers.

Regarding ASP .NET Core, it beats only Java servers.

Regarding Javascript, both vanilla node and bun servers run smooth. They both have higher throughput than Java and .NET servers.

The benchmark only evolves event loop with Http implementation, without any business logics. The QPS can decrease dramatically if cross-service calls or database operations are added in a real-world application. But the benchmark does expose difference among languages and their potentials.

Maybe it time to say goodbye to Java and .NET in 2026. I almost have no knowledge about rust, so I think Go frameworks can be the first choice for application-level servers, as they are easier to get started.

Also see my previous post: BlissOS as An Android Emulator Under Linux.

Redroid(Remote-Android) is a docker-based solution for Android emulation, GPU enabled, super fast. It is also easy to deploy and use.

What is the difference between Waydroid? Redroid runs in docker, it won’t pollute your host environment with Waydroid packages. Redroid also supports remote access, while Waydroid does not. You can even build a cloud environment above it. Though, Waydroid should provide better performance, since no video encoding/decoding and redirection exist.

I’m using LinuxMint 22 / Ubuntu 24.04. Should also work under other Linux OS. Android 12 and 14 are verified to work.

1. Pull Redroid Docker Images

1
$ docker pull redroid/redroid:14.0.0-latest

If you are in Mainland China, edit /usr/lib/systemd/system/docker.service to add a proxy for docker repository:

1
2
3
[Service]
Environment="HTTPS_PROXY=http://127.0.0.1:10809"
...

Restart docker service:

1
2
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

2. Install libhoudini and Magisk

libhoudini provides a translation layer from Arm to x86, while Magisk provides root access.

1
2
3
$ git clone https://github.com/ayasa520/redroid-script
$ cd redroid-script
$ ./redroid.py -a 14.0.0 -i -m

A new docker image is built with libhoudini and Magisk added.

1
2
3
4
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redroid/redroid 14.0.0_houdini_magisk e250d0ca87c5 About an hour ago 2.34GB
redroid/redroid 14.0.0-latest 639792975c26 21 months ago 2.09GB

The readme file says libhoudini does not work. But it actually works, after this PR fixing the linker namespace issue. More details can be found here, in Chinese.

3. Run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ sudo apt-get install linux-modules-extra-`uname -r`
$ sudo modprobe binder_linux devices="binder,hwbinder,vndbinder"
$ docker run -itd --rm --privileged \
-v ~/data:/data \
-p 5555:5555 \
redroid/redroid:14.0.0_houdini_magisk \
androidboot.redroid_width=1080 \
androidboot.redroid_height=1920 \
androidboot.redroid_gpu_mode=host \
androidboot.use_memfd=true \
ro.product.cpu.abilist=x86_64,arm64-v8a,x86,armeabi-v7a,armeabi \
ro.product.cpu.abilist64=x86_64,arm64-v8a \
ro.product.cpu.abilist32=x86,armeabi-v7a,armeabi \
ro.dalvik.vm.isa.arm=x86 \
ro.dalvik.vm.isa.arm64=x86_64 \
ro.enable.native.bridge.exec=1 \
ro.vendor.enable.native.bridge.exec=1 \
ro.vendor.enable.native.bridge.exec64=1 \
ro.dalvik.vm.native.bridge=libhoudini.so

Download scrcpy to view your Android OS:

1
2
3
4
5
$ wget https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-linux-x86_64-v3.3.4.tar.gz
$ tar xzvf scrcpy-linux-x86_64-v3.3.4.tar.gz
$ cd scrcpy-linux-x86_64-v3.3.4
$ ./adb connect localhost:5555 &
$ ./scrcpy -s localhost:5555 --audio-codec=aac --audio-encoder=OMX.google.aac.encoder &

The Android OS should now appear. Open the Magisk app. It will reboot to finish installation.

4. Install Apps and Games

1
$ ./adb install some_app.apk

With aida64 and termux showing system info:

redroid_home redroid_version
redroid_home redroid_home

Note: I set androidboot.redroid_gpu_mode=host to pass through the host GPU, which dramatically improves the graphic performance. Surprisingly, it also has add Vulkan support. Otherwise, SwiftShader or LLVM-based software render is used.

If you have an Nvidia GPU, hardware acceleration is likely to fail. See here for details. The virtio-gpu approach are recommended by the author.

The issue previously mentioned is also resolved. When playing 阴阳师(Chinese version of Onmyoji), all maps do not load. I was finally able to collect the detailed logs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
02-21 18:22:02.276  4522  4649 I [18:22:02.276] M: [UIManager] openUI MapGuild
02-21 18:22:02.289 4522 4649 I [18:22:02.289] M: [MapManager] createMapView size (1920.0, 1080.0), init_lat 31.32253721571831, init_lng 121.45974425804117, init_altitude 17
02-21 18:22:02.300 4522 4649 W System.err: java.lang.UnsatisfiedLinkError: dlopen failed: "/data/app/~~Q9WxXLThl_RFQ0m11zDw4w==/com.netease.onmyoji.wyzymnqsd_cps-1NPxTDts0hEOW_vq1dzUIQ==/lib/x86_64/libGNaviUtils.so" is for EM_AARCH64 (183) instead of EM_X86_64 (62)
02-21 18:22:02.300 4522 4649 W System.err: at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
02-21 18:22:02.300 4522 4649 W System.err: at java.lang.Runtime.loadLibrary0(Runtime.java:998)
02-21 18:22:02.300 4522 4649 W System.err: at java.lang.System.loadLibrary(System.java:1656)
02-21 18:22:02.300 4522 4649 W System.err: at com.autonavi.amap.mapcore.AEUtil.<clinit>(Unknown Source:2)
02-21 18:22:02.300 4522 4649 W System.err: at com.autonavi.amap.mapcore.AEUtil.init(Unknown Source:0)
02-21 18:22:02.300 4522 4649 W System.err: at com.amap.api.mapcore.util.a.<init>(Unknown Source:286)
02-21 18:22:02.300 4522 4649 W System.err: at com.amap.api.mapcore.util.b.<init>(Unknown Source:11)
02-21 18:22:02.300 4522 4649 W System.err: at com.amap.api.mapcore.util.b.<init>(Unknown Source:1)
02-21 18:22:02.300 4522 4649 W System.err: at com.amap.api.mapcore.util.o.getMap(Unknown Source:132)
02-21 18:22:02.300 4522 4649 W System.err: at com.amap.api.maps.UnityGLRenderer.getMap(Unknown Source:4)
02-21 18:22:02.300 4522 4649 W System.err: at com.amap.api.maps.UnityGLRenderer.setContext(Unknown Source:20)
02-21 18:22:02.300 4522 4649 W System.err: at com.netease.neox.NXMapViewContainer.<init>(Unknown Source:65)
02-21 18:22:02.300 4522 4649 W System.err: at com.netease.neox.PluginMapView.create(Unknown Source:12)
02-21 18:22:02.300 4522 4649 E NeoX :
02-21 18:22:02.300 4522 4649 E NeoX : ERROR *****************************************************************************
02-21 18:22:02.300 4522 4649 E [18:22:02.300] M [ANDROID]: JNIManager Failed to call method create with signiture (I)Lcom/netease/neox/NXMapViewContainer;!
02-21 18:22:02.300 4522 4649 E NeoX :
02-21 18:22:02.300 4522 4649 E NeoX :
02-21 18:22:02.300 4522 4649 E NeoX : ERROR *****************************************************************************
02-21 18:22:02.301 4522 4649 E [18:22:02.301] M: create mapview error
02-21 18:22:02.301 4522 4649 E NeoX :
02-21 18:22:02.301 4522 4649 I NeoX :
02-21 18:22:02.301 4522 4649 I NeoX : SCRIPT ERROR **********************************************************************

They are packaging *.so with mismatching architectures, WTF. Seems AutoNavi does not provide a x86_64 SDK for Android. Not knowing how MumuPlayer workaround this.

After installing the normal version(arm64 ABI), all maps can be loaded. The game just looks like a native Linux application. Redroid is now the best Android emulator for Linux.

redroid_yys

Also see my previous post: Uniform look for Qt and GTK applications.

It is really frustrated to find that libadwaita applications are entirely not themed. Gtk guys are breaking things! again!!

Well, just want to dig more here. I’m running LinuxMint 22.3 / Ubuntu 24.04.

1. Default Looks

Write a minimum gtk3/gtk4 window with a button:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// gcc gtk4.c -o gtk4 `pkg-config --cflags --libs gtk4`
// gcc gtk4.c -o gtk3 `pkg-config --cflags --libs gtk+-3.0`
#include <gtk/gtk.h>

static void on_button_clicked(GtkButton *button, gpointer user_data)
{
g_print("Button Clicked!\n");
}

static void activate(GtkApplication *app, gpointer user_data)
{
GtkWidget *window;
GtkWidget *button;

window = gtk_application_window_new(app);
#if GTK_MAJOR_VERSION >= 4
gtk_window_set_title(GTK_WINDOW(window), "Gtk4 App");
#else
gtk_window_set_title(GTK_WINDOW(window), "Gtk3 App");
#endif
gtk_window_set_default_size(GTK_WINDOW(window), 300, 200);

button = gtk_button_new_with_label("Click Me");
gtk_widget_set_halign(button, GTK_ALIGN_CENTER);
gtk_widget_set_valign(button, GTK_ALIGN_CENTER);
#if GTK_MAJOR_VERSION >= 4
gtk_widget_add_css_class(button, "suggested-action");
#else
gtk_style_context_add_class(gtk_widget_get_style_context(button), "suggested-action");
#endif
g_signal_connect(button, "clicked", G_CALLBACK(on_button_clicked), NULL);

#if GTK_MAJOR_VERSION >= 4
gtk_window_set_child(GTK_WINDOW(window), button);
gtk_window_present(GTK_WINDOW(window));
#else
gtk_container_add(GTK_CONTAINER(window), button);
gtk_widget_show_all(window);
#endif
}

int main(int argc, char *argv[])
{
GtkApplication *app;
int status;

#if GTK_MAJOR_VERSION >= 4
app = gtk_application_new("org.example.gtk4", G_APPLICATION_DEFAULT_FLAGS);
#else
app = gtk_application_new("org.example.gtk3", G_APPLICATION_DEFAULT_FLAGS);
#endif
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
status = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
return status;
}

A gtk4 one with libadwaita:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// gcc gtk4adw.c -o gtk4adw `pkg-config --cflags --libs libadwaita-1`
#include <adwaita.h>

static void on_button_clicked(GtkButton *button, gpointer user_data)
{
g_print("Button Clicked!\n");
}

static void activate(GtkApplication *app, gpointer user_data)
{
GtkWidget *window;
GtkWidget *main_box;
GtkWidget *header_bar;
GtkWidget *button;

window = adw_application_window_new(app);
gtk_window_set_title(GTK_WINDOW(window), "Gtk4 Adwaita App");
gtk_window_set_default_size(GTK_WINDOW(window), 300, 200);

main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
adw_application_window_set_content(ADW_APPLICATION_WINDOW(window), main_box);

header_bar = adw_header_bar_new();
gtk_box_append(GTK_BOX(main_box), header_bar);

button = gtk_button_new_with_label("Click Me");
gtk_widget_set_halign(button, GTK_ALIGN_CENTER);
gtk_widget_set_valign(button, GTK_ALIGN_CENTER);
gtk_widget_set_vexpand(button, TRUE);
gtk_widget_add_css_class(button, "suggested-action");
gtk_box_append(GTK_BOX(main_box), button);
g_signal_connect(button, "clicked", G_CALLBACK(on_button_clicked), NULL);

gtk_window_present(GTK_WINDOW(window));
}

int main(int argc, char *argv[])
{
AdwApplication *app;
int status;

app = adw_application_new("org.example.gtk4adw", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
status = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
return status;
}

These applications look like bellow. There is no difference between a plain gtk3 and gtk4 application. But gtk4 with libadwaita really looks different. The title bar of the window seems to be hidden by default. I am using the famous arc theme. It does have a gtk4 adoption. You can find a gtk-4.0 folder in it. So, it is clear that a libadwaita application just ignores themes.

gtk_theme_all

This can be further verified with gtk3-widget-factory and gtk4-widget-factory. They just look the same. Transmission is the only application I can find, which uses gtk4 without libadwaita. It just renders the same when building with gtk3 and gtk4 options.

2. Theming in Gtk4

2.1 gtk4 - gtk/gtksettings.c

In settings_init_style(), gtk4 tries to load ~/.config/gtk-4.0/gtk.css for theming at first. Then it adds 3 css providers to the application. css_provider uses GTK_STYLE_PROVIDER_PRIORITY_USER(800), which is the highest priority. settings and settings->theme_provider use GTK_STYLE_PROVIDER_PRIORITY_SETTINGS(400). Lastly, it calls settings_update_theme().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*...*/
css_provider = gtk_css_provider_new ();
css_path = g_build_filename (g_get_user_config_dir (),
"gtk-4.0",
"gtk.css",
NULL);

if (g_file_test (css_path,
G_FILE_TEST_IS_REGULAR))
gtk_css_provider_load_from_path (css_provider, css_path);
/*...*/
cascade = _gtk_settings_get_style_cascade (settings, 1);
_gtk_style_cascade_add_provider (cascade,
GTK_STYLE_PROVIDER (css_provider),
GTK_STYLE_PROVIDER_PRIORITY_USER);

_gtk_style_cascade_add_provider (cascade,
GTK_STYLE_PROVIDER (settings),
GTK_STYLE_PROVIDER_PRIORITY_SETTINGS);

_gtk_style_cascade_add_provider (cascade,
GTK_STYLE_PROVIDER (settings->theme_provider),
GTK_STYLE_PROVIDER_PRIORITY_SETTINGS);
/*...*/
settings_update_theme (settings);
/*...*/

In settings_update_theme(), gtk4 calls get_theme_name() and gtk_css_provider_load_named() to actually load themes in common theme directories.

1
2
3
4
5
6
7
/*...*/
get_theme_name (settings, &theme_name, &theme_variant);

gtk_css_provider_load_named (settings->theme_provider,
theme_name,
theme_variant);
/*...*/

In get_theme_name(), gtk4 respects GTK_THEME and read theme settings.

1
2
3
4
5
6
7
8
9
/*...*/
if (g_getenv ("GTK_THEME"))
*theme_name = g_strdup (g_getenv ("GTK_THEME"));
/*...*/
g_object_get (settings,
"gtk-theme-name", theme_name,
"gtk-application-prefer-dark-theme", &prefer_dark,
NULL);
/*...*/

In gtk_settings_notify(), settings_update_theme() is also called here. This is a callback function, which is invoked when system-wide settings(including themes) are changed. Thus, a gtk4 application can apply the theme change on-the-fly.

The Gtk property name is gtk-theme-name, it is mapped from GSettings value gtk-theme. Found in gdk/wayland/gdkdisplay-wayland.c:

1
2
3
static TranslationEntry translations[] = {
{ FALSE, "org.gnome.desktop.interface", "gtk-theme", "gtk-theme-name" , G_TYPE_STRING, { .s = "Adwaita" } },
/*...*/

2.2 gtk4 - gtk/gtkcssprovider.c

In gtk_css_provider_load_named(), gtk4 calls _gtk_css_find_theme() to find theme by name and variant, and fallback to default gtk4 or libadwaita themes. It also tries to load gtk.gresource.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  path = _gtk_css_find_theme (name, variant);
if (path)
{
/*...*/
resource_file = g_build_filename (dir, "gtk.gresource", NULL);
resource = g_resource_load (resource_file, NULL);
/*...*/
gtk_css_provider_load_from_path (provider, path);
/*...*/
}
else
{
/* Things failed! Fall back! Fall back!
*
* We accept the names HighContrast, HighContrastInverse,
* Adwaita and Adwaita-dark as aliases for the variants
* of the Default theme.
*/
/*...*/
}

In _gtk_css_find_theme(), you can see which directories are looked up for themes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*...*/
path = _gtk_css_find_theme_dir (g_get_user_data_dir (), "themes", name, variant);
/*...*/
path = _gtk_css_find_theme_dir (g_get_home_dir (), ".themes", name, variant);
/*...*/
dirs = g_get_system_data_dirs ();
for (i = 0; dirs[i]; i++)
{
path = _gtk_css_find_theme_dir (dirs[i], "themes", name, variant);
/*...*/
}
/*...*/
dir = _gtk_get_theme_dir ();
path = _gtk_css_find_theme_dir (dir, NULL, name, variant);
/*...*/

2.3 gtk4 - gtk/inspector/visual.c

In init_theme(), you can see how the gtk inspector works with themes.

3. Theming in Libadwaita

3.1 libadwaita - adw-style-manager.c

In adw_style_manager_constructed(), there are tricks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
static void
adw_style_manager_constructed (GObject *object)
{
AdwStyleManager *self = ADW_STYLE_MANAGER (object);

G_OBJECT_CLASS (adw_style_manager_parent_class)->constructed (object);

if (self->display) {
GtkSettings *settings = gtk_settings_get_for_display (self->display);
gboolean prefer_dark_theme;

g_object_get (settings,
"gtk-application-prefer-dark-theme", &prefer_dark_theme,
NULL);

if (prefer_dark_theme)
warn_prefer_dark_theme (self);

g_signal_connect_object (settings,
"notify::gtk-application-prefer-dark-theme",
G_CALLBACK (warn_prefer_dark_theme),
self,
G_CONNECT_SWAPPED);

if (!adw_is_granite_present () && !g_getenv ("GTK_THEME")) {
g_object_set (gtk_settings_get_for_display (self->display),
"gtk-theme-name", "Adwaita-empty",
NULL);

self->provider = gtk_css_provider_new ();
gtk_style_context_add_provider_for_display (self->display,
GTK_STYLE_PROVIDER (self->provider),
GTK_STYLE_PROVIDER_PRIORITY_THEME);

self->colors_provider = gtk_css_provider_new ();
gtk_style_context_add_provider_for_display (self->display,
GTK_STYLE_PROVIDER (self->colors_provider),
GTK_STYLE_PROVIDER_PRIORITY_THEME);
}

self->animations_provider = gtk_css_provider_new ();
gtk_css_provider_load_from_string (self->animations_provider,
"* { transition: none; }");
}

self->settings = adw_settings_get_default ();

g_signal_connect_object (self->settings,
"notify::system-supports-color-schemes",
G_CALLBACK (notify_system_supports_color_schemes_cb),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->settings,
"notify::color-scheme",
G_CALLBACK (update_dark),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->settings,
"notify::high-contrast",
G_CALLBACK (notify_high_contrast_cb),
self,
G_CONNECT_SWAPPED);

update_dark (self);
update_stylesheet (self);
}

It hardcodes gtk-theme-name to Adwaita-empty. This prevent the theme name change callback from working, since g_object_set() has higher priority than a GSettings binding in the gtk4 library. The application will still read the hardcoded value. Also see comments in gtk/gtksettings.c:

1
2
3
4
5
6
/*
* Applications can override system-wide settings by setting the property
* of the `GtkSettings` object with g_object_set(). This should be restricted
* to special cases though; `GtkSettings` are not meant as an application
* configuration facility.
*/

There is another series of setting objects. Say, libadwaita has its own GtkSettings(AdwSettings) and corresponding css providers. libadwaita does not change these settings in gtk4, it works more like an overlay, to encapsulate Gnome HIG(Human Interface Guidelines).

libadwaita uses GTK_STYLE_PROVIDER_PRIORITY_THEME(200) for these css providers. It actually has lower priority than GTK_STYLE_PROVIDER_PRIORITY_SETTINGS(400) used in gtk4. Since it forces to use the Adwaita-empty theme, css providers in gtk4 do not take effect.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct _AdwStyleManager
{
GObject parent_instance;

GdkDisplay *display;
AdwSettings *settings;
GtkCssProvider *provider;
GtkCssProvider *colors_provider;

AdwColorScheme color_scheme;
gboolean dark;
gboolean setting_dark;

GtkCssProvider *animations_provider;
guint animation_timeout_id;
};

At last, if GTK_THEME is set, libadwaita is forced to use the given gtk4 theme.

4. gtk.css and gtk-dark.css

In 2.1 section, we can see gtk4 tries to load ~/.config/gtk-4.0/gtk.css for theming. The file has the highest priority. That’s why most libadwaita-supporting themes like Colloid suggest using these files to force enable theming in libadwaita applications.

In both gtk3 and gtk4, auto loading of gtk.gresource does not always take effect. When in ~/.config/gtk-3.0,gtk-4.0/ directory, gtk loads css using gtk_css_provider_load_from_path(), gtk.gresource is not loaded. When in common theme directories, gtk loads css using gtk_css_provider_load_named(), gtk.gresource is loaded. Thus, @import directive in a css file does not work if the url starts with resource:// and the actual resource file is not already loaded by the gtk library. For the same reason, 3rd part libadwaita-supporting themes does not use gtk.gresource at all.

A gtk.gresource file can be extracted using following script:

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/bash

f="gtk.gresource"

for r in $(gresource list "$f"); do
r_path=`dirname "$r"`
mkdir -p ".$r_path"
echo "Extracting file $r"
gresource extract "$f" "$r" > ".$r"
done

To force libadwaita applications to use arc theme, run following commands:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cd ~/.config/gtk-4.0
$ ln -sf /usr/share/themes/Arc/gtk-4.0/gtk.gresource gtk.gresource
$ ./extract.sh
$ ln -sf org/gnome/arc-theme/gtk-main.css gtk.css
$ ln -sf org/gnome/arc-theme/gtk-main-dark.css gtk-dark.css
$ ln -sf org/gnome/arc-theme/assets assets
$ ls ~/.config/gtk-4.0
drwxrwxr-x 3 gonwan gonwan 4096 Feb 25 16:22 .
drwx------ 78 gonwan gonwan 4096 Feb 24 04:15 ..
lrwxrwxrwx 1 gonwan gonwan 26 Feb 25 16:22 assets -> org/gnome/arc-theme/assets
-rwxrwxr-x 1 gonwan gonwan 189 Feb 24 15:00 extract.sh
lrwxrwxrwx 1 gonwan gonwan 32 Feb 25 16:22 gtk.css -> org/gnome/arc-theme/gtk-main.css
lrwxrwxrwx 1 gonwan gonwan 37 Feb 25 16:22 gtk-dark.css -> org/gnome/arc-theme/gtk-main-dark.css
lrwxrwxrwx 1 gonwan gonwan 43 Feb 25 16:21 gtk.gresource -> /usr/share/themes/Arc/gtk-4.0/gtk.gresource
drwxrwxr-x 3 gonwan gonwan 4096 Feb 24 15:00 org

Alternatively, GTK_THEME can be used:

1
$ GTK_THEME=Arc adwaita-1-demo

GTK_DEBUG=interactive is a helpful debugging feature for inspecting theme and other info inside a gtk application.

5. Themes in Distributions

5.1 Ubuntu

Ubuntu patches libadwaita to support theme switching when changing accent color in gnome-control-panel:

1
2
3
4
5
6
7
8
libadwaita-1 (1.5.0-1ubuntu1) noble; urgency=medium

* Merge with Debian. Remaining changes:
- Add patch to support Yaru accent colors using Adwaita theming
- Drop all revert patches
- Set minimum GTK4 to 4.13.4

-- Jeremy Bícha <[email protected]> Wed, 20 Mar 2024 15:37:01 -0400

No such patch found in upstream Debian package.

5.2 LinuxMint

LinuxMint ignores the patch above, it uses its own libadwaita fork instead. In both Ubuntu and LinuxMint, theme changing is notified from GSettings and XDG Desktop Portal. For a libadwaita application, only accent color, color-scheme and high_contrast are affected, the libadwaita widget style stays unchanged. The fork is more like a backport of accent color support in libadwaita 1.6.

5.3 ArchLinux

There is a AUR package libadwaita-without-adwaita, which is a patched version of libadwaita to respect gtk4 themes. It is different from patches from Ubuntu and LinuxMint, where the entire gtk4 theme is applied for libadwaita applications.

5.4 Fedora

Seems Fedora uses vanilla Gnome, there is no such patch for libadwaita.

6. Flatpak

Flatpak does not respect system themes by default, themes in /usr are not accessible by it. The recommend solution is to use packaged themes.

NOTE: After you install a theme, only theme files are installed there. You still need to switch to use it. Flatpak does detect gtk-theme value in GSettings on the host. But the theme should also be available in the Flatpak sandbox. So, I’m using Arc theme, I should install org.gtk.Gtk3theme.Arc package. If I switches to use Mint-Y, I should also install org.gtk.Gtk3theme.Mint-Y package.

Alternatively, you can put themes and icons into ~/.themes and ~/.icons, then make them accessible to Flatpak by running:

1
2
3
$ flatpak override --user --filesystem=~/.themes
$ flatpak override --user --filesystem=~/.icons
$ flatpak override --user --filesystem=xdg-config/gtk-4.0

Flatseal is a GUI utility to simplify configuration for Flatpak applications.

Refer to official document for more details.

7. Alternative Themes

Given there are so many gtk3/gtk4 themes, arc theme is still the best one. I want a compact theme with mixed light and dark blocks. I don’t like full-light or full-dark themes. I like light blocks with dark background to help me focus on the content.

Alternative themes:

Unfortunately, no one works better than the great arc theme. Some themes have too much contrast. Some themes just have visual bugs.

8. Conclusion

The Gnome guys are right to an extent. libadwaita forces users not to theme the original apps. Thus, all libadwaita apps can have a unified look, in several years maybe. libadwaita with Gnome HIG actually does a good job, in my opinion. I suggest to only theme gtk2, gtk3 and gtk4 apps without libadwaita, but not libadwaita apps. Leave them as-is. See articles from Gnome developers:

0%