Friday, 13 June 2025
Learn how to use Cucumber-CPP and Gherkin to implement better black box tests for a C++ library. We developed a case-study based on Qt OPC UA.
Continue reading Improved Black Box Testing with Cucumber-CPP at basysKom GmbH.
Learn how to use Cucumber-CPP and Gherkin to implement better black box tests for a C++ library. We developed a case-study based on Qt OPC UA.
Continue reading Improved Black Box Testing with Cucumber-CPP at basysKom GmbH.
One of the largest hurdles in any job or activity is getting your resources set up. Luckily for you, Krita has one of the most detailed and straightforward documentation for setup. In this blog I will go over my experience setting up Krita and provide quick links to answer all the questions you may have during set up.
One Stop Shop for Links
Download and Install Ubuntu
Create KDE account
Fork Krita Repository
Follow build instructions
If you use QTCreator to Build and Run Krita follow this video
Krita Chat - Create account, join chat room, introduce yourself and ask questions
The goal is to get Krita running on your machine. For my setup and for simplicity of instructions, I use Oracle's Virtualbox to run a virtual machine(VM) with Ubuntu on my windows machine. You can use any VM host for set up. The Follow build instructions should be straightforward to follow. The great thing about these instructions is that you don't need to know a lot of detail about docker or C++ yet, but you will need to understand some basic linux and git commands.
In the above links, follow the instruction in the hyperlink title.
When I set up Krita for the first time, I felt a sense of accomplishment. Not only was I able to set up Krita, but I was able to deepen my understanding of git, learn about docker, VMs and QT.
I think the biggest take away from setting up Krita is to never give up, ask questions in chat, ask yourself "What do I not understand?" before moving to the next instruction.
Setting up Krita is as simple as you make it out to be. The hardest part is finding the resources to be successful. I hope this blog post can simplify set up for newcomers and experienced users.
To anyone reading this, please feel free to reach out to me. I’m always open to suggestions and thoughts on how to improve as a developer and as a person.
Email: ross.erosales@gmail.com
Matrix: @rossr:matrix.org
To briefly recap, Natalie Clarius and I applied for an NLnet grant to improve gesture support in Plasma, and they accepted our project proposal. We thought it would be a good idea to meet in person and workshop this topic from morning to evening for three days in a row. Props to Natalie taking the trip from far away in Germany to my parents' place, where we were kindly hosted and deliciously fed.
Our project plan starts with me adding stroke gesture support to KWin in the first place, while Natalie works on making multi-touch gestures customizable. Divvying up the work along these lines allows us to make progress independently without being blocked on each other's work too often. But of course there is quite a bit of overlap, which is why we applied to NLnet together as a single project.
The common thread is that both kinds of gestures can result in similar actions being triggered, for example:
So if we want to avoid duplicating lots of code, we'll want a common way to assign actions to a gesture. We need to know what to store in a config file, how Plasma code will make use of it, and how System Settings can provide a user interface that makes sense to most people. These are the topics we focused on. Time always runs out faster than you'd like, ya gotta make it count.
Getting to results is an iterative process. You start with some ideas for a good user experience (UX) and make your way to the required config data, or you start with config data and make your way to actual code, or you hit a wall and start from the other end going from code to UX until you hit another wall again. Rinse and repeat until you like it well enough to ship it.
On day 1, we:
kcm_keys
.On day 2, we:
On day 3, we:
kglobalshortcutsrc
file instead.What I just wrote is a lie, of course. I needed to break up the long bullet point list into smaller sections. In reality we jumped back and forth across all of these topics in order to reach some sort of conclusion at the end. Fortunately, we make for a pretty good team and managed to answer a good amount of questions together. We even managed to make time for ice cream and owl spottings along the way.
Since you asked for it, here's a picture of Natalie and I drawing multi-touch gestures in the air.
So there are some good ideas, we need to make them real. Since the sprint, I've been trying my hand on more detailed mockups for our rough design sketches. This always raises a few more issues, which we want to tackle before asking for opinions from KWin maintainers and Plasma's design community. There isn't much to share with the community yet, but we'll involve other contributors before too long.
Likewise, my first KWin MR for stroke gesture infrastructure is not quite there yet, but it's getting closer. The first milestone will be to make it possible for someone to provide stroke gesture actions. The second milestone will be for Plasma/KWin to provide stroke gesture actions by itself and offer a nice user interface for it.
Baby steps. Keep chiseling away at it and trust that you'll create something decent eventually. This is not even among the largest efforts in KDE, and yet there are numerous pieces to fit and tasks to tackle. Sometimes I'm frankly in awe of communities like KDE that manage to maintain a massive codebase together, with very little overhead, through sheer dedication and skill. Those donations don't go to waste.
At this point I would also like to apologize to anyone who was looking for reviews or other support from me elsewhere in Plasma (notably, PowerDevil) which I haven't helped with. I get stressed when having to divide my time and focus between different tasks, so I tend to avoid it, in the knowledge that someone or something will be left wanting. I greatly admire people who wear lots of different hats simultaneously, and it would surely be so nice to have the aptitude for that, but it kills me so I have to pick one battle at a time.
Right now, that's gestures. Soon, a little bit of travel. Then gestures again. Once that's done, we'll see what needs work most urgently or importantly.
Take care & till next time!
Developing an application for desktop or embedded platforms often means choosing between Qt Widgets and Qt Quick to develop the UI. There are pros and cons to each. Qt, being the flexible framework that it is, lets you combine these in various ways. How you should integrate these APIs will depend on what you're trying to achieve. In this entry I will show you how to display Qt Widget windows on an application written primarily using Qt Quick.
Qt Quick is great for software that puts emphasis on visual language. A graphics pipeline, based around the Qt Quick Scene Graph, will efficiently render your UI using the GPU. This means UI elements can be drawn, decorated, and animated efficiently as long as you pick the right tools (e.g. Shaders, Animators, and Qt's Shapes API instead of its implementation of HTML's Canvas).
From the Scene Graph also stem some of Quick's weaknesses. UI elements that in other applications would extend outside of the application's window, such as tool tips and the ComboBox control, can only be rendered inside of Qt Quick windows. When you see other app's tooltips and dropdowns extend beyond the window, those items are being rendered onto a separate windows; one without window decorations (a.k.a. borderless windows). Rendering everything on the same window helps ensure your app will be compatible with systems that can only display a single window at a time, such as Android and iOS, but it could result in wasted space if your app targets PC desktop environments.
An animation shows a small window with QML's and Widget's ComboBoxes opening for comparison purposes
QML ComboBox is confined to the Qt Quick window while the Widgets ComboBox extends beyond the window
Qt lets us combine Widgets and Quick in a few ways. The most common approach is to embed a Qt Quick view into your Widgets app, using QQuickWidget. That approach is fitting for applications that primarily use Widgets. Another option is to render Widgets inside a Qt Quick component, by rendering it through a QQuickPaintedItem. However, this component will be limited to the same window confines as the rest of the items in your Quick window and it won't benefit from Scene Graph rendering optimizations, meaning you get the worst of both worlds.
A third solution is to open widget windows from your Qt Quick apps. This has none of the aforementioned drawbacks, however, the approach has a couple of drawback of its own. First, the app would need to be run from a multi-window per screen capable environment. Second, widget windows are not parentable to Qt Quick windows; meaning certain window z-stack related features, such as setting window modality to Qt::WindowModal, won't have effect on the triggering window when a Widget is opened from Qt Quick. You can work around that by setting modality to Qt::ApplicationModal instead, if you're okay with blocking all other windows for modality.
Displaying Widget windows in Qt Quick applications has been useful to me in the past, and is something I haven't seen documented anywhere, hence this tutorial.
Displaying a Qt Widget window from Qt Quick is simpler than it seems. You'll need two classes:
You might be tempted to forgo the interface class and instantiate the widget directly. However, this would result in a crash. We'll display the widget window by running Widget::show
from the interface class.
CMakeLists.txt
In addition to those classes, you'll also need to make sure that your app links to both Qt::Quick
and Qt::Widgets
libraries. Here's what that looks like for a CMake project
// Locate libraries
find_package(Qt6 6.5 REQUIRED COMPONENTS
Quick
Widgets)
// Link build target to libraries
target_link_libraries(${TARGET_NAME} PRIVATE
Qt6::Quick
Qt6::Widgets)
// Replace ${TARGET_NAME} with the name of your target executable
main.cpp
In addition to that, in main.cpp
you'll need to use QApplication in place of QGuiApplication.
QApplication app(argc, argv);
Prepare the interface layer as you would any C++ based Quick component. By this I mean: derive from QObject
, and use the Q_OBJECT
and QML_ELEMENT
macros to make your class available from QML.
// widgetFormHandler.h
#pragma once
class WidgetFormHandler : public QObject
{
Q_OBJECT
QML_ELEMENT
public:
explicit WidgetFormHandler(QObject *parent = nullptr);
};
// widgetFormHandler.cpp
WidgetFormHandler::WidgetFormHandler(QObject *parent)
: QObject(parent)
{
}
// widgetFormHandler.h
#pragma once
class WidgetsForm;
class WidgetFormHandler : public QObject
{
Q_OBJECT
QML_ELEMENT
public:
explicit WidgetFormHandler(QObject *parent = nullptr);
~WidgetFormHandler();
private:
std::unique_ptr<WidgetsForm> m_window;
}
Use std::make_unique
in the constructor to initialize the unique pointer to m_window.
Define the instantiating class' destructor to ensure the pointers are de-alocated, thus preventing memory leaks. If you stick to using smart pointers, C++ will do all the work for you; simply use the default destructor, like I do here. Make sure to define it outside of the class' header; some compilers have trouble dealing with the destructor when it's defined inside the header.
// widgetFormHandler.cpp
#include "widgetFormHandler.h"
WidgetFormHandler::WidgetFormHandler(QObject *parent)
: QObject(parent)
, m_window(std::make_unique<WidgetsForm>())
{
// ...
}
WidgetFormHandler::~WidgetFormHandler() = default;
Now we want to make properties from the widget available in QML. How we do this will depend on the property and on whether we will manipulate the property's value from both directions or only from one side only and update on the other.
Let's look at a bi-directional example in which we add the ability to control the visible state of the widget window from QML. We'll add a property called "visible" to the C++ interface so that it matches the visible that we get from Qt Quick windows in QML. Declare the property using Q_PROPERTY
. Use READ and WRITE functions to control the window's state.
Here's what that would look like:
// widgetFormHandler.h
#pragma once
class WidgetsForm;
class WidgetFormHandler : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged)
public:
explicit WidgetFormHandler(QObject *parent = nullptr);
~WidgetFormHandler();
const bool isVisible();
void setVisible(bool);
signals:
void visibleChanged();
private:
std::unique_ptr<WidgetsForm> m_window;
};
// widgetFormHandler.cpp
#include "widgetFormHandler.h"
#include "widgetForm.h"
WidgetFormHandler::WidgetFormHandler(QObject *parent)
: QObject(parent)
, m_window(std::make_unique<WidgetsForm>())
{
// Hide window by default
m_window->setVisible(false);
}
WidgetFormHandler::~WidgetFormHandler() = default;
const bool WidgetFormHandler::isVisible()
{
return m_window->isVisible();
}
void WidgetFormHandler::setVisible(bool visible)
{
m_window->setVisible(visible);
emit visibleChanged();
}
To make this bi-directional, set NOTIFY to a signal that allows the property to be updated in QML after it being emitted and emit the signal where applicable. We emit it from setVisible in this class, however if QWidget
had a signal that emitted when its visible state changed, I would also make a connection between that signal and that of our handler’s visibleChanged
. However, that isn’t the case, so we have to make sure to emit it ourselves.
Develop the widget window as you would any other widget. If you use UI forms, go to the header file and create a signal for each action that you wish to relay over to QML.
In this example we'll relay a button press from the UI file, so we'll create a button named pushButton in our ui file:
Qt Designer shows UI file with a button named pushButton, in camel case.
Now add a buttonClicked
signal to our header:
// widgetsForm.h
#pragma once
#include <QWidget>
namespace Ui
{
class WidgetsForm;
}
class WidgetsForm : public QWidget
{
Q_OBJECT
public:
explicit WidgetsForm(QWidget *parent = nullptr);
~WidgetsForm();
signals:
void buttonClicked();
// Signal to expose button click from Widgets window
private:
std::unique_ptr<Ui::WidgetsForm> ui;
};
Once again, we use a unique pointer, this time to hold the ui object. This is better than what Qt Creator templates give us because it means C++ handles the memory management for us and we can avoid the need for a delete statement in the destructor.
In the window's constructor, we make a connection between the UI's button's signal and the one that we've created to relay the signal for exposure.
// widgetsForm.cpp
#include "widgetsform.h"
#include "ui_widgetsform.h"
WidgetsForm::WidgetsForm(QWidget *parent)
: QWidget(parent)
, ui(std::make_unique<Ui::WidgetsForm>())
{
ui->setupUi(this);
// Expose click
connect(ui->pushButton, &QPushButton::clicked, this, &WidgetsForm::buttonClicked);
}
WidgetsForm::~WidgetsForm() = default;
Before we connect the exposed signal to the QML interface, we need another signal on the interface to expose our event over to QML. Here I add qmlSignalEmitter
signal for that purpose:
// widgetFormHandler.h
[..]
signals:
void visibleChanged();
void qmlSignalEmitter(); // Signal to relay button press to QML
[..]
To complete all the connections, go to the interface layer’s constructor and make a connection between your window class’ signal and that of the interface layer. This would look as follows:
// widgetFormHandler.cpp
[..]
WidgetFormHandler::WidgetFormHandler(QObject *parent)
: QObject(parent)
, m_window(std::make_unique<WidgetsForm>())
{
QObject::connect(m_window, &WidgetsForm::buttonClicked, this,
&WidgetFormHandler::qmlSignalEmitter);
}
[..]
By connecting one emitter to another emitter we keep each classes' concerns separate and reduce the amount of boilerplate code, making our code easier to maintain.
Over at the QML, we connect to qmlSignalEmitter
using the on prefix. It would look like this:
import NameOfAppQmlModule // Should match qt_add_qml_module's URI on CMake
WidgetFormHandler {
id: fontWidgetsForm
visible: true // Make the Widgets window visible from QML
onQmlSignalEmitter: () => {
console.log("Button pressed in widgets") // Log QPushButton's click event from QML
}
}
I've prepared a demo app where you can see this technique in action. The demo displays text that bounces around the screen like an old DVD player's logo would. You change the text and font through two identical forms, one implemented in QML and the other done in Widgets. The code presented in this tutorial comes from that demo app.
Example code: https://github.com/KDABLabs/kdabtv/tree/master/Blog-projects/Widget-window-in-Qt-Quick-app
The moving text should work on all desktop systems except for Wayland sessions on Linux. That is because I'm animating the window's absolute position (which is restricted in Wayland for security reasons) rather than the contents inside a window. This has the benefit of not obstructing other applications, since the moving window that contains the text would capture mouse inputs if clicked, preventing those from reaching the application behind it.
The first time I employed this technique was in my FOSS project, QPrompt. I use it there to provide a custom font dialog that doubles as a text preview. Having a custom dialog gives me full control over formatting options presented to users, and for this app we only needed a preview for large text and a combo box to choose among system fonts. QPrompt is also open source, you can find the source code relevant to this technique here: https://github.com/Cuperino/QPrompt-Teleprompter/blob/main/src/systemfontchooserdialog.h
Thank you for reading. I hope you’ll find this useful. A big thank you to David Faure for suggesting the use of C++ unique pointers as well as reviewing the code along with Renato and my team.
If there are other techniques that you’d like for us to try or showcase, let us know.
The post Display Widget Windows in Qt Quick Applications appeared first on KDAB.
Release notes: https://kde.org/announcements/gear/25.04.2/
Now available in the snap store!
Along with that, I have fixed some outstanding bugs:
Ark: now can open/save files in removable media
Kasts: Once again has sound
WIP: Updating Qt6 to 6.9 and frameworks to 6.14
Enjoy everyone!
Unlike our software, life is not free. Please consider a donation, thanks!
Kdenlive 25.04.2 is now available, containing several fixes and small workflow improvements. Some highlights include:
Some last minute fixes were also included in the Windows/Mac/AppImage versions:
See the full changelog below.
I'm Ajay Chauhan (Matrix: hisir:matrix.org), currently in my third year of undergraduate studies in Computer Science & Engineering. I'll be working on improving Kdenlive timeline markers for my Google Summer of Code project. I have previously worked on Kdenlive as part of the Season of KDE '24.
Kdenlive currently supports single-point timeline markers, which limits efficiency for workflows that require marking time ranges, such as highlight editing or collaborative annotations. This project proposes enhancing Kdenlive's marker system by introducing duration-based markers that define a clear start and end time.
The project will extend the marker data model to support a duration attribute while maintaining backward compatibility. The UI will be updated to visualize range markers as colored regions on the timeline, with interactive handles for resizing and editing.
These markers will be integrated with key features like zone-to-marker conversion, search and navigation, rendering specific ranges, and import/export capabilities.
The problem that this project aims to solve is the need for efficient range-based marking functionality in Kdenlive's timeline (see issue #614). By implementing duration-based markers, the project will ensure that video editors can work more efficiently with time ranges for various workflows like highlight editing, collaborative annotations, and section-based organization.
My mentor for the project is Jean-Baptiste Mardelle, and I appreciate the opportunity to collaborate with and learn from him during this process.
CommentedTime
ClassThe CommentedTime
class, which represents individual markers, has been extended to support duration information. This change enables the range marker functionality throughout the application.
I added several new methods and properties to the CommentedTime
class:
duration()
: Returns the marker's duration as a GenTime
objectsetDuration()
: Sets the marker's durationhasRange()
: Boolean check to determine if a marker is a range marker (duration > 0)endTime()
: Calculates and returns the end position (start + duration)The class now includes a new constructor that accepts duration as a parameter, while maintaining backward compatibility with existing point marker creation.
// New constructor with duration support
CommentedTime(const GenTime &time, QString comment, int markerType, const GenTime &duration);
// New member variable
GenTime m_duration{GenTime(0)}; // Defaults to 0 for point markers
🔗 Commit: Add duration handling to CommentedTime class
MarkerListModel
Previously, Kdenlive only supported point markers - simple markers that existed at a specific timestamp without any duration. I've now implemented range marker support, allowing users to create markers that span across time intervals.
The core changes involved extending the MarkerListModel
class with several new methods:
addRangeMarker()
: A new public method that creates markers with both position and durationaddOrUpdateRangeMarker_lambda()
: An internal helper function that handles both creating new range markers and updating existing oneseditMarker()
: Added an overloaded version that preserves duration when editing markersThe implementation uses a lambda-based approach for undo/redo functionality, ensuring that range marker operations integrate seamlessly with Kdenlive's existing command pattern. When updating existing markers, the system intelligently determines whether to preserve the current duration or apply a new one.
// New method signature for range markers
bool addRangeMarker(GenTime pos, GenTime duration, const QString &comment, int type = -1);
// Extended edit method with duration support
bool editMarker(GenTime oldPos, GenTime pos, QString comment, int type, GenTime duration);
The model now emits appropriate data change signals for duration-related roles (DurationRole
, EndPosRole
, HasRangeRole
) when range markers are modified, ensuring the UI stays synchronized.
🔗 Commit: Implement range marker support in MarkerListModel
All existing marker functionality continues to work exactly as before. Point markers are simply range markers with zero duration, ensuring a smooth transition for existing projects and workflows.
In the upcoming weeks, with the core range marker backend in place, the next phase will focus on:
Welcome to a new issue of "This Week in KDE Apps"! Every week we cover as much as possible of what's happening in the world of KDE apps.
This week issue is a bit special as it is also covering the past week as last Sunday some other contributors and me were busy at the KDE booth at the Umweltfestival in Berlin.
Additionally, as it is the beginning of Pride Month, I would like to take this opportunity to celebrate and acknowledge the invaluable contributions of LGBTQIA+ members within the KDE community. Their work, creativity, and dedication continue to enrich our project and foster a more inclusive and diverse environment for all.
This celebration is especially important at a time when many large tech corporations are rolling back their visible support for the LGBTQIA+ community. KDE and other grass roots organisations have your back!
Getting back to all that's new in the KDE App scene, let's dig in!
Christoph Cullmann published a blog post about the state of KDE apps in the Microsoft Store. Things are looking good!
Stefan Brüns speed up the parsing of MobiPocket files considerably (25.08.0 - link 1, link 2 and link 3) and also fixed some parsing issues (link1 and link 2). These changes were implemented in QMobiPocket and improve the rendering speed in Okular for MobiPocket documents but also speed up Baloo indexing of these files and the creation of thumbnails in Dolphin.
Carl Schwan did some further improvements and reduced the number of temporary allocations (link 1, link 2 and link 3)
Carl Schwan fixed the detection of HTML inside mobipocket files (25.04.3 - link).
Sune Vuorela improved the scaling of stamps for annotations (25.08.0 - link). Now, when using a non-default sized stamp, the stamp won't appear pixelated anymore.
Victor Blanchard added an option to play the next video automatically in a playlist (link).
Aleksandr Borodetckii added the option to sort the songs in the "Played songs" list by "last added" (link).
Volker added a departure details dialog. This dialog contains service alerts, occupancy information, vehicle amenities and the operator information when available (link), and published his bi-monthly blog post about all the changes in Itinerary, KTrip, Transitous and co.
Johannes Krattenmacher added ticket extractors for Stena, Viking and IHG (link).
Matthieu Carteron ported Kig to Qt6/KF6 (25.08.0 - Link).
Freya Lupen fixed a crash when clearing the text field in text brush pipe mode (link).
Victor Blanchard added an off-by-default setting to automatically switch to icons view mode in folders with a lot of image or video files (25.08.0 - link).
Kai Uwe Broulik simplified how locations in window and tab titles for search results pages are displayed (25.08.0 - link).
Vladislav Kachegov fixed an incorrect view reset when unmounting similarly-named devices (link).
Joshua Goins made it possible to boost your own private post (25.04.03 - link), improved the tooltips for disabled polls and attachment buttons (25.08.0 - link) and did some small improvements to the multi account handling (link).
Joshua also limited the number of poll choice using the limit defined by the server (25.08.0 - link) and fixed a crash when clicking on "Mark as Read" on the notifications page (25.04.3 - link).
James Graham made the view position itself correctly at the bottom when switching rooms (25.08.0 - link).
Fabio Bas fixed a crash when the main window is outside of any screen (25.04.03 - link).
Julius Künzel fixed opening links with a relative URLs (25.04.03 - link 1 and link 2).
Allen Winter fixed a crash in KOrganiser (link) and added a feature to display times in 24h format in the agenda view (link).
Manuel Alcaraz created a new logo for Chessament (link).
Manuel also added a feature to select the color of the player. This can be set manually or be randomly chosen (link).
Laurent started working on a frontend for Ollama.
Siavosh Kasravi enabled a feature to save the prompt history in the session so that you can quickly re-execute or edit a previous prompt.
Anyone familiar with my blog will know that I like to write about incense. A reader wrote to me some time ago asking about what sticks I’ve been enjoying lately, and it occurred to me that it might be a nice thing to have a “now listening” type feature on my website, so that fellow incense heads could get a sense of the types of incense I like. After all, while I write plenty of incense reviews, they represent only a small percentage of the sticks, cones, powders, woods, and resins I’m burning or heating from day to day. (If you’re here for my incense content, feel free to skip this one and head to /now-burning to see the new feature!)
While it would have been simple enough for me to build a microblogging feature into my Eleventy website, the trouble was wanting to use it after it was built. Unlike using a CMS such as WordPress to make a website, I knew of no nice interface for Eleventy, or for that matter any SSG, that would help me create a post and publish it online without opening an IDE[1] and using the command line. Instead, the process looks something like this:
As big of a nerd as I am, I’m just not going to want to do that multiple times a day for what amounts to a status post. This lead me to scour the internet looking for a solution: something that I could run on my own desktop or laptop that could build my site locally and push changes to my website, hosted the old fashioned way: as a bunch of text files sitting on a server accessible via SFTP. No needless complexity like running Eleventy on the server, or using a host like Netlify.[2] Surely there’d be something, right? Surely, the realm of SSGs can’t be without at least one nice, local user interface that people can use without being a web developer?
In the end, I did find one answer to the problem: Publii. Publii seems to be made predominantly with end-users in mind, however. It’s not just a local[3] CMS, it’s an SSG in its own right, which does me no good as I can’t make it work with my website[4]. So after coming up with nothing I could use, I gave the idea a rest for a while until I had the epiphany that I could solve the problem with a simple script using KDE’s KDialog to provide a rudimentary UI. So that’s what I did.
The idea was simple: a wizard-like experience that guides the user through the creation of a microblog / status post. Post types and the data they collect should be customized by the user via a JSON configuration file. After the post data is collected from the user, the script should execute a user-defined build command as well as a user-defined command to sync the static files to the server.
For some reason, I decided to write my script in Ruby, a language for which I once completed a course before promptly forgetting everything I knew about it. I would have had a much easier time using JavaScript and Node, which I am much more familiar with and have successfully used for similar purposes. Why I did not is anyone’s guess. All this to say: please do not make (too much) fun of my shitty little script, which I have dubbed “Poaster.”
I started with the JSON configuration file, /Poaster/config/config.json
:
{
"buildCommand": "npx @11ty/eleventy",
"postTypes": [
{
"name": "Now Burning",
"postUnitName": "incense",
"contentEnabled": true,
"frontMatter": [
{
"name": "title"
},
{
"name": "manufacturer"
},
{
"name": "date"
},
{
"name": "time"
}
],
"postDirectory": "/post/output/dir"
}
],
"uploadCommand": "rsync -av --del /local/path/to/site/output
username@my.server:/remote/path/to/public/site/files",
"siteDirectory": "/local/path/to/site/repo"
}
Here, the user can specify as many post types as they like, each with their own output directory. Each post type can also collect as many pieces of frontmatter as the user cares to specify.
The first thing the script needed to do was ask the user which post type they want to create, so I referenced the KDialog tutorial and wrote a method to handle that /Poaster/lib/spawn_radio_list.rb
:
def spawn_radio_list(title, text, options_arr)
command = %(kdialog --title "#{title}" --radiolist "#{text}")
options_arr.each_with_index do |option, i|
command += %( #{i} "#{option}" off)
end
`#{command}`
end
I wrote a few more methods in /Poaster/lib
to spawn toast notifications, input boxes, create directories if they don’t exist, and write files:
/Poaster/lib/spawn_toast.rb
:
def spawn_toast(title, text, seconds)
`kdialog --title "#{title}" --passivepopup "#{text}" #{seconds}`
end
/Poaster/lib/spawn_input_box.rb
:
def spawn_input_box(title, text)
`kdialog --title "#{title}" --inputbox "#{text}"`
end
/Poaster/lib/ensure_dir_exists.rb
:
def ensure_dir_exists(directory_path)
unless Dir.exist?(directory_path)
FileUtils.mkdir_p(directory_path)
spawn_toast 'Directory Created', %(Poaster created #{directory_path}.), 10
end
end
/Poaster/lib/write_file.rb
:
def write_file(directory, name, extension, content)
post_file = File.new(%(#{directory}/#{name}.#{extension}), 'w+')
post_file.syswrite(content)
post_file.close
end
All I had to do then was tie it all together in /Poaster/poaster.rb
:
#!/usr/bin/env ruby
require 'json'
require 'fileutils'
require './lib/spawn_input_box'
require './lib/spawn_radio_list'
require './lib/spawn_toast'
require './lib/ensure_dir_exists'
require './lib/write_file'
config_data = JSON.parse(File.read('./config/config.json'))
dialog_title_prefix = 'Poaster'
# Populate types_arr with post types
post_types_arr = []
config_data['postTypes'].each do |type|
post_types_arr.push(type['name'])
end
# Display post list dialog to user
post_type = config_data['postTypes'][Integer(spawn_radio_list(dialog_title_prefix, 'Select a post type:', post_types_arr))]
# Set the word we will use to refer to the post
post_unit = post_type['postUnitName']
# Collect frontmatter from user
frontmatter = []
post_type['frontMatter'].each do |item|
frontmatter.push({ item['name'] => spawn_input_box(%(#{dialog_title_prefix} - Enter Frontmatter'), %(Enter #{post_unit} #{item['name']}:)) })
end
# Collect post content from user
post_content = spawn_input_box %(#{dialog_title_prefix} - Enter Content), %(Enter #{post_unit} content:)
# Make sure the output folder exists
post_directory = post_type['postDirectory']
ensure_dir_exists(post_directory)
# Create post string
post = %(---\n)
post_id = ''
frontmatter.each_with_index do |item, i|
post += %(#{item.keys[0]}: #{item[item.keys[0]]})
post_id += %(#{item[item.keys[0]].chomp}#{i == frontmatter.length - 1 ? '' : '_'})
end
post += %(---\n#{post_content})
# Write post string to file and notify user
post_file_name = %(#{post_type['name']}_#{post_id.chomp})
post_extension = 'md'
write_file post_directory, post_file_name, post_extension, post
spawn_toast 'File Created', %(Poaster created #{post_file_name}#{post_extension} at #{post_directory}.), 10
# Run build and upload commands
`cd #{config_data['siteDirectory']} && #{config_data['buildCommand']} && #{config_data['uploadCommand']}`
There is a lot that this script should do that it doesn’t, but for now, it’s still a handy wee utility for SSG users on GNU/Linux systems running KDE who want to make creating quick status-type posts a little less painful. Just make sure KDialog is installed (as well as Ruby, naturally), clone the repo, create /Poaster/config/config.json
to meet your needs using the example as a reference and you’re off to the races! I’ve even made a silly little toaster icon using assets from some of the KDE MimeType icons that you can use if you want to make a .desktop
file so that you can click an icon on your app launcher to start the script.
My poaster.desktop
file looks something like this:
[Desktop Entry]
Exec=/path/to/poaster.rb
GenericName[en_US]=Create a post with Poaster.
GenericName=Create a post with Poaster.
Icon=/path/to/poaster_icon.svg
Name=Poaster
NoDisplay=false
Path=/path/to/repo/
StartupNotify=true
Terminal=false
Type=Application
Here’s the script in action:
To build the new “now burning” incense microblog feature, I created two new pages. /now-burning shows the latest entry:
---
layout: layouts/base.njk
title: "Nathan Upchurch | Now Burning: What incense I'm burning at the moment."
structuredData: none
postlistHeaderText: "What I've been burning:"
---
{% set burning = collections.nowBurning | last %}
<h1>Now Burning:</h1>
<article class="post microblog-post">
<img class="microblog-icon" src="/img/censer.svg">
<div class="microblog-status">
<h2 class="">{{ burning.data.title }}{% if burning.data.manufacturer %}, {{ burning.data.manufacturer }}{% endif %}, {{ burning.date | niceDate }}, {{ burning.data.time }}</h2>
{% if burning.content %}
<div class="microblog-comment">
{{ burning.content | safe }}
</div>
{% endif %}
</div>
</article>
<a href="/once-burned/">
<button type="button">Previous Entries »</button>
</a>
…and /once-burned shows past entries:
---
layout: layouts/base.njk
title: "Nathan Upchurch | Once Burned: Incense I've burning in the past."
structuredData: none
---
{% set burning = collections.nowBurning | last %}
<h1>Previous “Now Burning” Entries:</h1>
{% set postsCount = collections.nowBurning | removeMostRecent | length %}
{% if postsCount > 0 %}
{% set postslist = collections.nowBurning | removeMostRecent %}
{% set showPostListHeader = false %}
{% include "incenseList.njk" %}
{% else %}
<p>Nothing’s here yet!</p>
{% endif %}
<a href="/now-burning/">
<button type="button">Latest »</button>
</a>
…using a post-listing include built specifically for microblogging:
<section class="postlist microblog-list">
{% if postlistHeaderText %}<h2>{{ postlistHeaderText }}</h2>{% endif %}
<div class="postlist-item-container">
{% for post in postslist | reverse %}
<article class="postlist-item">
<div class="post-copy">
<h3>
{% if post.data.title %}{{ post.data.title | safe }}{% else %}?{% endif %}{% if post.data.manufacturer %}, {{ post.data.manufacturer | safe }}{% endif %}
</h3>
<div class="post-metadata">
<div class="post-metadata-copy">
<p>
<time datetime="{{ post.date | htmlDateString }}">{{ post.date | niceDate }}{% if post.data.time %}—{{ post.data.time }}{% endif %}</time>
</p>
</div>
</div>
{% if post.content %}
<div class="microblog-comment">
{{ post.content | safe }}
</div>
{% endif %}
</div>
</article>
<hr>
{% endfor %}
</div>
</section>
And that’s about it! There’s a lot to do to make the script a little less fragile, such as passing along build / upload error messages, allowing for data validation via regex, et cetera. I’m sure I’ll get to it at some point. If Poaster is useful to you, however, and you’d like to submit a patch to improve it, please do let me know.
At risk of sounding crabbit and behind the times, I don’t know why web development has to be so damned complicated these days. Like, an entire fancy for-profit infrastructural platform that exists just to host static websites? It seems nuts to me. ↩︎
Thank christ. Why does everything need to run in the cloud when we already have computers at home? ↩︎
I did however use it to very quickly set up a nice looking blog site for my partner. ↩︎