Janet's Shenanigans

libre

The source for all the tools mentioned in this blog post is available here

A screenshot of QML DAP providing debugger functionality for QML in VSCode

qml-dap: QML debugger for editors

While working on qml-lsp, I took a tangent to write a DAP implementation for QML. This ended up being a very long tangent, but it's worth it: being able to debug QML without needing Qt Creator available. The DAP protocol is the debugger equivalent to LSP: it's a cross-editor and cross-language protocol that allows debuggers to implement DAP and get support for a bunch of editors, and allows editors to implement DAP and get support for a bunch of debuggers.

In short, this means that you can use qml-dap in combination with a DAP-supporting editor of your choice, and gain access to a debugger.

Note that QML DAP doesn't support the entirety of DAP, but supports enough to serve as an improvement over print debugging. More of the protocol will be covered as I improve qml-dap.

qml-dbg: QML debugger for the terminal

While writing the code necessary for debugging QML programs for qml-dap, I realised that QML had no command line debugger. So, I wrote one, and called it qml-dbg.

The output of a sample qml-dbg session is provided here for your browsing:

❯ qml-dbg
Hi, welcome to qml-dbg! Type "help" if you want me to explain how you use me.
> attach localhost:5050
Connecting to localhost:5050...
I connected to the program! Now, you can start debugging it.
> b a.qml:13
I set breakpoint #0
The program will pause right before it starts to run that line of code
You can disable it with 'breakpoint-disable 0'
See active breakpoints with 'breakpoints'
> breakpoints
Breakpoints:
        #0 at a.qml:13
> breakpoint-disable 0
Disabled breakpoint 0
> breakpoints
Breakpoints:
        (disabled) #0 at a.qml:13
> breakpoint-enable 0
Enabled breakpoint 0
The program paused!
Run 'backtrace' to see a stack trace.
> 
> bt
Most recently called function:
        onClicked in a.qml:13 (file:///home/jblackquill/Scratch/a.qml)
> eval btn1.text = "hello debugger!"
"hello debugger!"
> continue
> 

qml-lint: standalone linting tool

If you really liked qml-lsp's lints, then you can now have them standalone on the command-line.

❯ qml-lint a.qml 
12:3 - 12:6     a.qml   Don't use var in modern JavaScript. Consider using "const" here instead. (var lint)

        var a = 5

15:2 - 15:14    a.qml   Don't use anchors.fill in a RowLayout. Instead, consider using "Layout.fillWidth: true" and "Layout.fillHeight: true" (anchors in layouts lint)

        anchors.fill: parent

Tags: #libre

The Cool Now

With the infrastructure I built in qml-lsp for parsing and analysing QML files, I thought “hm, since doxyqml is just a glorified qml parser –> c++ header file converter, wouldn't it be trivial to write the same thing in go reusing qml-lsp's infrastructure?” And that's exactly what I did. I wrote a 130-line program that faithfully replicated doxyqml's functionality in Go.

By virtue of being a Go program that calls on a pretty optimised parser in C, it ended up being a little over 10 times faster than doxyqml on my system.

I wasn't done there.

I thought “hmm, couldn't I reuse the semantic analysis I did for qml-lsp to improve the output a bit?”

So, that's pretty much what I did.

Currently, the most notable improvement over doxyqml is in producing a better superclass for the output:

doxyqml, with aliased import (import foo as bar):

class Avatar : public QtQuick.Controls.Control {

doxyqml, without aliased import:

class Avatar : Control {

One isn't valid C++ (I'm surprised Doxygen takes it at all), and the other fails to specifically name where Control comes from, leading to issues with Doxygen trying to locate the superclass.

qml-doxygen reuses the semantic analysis from qml-lsp to generate the following output, whether the import is aliased or not:

class Avatar : public QtQuick::Controls::Control {

It's both valid C++, and tells Doxygen exactly where the name is coming from.

The Roadmap

The next thing I'm planning to do is to resolve the concrete type of an alias property, so that documentation generation for aliases can be improved without developers needing to explicitly tell the computer what type the alias points to.

I may also add the ability to “splat” grouped properties with a special sigil, so that something like readonly property AvatarGroup actions: AvatarGroup { } can be expanded into the properties of the AvatarGroup by qml-doxygen, resulting in better documentation.

Tags: #libre

qml-lsp

I worked on fancying up qml-lsp's underlying infrastructure and added some new features. One of the more noticeable things is that attached properties can complete now:

screenshot of completing an attached property

There's also some lints:

screenshot of linting warning about an unused import

Currently there's some lints warning against usage of language features that negatively impair readability, (with statements, alias, etc.), warning about using anchors in a Layout, and unused imports.

Shades

Shades is a small utility app I made that lets you grab shades of a colour.

screenshot of shades

It uses the OKLAB colour space to generate shades, which means it produces really good variations on a colour that only differ in visual brightness instead of going “off-brand” like other colour spaces would have.

Tok

Yeah, Tok just has the “saved messages” room display as such instead of using your account's name and avatar. Not much, which is why it's getting lumped in here instead of in its own blog post.

Tags: #libre

This response from a reader pretty much sums it up:

Stop One: Will Craft Work?

Craft seems like the no-brainer for a KDE project to use, considering it's in-house and supports all of KDE's frameworks and has packaging capabilities. Unfortunately, what sounds good on paper does not translate to what's good in execution.

When I first checked it out, the current version being shipped had incorrect code that would have failed to compile had it been a compiled language instead of Python. Heck, even running a typechecker for Python would have revealed the fact that the code was trying to call a function that didn't exist. Yet, this managed to get shipped. Not a good first impression.

After manually patching in the function into existence on my system, I ran into another hurdle: Craft's env script is broken; mangling PATH to an extent where entries like /usr/bin and /bin and other things got just truncated into oblivion, resulting in a shell where you couldn't do much of anything.

After manually patching PATH to be not mangled, I ran into another and the final hurdle before I gave up: Craft tried to use half bundled libraries and tools and half system libraries and tools, resulting in dynamic linker errors from system tools not finding symbols they needed from bundled libraries.

When I brought these issues up in the Craft chat, the answers basically amounted to a lack of care and “go use Ubuntu.” Not acceptable for Tok considering most of the people interested in building Tok like this don't use Ubuntu, and honestly doesn't make you have much faith in a system for porting utilities to other platforms if said system doesn't even work across the distributions of one platform.

Stop Two: Maybe Conan?

Conan seems like the second-in-line no-brainer for Tok to use. It's the largest C++ package manager, even supporting Qbs. Of course, like with Craft, what sounds good on paper doesn't match up to execution.

Out of the gate, I looked at the Qt package, only to find that there was one (1) Qt package for it consisting of the entirety of Qt, WebEngne and all. Kinda oof, but not a deal breaker. Well, it wouldn't be a dealbreaker if Conan had prebuilt Qt packages for

- Settings: arch=x86_64, build_type=Release, compiler=gcc, compiler.version=11, os=Linux

But it doesn't. I'm not going to build an entire web engine just for an attempt at maybe getting a non-Linux build of Tok, and having to build a web engine as part of Tok's CI is a no-go in terms of disk, memory and CPU.

Stop Three: vcpkg

Considering Microsoft has been a developer tools company for about as thrice as long as I've been alive, I hope their take at the C++ package manager thing is worth their salt.

Some weirdness ensued with the VCPKG_ROOT environment variable at first, but it was easy to fix by pointing it at the vcpkg repo.

While doing the vcpkg install, I found the output somewhat hard to follow, so I had no idea how far along it was. I just let it sit since it seemed to be making progress.

While vcpkg didn't have prebuilt binaries for my setup, it didn't require building all Qt modules like Conan did, so the ask was much more reasonable.

And then I noticed a big issue: vcpkg has absolutely zero versioning, other than the git repository with all the package manifests. This essentially means that in order to build with Qt5, I need to commit to an ancient version of vcpkg packages and stay there indefinitely. I also have to ask users to roll back their vcpkg install that far to build Tok. Not really acceptable as an ask for people who might want to build Tok for not Linux.

Stop Four: Wait, It's Already Here (At Least For Macs)

Turns out Nix, the thing that Tok already supports building with, also supports macOS. Well, that was easy. While it doesn't spit out a premade .app like other porting systems can do, it does ensure a working build with available dependencies, which is already most of the way there.

Conclusion: Apples And Penguins Rule, Everyone Else Drools

Cross-platform C++ packaging and distribution is still a very unsolved problem, unlike other languages/frameworks like Go, Electron, Rust, Zig, etc. as I learned painfully through these escapades. Nix seems the most promising on this front, as it offers a very consistent environment across platforms, which gets you most of the way there in terms of building. It doesn't support Windows (yet?), but simply being able to use a functional compiler instead of Apple Clang is already a killer feature for using it to port apps to macOS.

Qbs is also a huge help in terms of the porting process, as it natively supports building things that require auxiliary scripts or build system hacks with other build systems, like .app bundles, Windows installers, and multi-architecture .app/.aab/.apks with just a few or no extra lines in the build system.

For Tok on macOS, all I need to do is add these two lines to the build script in order to get a usable .app file from it:

Depends { name: "bundle" }
bundle.isBundle: true

While it lacks a lot of metadata that you need to fill in yourself, it's again, another 99% of the way there solution where the remaining 1% is mostly just a little data or boilerplate or running a tool.

I still haven't figured out what I'll be doing for Windows, but the need for an end-user Windows package is still a long ways off, considering Tok is still nowhere near a 1.0 release status. Perhaps I can make leverage of Fedora's mingw packages or check out https://mxe.cc/, or maybe just install dependencies from source to a Windows system without a package manager, and bundle them during the build process. If you have any suggestions, do feel free to hop on by in the Tok chat and drop them.

Tags: #libre

This is the sequel post to the previous post on hRPC; after hRPC has matured and gotten an actual spec written for it. This post contains much more information about hRPC, as well as tutorials for it.

This post is mirrored from our post on dev.to

Co-authored by: Yusuf Bera Ertan (yusdacra@GitHub), Danil Korennykh (Bluskript@GitHub), Janet Blackquill (pontaoski@GitHub)

hRPC is a new RPC system that we, at Harmony, have been developing and using for our decentralized chat protocol. It uses Protocol Buffers (Protobufs) as a wire format, and supports streaming.

hRPC is primarily made for user-facing APIs and aims to be as simple to use as possible.

If you would like to learn more, the hRPC specification can be found here.

What is an RPC system? If you know traditional API models like REST, then you can think of RPC as a more integrated version of that. Instead of defining requests by endpoint and method, requests are defined as methods on objects or services. With good code generation, an RPC system is often easier and safer to use for both clients and servers.

Why hRPC?

hRPC uses REST to model plain unary requests, and WebSockets to model streaming requests. As such, it should be easy to write a library for the languages that don't already support it.

hRPC features:

  • Type safety
  • Strict protocol conformance on both ends
  • Easy streaming logic
  • More elegant server and client code with interfaces/traits and endpoint generation.
  • Cross-language code generation
  • Smaller request sizes
  • Faster request parsing

Why Not Twirp?

Twirp and hRPC have a lot in common, but the key difference that makes Twirp a dealbreaker for harmony is its lack of support for streaming RPCs. Harmony's vision was to represent all endpoints in Protobuf format, and as a result Twirp became fundamentally incompatible.

Why Not gRPC?

gRPC is the de-facto RPC system, in fact protobuf and gRPC come together a lot of the time. So the question is, why would you want to use hRPC instead?

Unfortunately, gRPC has many limitations, and most of them result from its low-level nature.

The lack of web support

At Harmony, support for web-based clients was a must, as was keeping things simple to implement. gRPC had neither. As stated by gRPC: > It is currently impossible to implement the HTTP/2 gRPC spec in the browser, as there is simply no browser API with enough fine-grained control over the requests.

The gRPC slowloris

gRPC streams are essentially just a long-running HTTP request. Whenever data needs to be sent, it just sends a new HTTP/2 frame. The issue with this, however, is that most reverse proxies do not understand gRPC streaming. At Harmony, it was fairly common for sockets to disconnect because they are idle for long stretches of time. NGINX and other reverse proxies would see these idle connections, and would close them, causing issues to all of our clients. hRPC's use of WebSockets solves this use-case, as reverse proxies are fully capable to understand them.

In general, with hRPC we retain the bulk of gRPC's advantages while simplifying stuff massively.

Why not plain REST?

Protobuf provides a more compact binary format for requests than JSON. It lets the user to define a schema for their messages and RPCs which results in easy server and client code generation. Protobuf also has features that are very useful for these kind of schemas (such as extensions), and as such is a nice fit for hRPC.

A Simple Chat Example

Let's try out hRPC with a basic chat example. This is a simple system that supports posting chat messages which are then streamed back to all clients. Here is the protocol:

syntax = "proto3";

package chat;

// Empty object which is used in place of nothing
message Empty { }

// Object that represents a chat message
message Message { string content = 1; }

service Chat {
  // Endpoint to send a chat message
  rpc SendMessage(Message) returns (Empty);
  // Endpoint to stream chat messages
  rpc StreamMessages(Empty) returns (stream Message);
}

By the end, this is what we will have:

vue client being demonstrated

Getting Started

NOTE: If you don't want to follow along, you can find the full server example at hRPC examples repository.

Let's start by writing a server that implements this. We will use hrpc-rs, which is a Rust implementation of hRPC.

Note: If you don't have Rust installed, you can install it from the rustup website.

We get started with creating our project with cargo new chat-example --bin.

Now we will need to add a few dependencies to Cargo.toml:

[build-dependencies]
# `hrpc-build` will handle generating Protobuf code for us
# The features we enable here matches the ones we enable for `hrpc`
hrpc-build = { version = "0.29", features = ["server", "recommended"] }

[dependencies]
# `prost` provides us with protobuf decoding and encoding
prost = "0.9"
# `hrpc` is the `hrpc-rs` main crate!
# Enable hrpc's server features, and the recommended transport
hrpc = { version = "0.29", features = ["server", "recommended"] }
# `tokio` is the async runtime we use
# Enable tokio's macros so we can mark our main function, and enable multi
# threaded runtime
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
# `tower-http` is a collection of HTTP related middleware
tower-http = { version = "0.1", features = ["cors"] }
# Logging utilities
# `tracing` gives us the ability to log from anywhere we want
tracing = "0.1"
# `tracing-subscriber` gives us a terminal logger
tracing-subscriber = "0.3"

Don't forget to check if your project compiles with cargo check!

Building the Protobufs

Now, let's get basic protobuf code generation working.

First, go ahead and copy the chat protocol from earlier into src/chat.proto.

After that we will need a build script. Make a file called build.rs in the root of the project:

// build.rs
fn main() {
    // The path here is the path to our protocol file
    // which we copied in the previous step!
    //
    // This will generate Rust code for our protobuf definitions.
    hrpc_build::compile_protos("src/chat.proto")
        .expect("could not compile the proto");
}

And lastly, we need to import the generated code:

// src/main.rs
// Our chat package generated code
pub mod chat {
    // This imports all the generated code for you
    hrpc::include_proto!("chat");
}

// This is empty for now!
fn main() { }

Now you can run cargo check to see if it compiles!

Implementing the Protocol

In this section, we will implement the protocol endpoints.

First, get started by importing the stuff we will need:

// src/main.rs
// top of the file

// Import everything from chat package, and the generated
// server trait
use chat::{*, chat_server::*};
// Import the server prelude, which contains
// often used code that is used to develop servers.
use hrpc::server::prelude::*;

Now, let's define the business logic for the Chat server. This is a simple example, so we can just use channels from tokio::sync::broadcast. This will allow us to broadcast our chat messages to all clients connected.

// ... other `use` statements

// The channel we will use to broadcast our chat messages
use tokio::sync::broadcast;

Afterwards we can define our service state:

pub struct ChatService {
    // The sender half of our broadcast channel.
    // 
    // We will use it's `.subscribe()` method to get a
    // receiver when a client connects.
    message_broadcast: broadcast::Sender<Message>,
}

Then we define a simple constructor:

impl ChatService {
    // Creates a new `ChatService`
    fn new() -> Self {
        // Create a broadcast channel with a maximum 100
        // amount of items that can be pending. This
        // doesn't matter in our case, so the number is
        // arbitrary.
        let (tx, _) = broadcast::channel(100);
        Self {
            message_broadcast: tx,
        }
    }
}

Now we need to implement the generated trait for our service:

impl Chat for ChatService {
    // This corresponds to the SendMessage endpoint
    // 
    // `handler` is a Rust macro that is used to transform
    // an `async fn` into a properly typed hRPC trait method.
    #[handler]
    async fn send_message(&self, request: Request<Message>) -> ServerResult<Response<Empty>> {
        // we will add this in a bit
    }
    
    // This corresponds to the StreamMessages endpoint
    #[handler]
    async fn stream_messages(
        &self,
        // We don't use the request here, so we can just ignore it.
        // The leading `_` stops Rust from complaining about unused
        // variables!
        _request: Request<()>,
        socket: Socket<Message, Empty>,
    ) -> ServerResult<()> {
        // we will add this in a bit
    }
}

And now for the actual logic, let's start with message sending:

#[handler]
async fn send_message(&self, request: Request<Message>) -> ServerResult<Response<Empty>> {
    // Extract the chat message from the request
    let message = request.into_message().await?;

    // Try to broadcast the chat message across the channel
    // if it fails return an error
    if self.message_broadcast.send(message).is_err() {
        return Err(HrpcError::new_internal_server_error("couldn't broadcast message"));
    }
    
    // Log the message we just got
    tracing::info!("got message: {}", message.content);

    Ok((Empty {}).into_response())
}

Streaming logic is simple. Simply subscribe to the broadcast channel, and then read messages from that channel forever until there's an error:

#[handler]
async fn stream_messages(
    &self,
    _request: Request<()>,
    socket: Socket<Message, Empty>,
) -> ServerResult<()> {
    // Subscribe to the message broadcaster
    let mut message_receiver = self.message_broadcast.subscribe();

    // Poll for received messages...
    while let Ok(message) = message_receiver.recv().await {
        // ...and send them to client.
        socket.send_message(message).await?;
    }

    Ok(())
}

Let's put all of this together in the main function. We'll make a new chat server, where we pass in our implementation of the service. We'll be serving using the Hyper HTTP transport for the server, although this can be swapped out with another transport if needed.

// ...other imports

// Import our CORS middleware
use tower_http::cors::CorsLayer;

// Import the Hyper HTTP transport for hRPC
use hrpc::server::transport::http::Hyper;

// `tokio::main` is a Rust macro that converts an `async fn`
// `main` function into a synchronous `main` function, and enables
// you to use the `tokio` async runtime. The runtime we use is the
// multithreaded runtime, which is what we want.
#[tokio::main]
async fn main() -> Result<(), BoxError> {
    // Initialize the default logging in `tracing-subscriber`
    // which is logging to the terminal
    tracing_subscriber::fmt().init();
    
    // Create our chat service
    let service = ChatServer::new(ChatService::new());

    // Create our transport that we will use to serve our service
    let transport = Hyper::new("127.0.0.1:2289")?;

    // Layer our transport for use with CORS.
    // Since this is specific to HTTP, we use the transport's layer method.
    //
    // Note: A "layer" can simply be thought of as a middleware!
    let transport = transport.layer(CorsLayer::permissive());

    // Serve our service with our transport
    transport.serve(service).await?;

    Ok(())
}

Notice how in the code above, we needed to specify a CORS layer. The next step of the process, of course, is to write a frontend for this.

Frontend (CLI)

If you don't want to use the web client example, you can try the CLI client at hRPC examples repository. Keep in mind that this post doesn't cover writing a CLI client.

To run it, after you git clone the repository linked, navigate to chat/tui-client and run cargo run. Instructions also available in the READMEs in the repository.

Frontend (Vue 3 + Vite + TS)

NOTE: If you don't want to follow along, you can find the full web client example at hRPC examples repository.

The setup is a basic Vite project using the Vue template, with all of the boilerplate demo code removed. Once you have the project made, install the following packages:

npm i @protobuf-ts/runtime @protobuf-ts/runtime-rpc @harmony-dev/transport-hrpc

npm i -D @protobuf-ts/plugin @protobuf-ts/protoc windicss vite-plugin-windicss

In order to get Protobuf generation working, we'll use Buf, a tool specifically built for building protocol buffers. Start by making the following buf.gen.yaml:

version: v1
plugins:
  - name: ts
    out: gen
    opt: generate_dependencies,long_type_string
    path: ./node_modules/@protobuf-ts/plugin/bin/protoc-gen-ts

The config above invokes the code generator we installed, and enables a string representation for longs, and generating code for builtin google types too.

Now, paste the protocol from earlier into protocol/chat.proto in the root of the folder, and run buf generate ./protocol. If you see a gen folder appear, then the code generation worked! ✅

The Implementation

When building the UI, it's useful to have a live preview of our site. Run npm run dev in terminal which will start a new dev server.

The entire implementation will be done in src/App.vue, the main Vue component for the site.

For the business logic, we'll be using the new fancy and shiny Vue 3 script setup syntax. Start by defining it:

<script setup lang="ts">
</script>

Now, inside this block, we first create a chat client by passing our client configuration into the HrpcTransport constructor:

import { ChatClient } from "../gen/chat.client";
import { HrpcTransport } from "@harmony-dev/transport-hrpc";

const client = new ChatClient(
  new HrpcTransport({
    baseUrl: "http://127.0.0.1:2289",
    insecure: true
  })
);

Next, we will define a reactive list of messages, and content of the text input:

const content = ref("");
const msgs = reactive<string[]>([]);

These refs are used in the UI, and these are what we'll ultimately need to use in order to reflect a change.

Now let's add our API logic:

// when the component mounts (page loads)
onMounted(() => {
  // start streaming messages
  client.streamMessages({}).responses.onMessage((msg) => {
    // add the message to the list
    msgs.push(msg.content);
  });
});

// keyboard handler for the input
const onKey = (ev: KeyboardEvent) => {
  if (ev.key !== "Enter") return; // only send a message on enter
  client.sendMessage({
    content: content.value,
  }); // send a message to the server
  content.value = ""; // clear the textbox later
};

Now let's add some layouting and styling, with registered event handlers for the input and a v-for loop to display the messages:

<template>
  <div class="h-100vh w-100vw bg-surface-900 flex flex-col justify-center p-3">
    <div class="flex-1 p-3 flex flex-col gap-2 overflow-auto">
      <p class="p-3 max-w-30ch rounded-md bg-surface-800" v-for="m in msgs" :key="m">{{ m }}</p>
    </div>
    <input
      class="
        p-2
        bg-surface-700
        rounded-md
        focus:outline-none focus:ring-3
        ring-secondary-400
	mt-2
      "
      v-model="content"
      @keydown="send"
    />
  </div>
</template>

If you are unsure what these classes mean, take a look at WindiCSS to learn more.

And with that we complete our chat application!

Other Implementations

While we used Rust for server and TypeScript for client here, hRPC is cross-language. The harmony-development organisation on GitHub has other implementations, most located in the hRPC repo.

Tags: #libre #harmony

kate showing a completion for qml

kate showing another completion for qml

qml-lsp is a simple LSP server for QML. it currently only offers primitive non-context-aware completions, without anything else. it understands:

  • completing component names
  • completing properties defined in surrounding component (not properties from its superclasses)
  • completing enums

note that it doesn't understand project-local QML components, only stuff installed to the system QML dirs with qmlplugindump set up correctly.

regardless, it does a decent job at completing system types, and considering that Qt Creator struggles with some of the plugins that qml-lsp has no problem with, it's pretty usable and an improvement over the nothing found in editors other than Qt Creator.

you can check out the source at https://invent.kde.org/cblack/qew-em-el-el-ess-pee, or fetch a statically linked binary here. plonk the binary in your PATH under the name qml-lsp.

kate LSP config:

	"qml": {
		"command": ["qml-lsp"],
		"highlightingModeRegex": "^QML$"
	}

tags: #libre

the messages view showing a file thumbnail

Tok has seen a handful of improvements this week, one being that file messages now show a thumbnail if available.

Music View

music view

Tok can now show you all songs that have been uploaded to a chat.

Hop To Message

Tok now allows you to hop to a message by tapping on it, no matter how far back in history it is.

Bugfixes:

  • messages with newlines can be sent
  • formatting works again
  • nix files updated
  • the message text field no longer keeps the last formatting applied when you send a message

Obtaining Tok

Tok can be built from source from https://invent.kde.org/network/tok.

There's a Telegram room for Tok available at https://t.me/kdetok, where you can come on and chat about anything Tok related, such as asking questions on using or building Tok.

Contributing

Interested in contributing? Come on by the dev chat and say hello!

Tags: #libre #tok

syntax highlighting being demonstrated in tok

Tok has had many changes since the last time I made one of these blog posts, the biggest one being that code blocks are syntax highlighted!

Tok uses KSyntaxHighlighting, the same syntax highlighting engine that powers Kate, KWrite, and other KDE applications that feature syntax highlighting.

Additionally, messages containing codeblocks are able to grow horizontally in width beyond the usual message size, letting you read horizontally wide code easier.

Emoji Completion

emoji completion

Tok now displays autocompletion for :emojis:, making the process of typing in emojis much more seamless.

Edited Indicator

edited indicator

Tok now indicates when a message has been edited by the sender.

Jump To Start Buttons

jump to start

Tok now has buttons that allow you to hop back to the start of various views, such as the chats list and the messages view.

Improved In-Window Menubar

better in-window menubar, featuring visual changes

Tok's in-window menubar now has various improvements, such as using the colour of the rest of the header area, as well as the right sidebar respecting the menubar's appearance.

Proxies

proxy support

Tok now supports configuring proxies, allowing you to access Telegram in countries that don't want you to access Telegram.

And in true anticonvergent fashion, Tok has a dedicated mobile UI for proxies instead of simply using the desktop UI on mobile or the mobile UI on desktop.

proxies on mobile

Better Notifications

better notifications

Tok notifications now display more information about the message, support more message types, and display the profile picture of the chat you're receiving the notification from.

mentions

Tok now renders mentions as links that display the user's profile when clicked.

Minor UI Improvements

Chat list unread indicators now become pill-shaped whenever they grow horizontally.

The typing indicator in the chat list is now accent-coloured, like the typing indicator in the header.

Translation Fixes

Tok now correctly loads translation files, allowing it to render in non-English languages.

Optimisations

Tok's startup time has been optimised by a few hundred milliseconds.

Bugfixes

Tok no longer resets the scroll position of the chats list whenever chats are moved.

Alt-Up, Alt-Down, and Ctrl-K keyboard shortcuts work again.

Formatting is generally less buggier, and message formats better match how they're supposed to look.

Obtaining Tok

Tok can be built from source from https://invent.kde.org/network/tok.

There's a Telegram room for Tok available at https://t.me/kdetok, where you can come on and chat about anything Tok related, such as asking questions on using or building Tok.

Contributing

Interested in contributing? Come on by the dev chat and say hello!

Tags: #libre

This blog post goes over the set of tools you have to work with when doing QtQuick UIs that need to perform something asynchronously.

Data Reactivity

Probably the oldest tool in the toolbox, as it's foundational to QML's construction, and one you should already know how to use.

BooksModel {
    id: booksModel
}
Button {
    text: `The model has ${booksModel.count} items`
}
Button {
   onClicked: booksModel.refresh()
}

QML is reactive, meaning that when you change a property, dependent bindings update automatically.

When applied to asynchrony, this means that asynchronous events or requests can simply mutate an object's properties on completion, and the UI will change when it does.

This is the most common form of asynchrony in a QtQuick UI, and it should be your first goto when you need to perform something asynchronously and display results in the UI, as it plays nicely with a declarative UI.

This is generally the right pattern to use when you can describe the asynchronous process as “the UI displays the current state of a model/object, which changes in response to events.”

Thennables

The newest tool in the toolbox (at least in QML), Thennables play nicely with other cool and shiny things, such as C++20 Coroutines.

Button {
    text: "Do it"
    onClicked: getNewButtonTextFromTheInternet().then((it) => this.text = it)
}

Implementations of the Thennable pattern can be found in Qt Automotive, as the QIviPendingReply family of classes, and in Croutons, as the Future family of classes. These allow you to make an object in C++ that you can return as a Thennable to QML after scheduling a task that'll fufill or reject the thennable from the C++ side. On the backend, it usually looks like this:

QIviPendingReply<bool> foo;

// runs asynchronously
client->getStatus([foo](bool isOnline) mutable {
    foo.setSuccess(response);
});

return foo;

Or, if you're using Croutons + C++20 Coroutines...

auto response = co_await client->getStatus();

co_return response.isOnline();

This is generally the right pattern to use when you can describe the flow of asynchronous actions as an “and then” chain. For example, “make a request and then display a notification to the user if it succeeds or fails”. If the asynchronous process can take advantage of data reactivity, this is probably not the correct pattern to use.

Task Objects

This is rarely the right pattern, and people that have to use your API will probably hate you if you ship this in a library. Despite that, it's a (thankfully uncommon) thing in the wild, so I'll document it here.

FileDownload {
    id: fileDownloader

    url: "http://placehold.it/350x150"

    onStarted: status.text = "Starting download..."
    onError: status.text = "Download failed"
    onProgressChanged: status.text = `Download ${progress}% done`
    onFinished: status.text = "Download finished"
}
Button {
	text: "Download File"
	onClicked: fileDownloader.start()
}

This essentially consists of using a full-fledged object description in QML to describe a task, and to connect to signals on it. The object needs to be given an id, and that id needs to be used to invoke a .start() method or setting a running property to true.

Task objects have many disadvantages: – trying to describe an imperative process directly with a declarative language, which goes as well as you'd expect – require creating and binding properties for an extra object, which can add up fast, even if the object's task isn't actually triggered by a user action. e.g., 50 download buttons with corresponding FileDownload objects, and only a few of those buttons will ever get pressed. – non-UI matters in an otherwise UI-only language

You can often recognise task objects by distinct “input” properties and “output” signals (less commonly, “output” properties) for a given task.

Instead of using a task object, considering using a Thennable, or better utilising reactive data.

Workers

I've honestly never seen the WorkerScript type get used in an actual program, so I'm not sure what their practical application is. Despite that, these are another option for currency, so I'll describe their behaviour.

A WorkerScript is a wrapper around the JS notion of a WebWorker, which runs in its own thread and receives and sends messages from the main thread. This is the only way to get data in and out of them, so you have to structure it as messages.

Tags: #libre

mobile upload dialogue

Tok now has TWO new completely revamped upload dialogues: one for desktop, and one for mobile. Despite how much of the app is spent using stuff that isn't the upload dialogues, the upload dialogues took a LOT of time and work to get implemented correctly.

The mobile dialogue makes it convenient to browse through your most recent photos, videos, music, and files to share them with your friends.

video upload dialogue on mobile

music upload dialogue on mobile

files upload dialogue on mobile

On desktop, the upload dialogue now offers a preview of your file, and for images, the option to compress it.

photo upload on desktop

video upload on desktop

music upload on desktop

file upload on desktop

Sending State

send state

The sending state of outgoing messages is now displayed with a little icon by the timestamp.

Adjusted Chat List Look

adjusted chat list look

The look of the chat list has been slightly adjusted, to make it look more like other KDE apps. More information is shown, such as the sending status of outgoing messages and the timestamp of the latest message.

Improved Pasting

Tok now has improved pasting capabilities, able to paste from apps that put images on the clipboard directly like Firefox and Spectacle now.

Obtaining Tok

Tok can be built from source from https://invent.kde.org/network/tok.

There's a Telegram room for Tok available at https://t.me/kdetok, where you can come on and chat about anything Tok related, such as asking questions on using or building Tok.

Contributing

Interested in contributing? Come on by the dev chat and say hello!

Tags: #libre #tok