After doing some research in general in owlkettle (https://github.com/can-lehmann/owlkettle) for a "client-server" architecture (see here ) I tried to figure out how to specifically write one for an owlkettle app.
The idea is to have 2 threads:
- A "client" thread running a GTK application built by owlkettle
- A "server" thread that does whatever heavy computing is necessary.
For them to communicate you need 2 channels:
- one for messages from client => server
- one for messages from client <= server
Now the problem becomes that a message from the server does not and can not trigger an update in owlkettle It gets stuck in the channel until owlkettle itself decides to trigger an update (e.g. when clicking a button) during which it reads the message. There is no convenient way or hook to say "New server message arrived, update the UI with the new data".
This is obvious in the below example. When clicking the button it sends a message to the server (via channel 1) which sends a response (via channel 2).
You do not immediately see that update in the UI. only when you click the button, because button-clicks trigger general UI updates on their own.
import owlkettle, owlkettle/[playground, adw]
import std/[options, os]
var counter: int = 1
type ChannelHub = ref object
serverChannel: Channel[string]
clientChannel: Channel[string]
proc sendToServer(hub: ChannelHub, msg: string): bool =
echo "send client => server: ", msg
hub.clientChannel.trySend(msg)
proc sendToClient(hub: ChannelHub, msg: string): bool =
echo "send client <= server: ", msg
hub.serverChannel.trySend(msg)
proc readClientMsg(hub: ChannelHub): Option[string] =
let response: tuple[dataAvailable: bool, msg: string] = hub.clientChannel.tryRecv()
return if response.dataAvailable:
echo "read client => server: ", response.repr
some(response.msg)
else:
none(string)
proc readServerMsg(hub: ChannelHub): Option[string] =
let response: tuple[dataAvailable: bool, msg: string] = hub.serverChannel.tryRecv()
return if response.dataAvailable:
echo "read client <= server: ", response.repr
some(response.msg)
else:
none(string)
proc setupServer(channels: ChannelHub): Thread[ChannelHub] =
proc serverLoop(hub: ChannelHub) =
while true:
let msg = hub.readClientMsg()
if msg.isSome():
discard hub.sendToClient("Received Message " & $counter)
counter.inc
sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
createThread(result, serverLoop, channels)
viewable App:
hub: ChannelHub
backendMsg: string = ""
method view(app: AppState): Widget =
let msg: Option[string] = app.hub.readServerMsg()
if msg.isSome():
app.backendMsg = msg.get()
result = gui:
Window:
defaultSize = (500, 150)
title = "Client Server Example"
Box:
orient = OrientY
margin = 12
spacing = 6
Button {.hAlign: AlignCenter, vAlign: AlignCenter.}:
Label(text = "Click me")
proc clicked() =
discard app.hub.sendToServer("Frontend message!")
Label(text = "Message sent by Backend: ")
Label(text = app.backendMsg)
proc setupClient(channels: ChannelHub): Thread[ChannelHub] =
proc startOwlkettle(hub: ChannelHub) =
adw.brew(gui(App(hub = hub)))
createThread(result, startOwlkettle, channels)
proc main() =
var serverToClientChannel: Channel[string]
var clientToServerChannel: Channel[string]
serverToClientChannel.open()
clientToServerChannel.open()
let hub = ChannelHub(serverChannel: serverToClientChannel, clientChannel: clientToServerChannel)
let client = setupClient(hub)
let server = setupServer(hub)
joinThreads(server, client)
main()
How do I get this to trigger updates in the frontend when the server sends a message?
The solution for this is
g_idle_add_fullproc (or alternativelyg_timeout_add_full). What these do is register a function with GTK that gets called whenever the GTK main thread is idle (or whenever the GTK thread is idle and a timeout expires in the case ofg_timeout_add_full).Both of these are in fact available and wrapped by owlkettle! Just use the
addGlobalTimeoutoraddGlobalIdleTaskprocs (don't forget to add a "sleep" in the global idle task to not fully block the CPU with it).Use this to check if there are any messages in the channel that receives messages from the server. If there are, trigger an update in owlkettle.
You can register that proc on startup in the
afterBuildhook of theAppWidget.That would look like this:
Here the complete example (with also a bit of clean-up). Note though, the owlkettle bits were moved into the main thread, there is no need to have it outside the main thread: