I did some research and playing around to figure out how a client-server architecture with owlkettle could look like and it works pretty well with using threads and channels (As Araq pretty much suggested if I understood him correctly).
Below the minimal example that I got to work. Basically you have 3 threads: 1. Main Thread (Does nothing, basically asleep the entire time waiting for the other 2) 2. Owlkettle Thread (Renders GUI, listens to channel messages from the "server" thread) 3. Server Thread (Listens to channel messages from the "client" thread and responds accordingly) And 2 channels: 1. Server => Client 2. Client => Server * A button click triggers a signal * String channel message sent to the server via the channel for client => server messages (clientChannel) * The server - listening for messages on the clientChannel - receives the message and sends a response via the channel for server => client messages (serverChannel) * The client checks for new messages on serverChannel in an "idle"-task registered with GTK via `g_idle_add_full`. That task is called by GTK every time the GTK thread has "nothing to do" and thus is perfect for checking for messages * Client sees that a server-message is available and triggers an update of the client on the owlkettle side of things * owlkettle re-renders its widgets and tries to read the message as part of doing so in the `view` method * That updates the AppState, which is then reflected in the rendered application import owlkettle, owlkettle/[widgetutils, adw] import owlkettle/bindings/gtk import std/[options, os] var counter: int = 0 ## Communication 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() result = 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() result = if response.dataAvailable: echo "read client <= server: ", response.repr some(response.msg) else: none(string) proc hasServerMsg(hub: ChannelHub): bool = hub.serverChannel.peek() > 0 ## Server 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) ## Client proc addServerListener(app: Viewable, hub: ChannelHub, priority: int = 200) # Forward declaration viewable App: hub: ChannelHub backendMsg: string = "" hooks: afterBuild: addServerListener(state, state.hub) type ListenerData = object hub: ChannelHub app: Viewable proc addServerListener(app: Viewable, hub: ChannelHub, priority: int = 200) = proc listener(cell: pointer): cbool {.cdecl.} = let data = cast[ptr ListenerData](cell)[] if data.hub.hasServerMsg(): discard data.app.redraw() sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency return true.cbool let data = allocSharedCell(ListenerData(hub: hub, app: app)) discard g_idle_add_full(priority.cint, listener, data, nil) 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) ## Main 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() Run This likely still requires some clean-up, for example I think updating AppState could possibly be pushed into the `listener` proc. But as a first stab at it (And this being my first proper attempt at doing multi-threading myself in nim), that seems reasonable.