{
    "archive_path": "archive/1791237914.181875",
    "base_url": "floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1",
    "basename": "",
    "bookmarked_date": "2026-10-05 22:05",
    "canonical": {
        "archive_org_path": "https://web.archive.org/web/floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1",
        "dom_path": "output.html",
        "favicon_path": "favicon.ico",
        "git_path": "git/",
        "google_favicon_path": "https://www.google.com/s2/favicons?domain=floreal.tech",
        "headers_path": "headers.json",
        "htmltotext_path": "htmltotext.txt",
        "index_path": "index.html",
        "media_path": "media/",
        "mercury_path": "mercury/content.html",
        "pdf_path": "output.pdf",
        "readability_path": "readability/content.html",
        "screenshot_path": "screenshot.png",
        "singlefile_path": "singlefile.html",
        "warc_path": "warc/",
        "wget_path": "floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1/index.html"
    },
    "domain": "floreal.tech",
    "extension": "",
    "hash": "19SDJSA4XES66SZSKQAK",
    "history": {
        "archive_org": [],
        "dom": [],
        "favicon": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--output",
                    "favicon.ico",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://www.google.com/s2/favicons?domain=floreal.tech"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-10-05T22:10:24.652105+00:00",
                "index_texts": null,
                "output": "favicon.ico",
                "pwd": "/data/archive/1791237914.181875",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-05T22:10:24.030407+00:00",
                "status": "succeeded"
            }
        ],
        "git": [],
        "headers": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--head",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-10-05T22:10:25.177401+00:00",
                "index_texts": null,
                "output": "headers.json",
                "pwd": "/data/archive/1791237914.181875",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-05T22:10:24.660778+00:00",
                "status": "succeeded"
            }
        ],
        "htmltotext": [],
        "media": [],
        "mercury": [
            {
                "cmd": [
                    "/app/node_modules/@postlight/parser/cli.js",
                    "https://floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1/"
                ],
                "cmd_version": "1.0.0",
                "end_ts": "2026-10-05T22:10:34.384039+00:00",
                "index_texts": null,
                "output": "mercury",
                "pwd": "/data/archive/1791237914.181875",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-05T22:10:29.745263+00:00",
                "status": "succeeded"
            }
        ],
        "pdf": [],
        "readability": [
            {
                "cmd": [
                    "/app/node_modules/readability-extractor/readability-extractor",
                    "/tmp/tmpa3ft1jf6",
                    "https://floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1/"
                ],
                "cmd_version": "0.0.11",
                "end_ts": "2026-10-05T22:10:29.735450+00:00",
                "index_texts": [
                    "In this series, we are going to build a todo-list application using\nHaskell, GTK 4, and the Adwaita library. Adwaita will provide us with\nmany useful widgets and styles. Let's dive in!\nThis series' intended audience is intermediate Haskellers,\nwith development experience with the language.\nGTK 4, Adwaita\nAdwaita is a library of GTK components that serve as the design language of\nthe GNOME project. In short: Every decision that the GNOME project has made\nin terms of accessibility and style (the Human Interface Guidelines, HIG)\nis encoded in libadwaita.\nLibawaita gives you many features: for instance it lets you create applications\nwith responsive design, which are recolored at runtime when the desktop switches\nbetween light and dark themes.\nHaskell and GTK\nThroughout this series we will use the haskell-gi toolkit,\nwhich auto-generates Haskell bindings from GTK libraries,\nand allows us to get a Haskell interface to GTK that you\ncan still relate to the C API.\n\nTo keep this post readable, the code that you will see will not be complete,\nin order for me to underline the main concepts.\nYou can find the whole project at https://github.com/Floreal-Technologies/adwaita-todo.\n\nYour first window\nTo begin, here is a self-contained example of the structure of a GTK 4 / Adwaita\napplication.\nLet's create the adwaita \"Application\" that will handle\nresource management for us (including Adwaita stylesheets, which are pretty cool):\nmodule Main (main) where\n\nimport GI.Adw qualified as Adw\nimport GI.Gio qualified as Gio\nimport GI.GTK qualified as Gtk\n\nmain :: IO ()\nmain = do\n  -- The `new X [#attribute := value]` syntax creates a Gtk object\n  -- with its properties. The hash syntax is called OverloadedLabels.\n  app <- new Adw.Application [#applicationId := \"tech.floreal.TodoApp\"]\n  -- We connect the \"activate\" signal to the \"activate\" handler.\n  on app #activate (activate app)\n  -- We run the main application loop.\n  Gio.applicationRun app Nothing\n  pure ()\n\nactivate :: Adw.Application -> IO ()\nactivate app = do\n  header <- new Adw.HeaderBar []\n  toolbar <- new Adw.ToolbarView []\n  Adw.toolbarViewAddTopBar toolbar header\n  window <- \n    new Adw.ApplicationWindow\n      [ #application := app\n      , #title := \"Todos\"\n      , #defaultWidth := 480\n      , #defaultHeight := 640\n      , #content := toolbar\n      ]\n  Gtk.windowPresent window\nLo and behold! An empty window that displays \"Todos\" as its title, and its\ndimensions are 480 by 640.\n\nModel-View-Update: The Elm Architecture\nThe Elm Architecture (or TEA) is a pattern\nfor architecting interactive programs. At its core are three concepts:\n\n  Model\n  The state of the application\n  View\n  The way to turn the Model into a user interface (HTML, GTK, etc)\n  Update\n  The way to update your Model based on Messages\n\nAlongside those concepts we can find\n\n  Message\n  An enumeration of all the possible interactions of the user with the application\n  Effects\n  Not only does the Update function return an updated Model,\n      but it returns also a list of actions to be performed on the side, called Effects.\n\nThis approach was broadly popularised by Elm, and lends itself quite well to\ntaming the imperative nature of the GTK toolkit.\nThe Todo App\nNow the time has come to represent our application state and its actions.\nIn the spirit of the Elm Architecture, everything will be modelled as\ndata structures, so that we have absolute visibility on what actions were\ntriggered and what they entail.\nThe model\nnewtype TodoId = TodoId Word\n  deriving stock (Show)\n  deriving newtype (Eq, Ord)\n\ndata Todo = Todo\n  { id :: TodoId\n  , title :: Text\n  , done :: Bool\n  }\n  deriving stock (Eq, Show)\n\ndata Model = Model\n  { todos :: Map TodoId Todo\n  , nextId :: TodoId \n  }\n  deriving stock (Eq, Show)\n\n-- In our case, the effect here represents\n-- saving the todo-list on disk.\ndata Effect = Save [Todo]\n  deriving stock (Eq, Show)\n\ninit :: Model\ninit = Model\n  { todos = Map.empty\n  , nextId = TodoId 0\n  }The messages\nUser interactions with the application are modelled as Messages:\nA known set of actions for which we have clear actions that modify the model.\nLet's start with a couple of messages that the user may trigger to update the model:\ndata Message\n  = Add Text\n  | SetDoneStatus TodoId Bool\nderiving stock (Eq, Ord)\nNot much so far, but we will add more as we go.\nUpdating the model\nupdate :: Message -> Model -> (Model, [Effect])\nupdate message model = case message of\n  Add raw -> \n    let text = Text.strip raw\n        todoId@(TodoId n) = model.nextId\n        todo = Todo{\u00a0id = todoId, title = text, done = False }\n    in if Text.null text\n    then (model, [])\n    else\n      withTodos\n        (Map.insert todo.id todo)\n        model{ nextId = TodoId (n + 1)}\n  SetDoneStatus todoId value ->\n    withTodos (Map.adjust (\\todo -> todo {done = value}) todoId) model\n where\n  -- This is where we determine if our todos have changed,\n  -- so that we can save them.\n  withTodos f changed =\n    let result = changed {todos = f changed.todos}\n    in if result.todos == model.todos\n         then (result, [])\n         else (result, [Save (Map.elems result.todos)])\nLet's now open GHCi and try things out:\n$ cabal repl\n-- Let's add a task to buy leeks\nghci> let (m1, e1) = update (Add \"Buy leeks\") init\n\n-- This triggers a \"Save\" effect for an unfinished task\nghci> e1\n[Save [Todo {id = TodoId 0, title = \"Buy leeks\", done = False}]]\n\n-- Let's set the task as done\nghci> let (m2, e2) = update (SetDoneStatus (TodoId 0) True) m1\n\n-- Since the status has changed, we have to save it\nghci> e2\n[Save [Todo {id = TodoId 0, title = \"Buy leeks\", done = True}]]\n\n-- Setting the task to True again does not trigger a Save effect,\n-- so the effects list is empty.\nghci> update (SetDoneStatus (TodoId 0) True) m2\n(Model{ nextId = TodoId 1\n      , todos = fromList [\n          (TodoId 0, Todo{ id = TodoId 0\n                         , title = \"Buy leeks\"\n                         , done = True})]\n      }, []) -- \u2190 empty list!\nThis is our domain logic, encoded as a sum type of messages and an update function to change the state.\nLet us now design our interface.\nThe View\nIt's always good to write down what your expectations are before starting a\nuser interface.\nFrom experience, design does not immediately follow from data, and so I tend\nless and less to look at the shape of my data to inform my designs.\n\n\n\nIn our case it is pretty simple, but I like to draw.\n\n\nWidgets\nWe are going to make use of several widgets (Click on their name to see a screenshot of what they look like):\n\n   Box \n  \n    A box arranges child widgets in a row or column\n  \n   ListBox \n  \n    A list of rows that can be dynamically filtered and sorted. Useful later to filter on status and sort by age.\n  \n   EntryRow \n  \n    The entry of a row, with a title, a placeholder text, and an icon to show that it is editable.\n    It is a sublass of ListBox, so it will live in a ListBox.\n  \n   ActionRow \n  \n    A more restricted version of the EntryRow, which cannot be edited in-place.\n    It can still get action icons, and lives in a ListBox.\n  \n   Clamp \n  \n    A widget that constrains its child to a given size. Useful to enforce margins so that the background is visible at the edges.\n  \n   ScrolledWindow  \n  \n    This widget makes its child scrollable. Does what it says on the tin.\n  \n   ToolbarView \n  \n    A view widget containing a page, as well as top and bottom bars.\n  \n\nTime 2 Lego\nWith those building blocks at hand, let's write down how our widgets connect with each other:\nmodule Todo.View (view) where\n\n-- Imports\n-- [\u2026]\n\n-- `dispatch` will be defined later in the Runtime module,\n-- and is the function that  turns a message into a change\n-- of state / model.\nview\n  :: (Message -> IO ())\n  -> Model\n  -> IO Gtk.Widget\nview dispatch model = do\n  -- Here, we define our input row, with its title and a signal handler\n  -- to activate the retrieval of our input when it is activated.\n  inputRow <- new Adw.EntryRow [#title := \"New task\"]\n  on entry #entryActivated $ do\n    text <- Gtk.editableGetText inputRow\n    dispatch (Add text) -- This is where we send our \"Add\" message\n\n  -- EntryRow is a subclass of ListBox,\n  -- so we must append our inputRow inside entryBox\n  entryBox <- newBoxedList \n  Gtk.listBoxAppend entryBox inputRow\n\n  -- The ListBox that will contain our tasks\n  todoList <- newBoxedList\n  forM_ (model.todos) $ \\todo -> do\n    row <- new Adw.ActionRow [#title := todo.title, #useMarkup := False]\n    Gtk.listBoxAppend todoList row\n\n  -- The main content widget, with various options\n  -- to define the margins within its parent\n  -- as well as the orientation.\n  content <-\n    new\n      Gtk.Box\n      [ #orientation := Gtk.OrientationVertical\n      , #spacing := 12\n      , #marginTop := 12\n      , #marginBottom := 12\n      , #marginStart := 12\n      , #marginEnd := 12\n      ]\n  -- Let's not forget to append everything\u2026\n  Gtk.boxAppend content entryBox\n  Gtk.boxAppend content todoList\n\n  -- The parents of the content widget.\n  clamp <- new Adw.Clamp [#child := content]\n  scrolled <-\n    new\n      Gtk.ScrolledWindow\n      [ #child := clamp\n      ]\n\n  -- A cute little footer for additional information,\n  -- like the amount of tasks.\n  count <- new Gtk.Label [#label := Text.show (Map.size model.todos)]\n  footer <-\n    new\n      Gtk.Box\n      [ #orientation := Gtk.OrientationHorizontal\n      , #marginTop := 6\n      , #marginBottom := 6\n      , #marginStart := 12\n      , #marginEnd := 12\n      ]\n  Gtk.boxAppend footer count\n\n  -- Now let's assemble the header, content and footer!\n  header <- new Adw.HeaderBar []\n  toolbar <- new Adw.ToolbarView [#content := scrolled]\n  Adw.toolbarViewAddTopBar toolbar header\n  Adw.toolbarViewAddBottomBar toolbar footer\n  Gtk.toWidget toolbar\n\n-- A little helper to create a ListBox with the correct settings.\nnewBoxedList :: IO Gtk.ListBox\nnewBoxedList =\n  new\n    Gtk.ListBox\n    [ #selectionMode := Gtk.SelectionModeNone\n    , #cssClasses := [\"boxed-list\"]\n    ]\n\nWe can't get a lot of visual feedback yet, so you will have to trust that it\nresembles the pencil-and-paper draft from earlier.\nThe Runtime\nSimilarly to the content of the demo from the beginning of the article,\nthis is where we plug the Model-View-Update trio.\nWe will define two more functions: dispatch and step, and they love each other very much.\ndispatch takes a message, handles GTK execution loop priority, and calls step to perform\nthe model update that will lead to the view update.\nstep reads the model, calls update (from Model.hs) on it to get the new\nmodel, write the new model, and pass the new model and the dispatch function to\nthe View. The View gives back the new content as a Gtk widget, and we set this\nnew content in the window.\nHere is the code:\nrun :: Adw.Application -> IO ()\nrun app = do\n  window <-\n    new\n      Adw.ApplicationWindow\n      [ #application := app\n      , #title := \"Todos\"\n      , #defaultWidth := 480\n      , #defaultHeight := 640\n      ]\n  ref <- newIORef Model.init\n  let {- rec -}\n      dispatch :: Model.Message -> IO ()\n      dispatch message =\n        void $ GLib.idleAdd GLib.PRIORITY_DEFAULT $ do\n          step message\n          pure GLib.SOURCE_REMOVE\n\n      step :: Model.Message -> IO ()\n      step message = do\n        oldModel <- readIORef ref\n        let (newModel, _effects) = Model.update message oldModel\n        writeIORef ref newModel\n        content <- View.view dispatch newModel\n        Adw.applicationWindowSetContent window (Just content)\n\n  content <- View.view dispatch Model.init\n  Adw.applicationWindowSetContent window (Just content)\n  Gtk.windowPresent window\nNow, our Main module looks like this:\nmodule Main where\n\nmain :: IO ()\nmain = do\n  app <- new Adw.Application\n     [#applicationId := \"tech.floreal.AdwaitaTodo\"]\n  on app #activate (Runtime.run app)\n  Gio.applicationRun app Nothing\n  pure ()\nAnd heeeere we go:\n\n  \n   Pretty rad. \n\n\nThis concludes this article. See you in Part 2 for more features for our Todo List\napplication!"
                ],
                "output": "readability",
                "pwd": "/data/archive/1791237914.181875",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-05T22:10:27.812206+00:00",
                "status": "succeeded"
            }
        ],
        "screenshot": [],
        "singlefile": [],
        "title": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-10-05T22:10:27.804325+00:00",
                "index_texts": null,
                "output": "Making a GTK application in Haskell, part 1 \u2014 Flor\u00e9al Technologies",
                "pwd": "/data/archive/1791237914.181875",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-05T22:10:27.765810+00:00",
                "status": "succeeded"
            }
        ],
        "wget": [
            {
                "cmd": [
                    "wget",
                    "--no-verbose",
                    "--adjust-extension",
                    "--convert-links",
                    "--force-directories",
                    "--span-hosts",
                    "--no-parent",
                    "-e",
                    "robots=off",
                    "--reject=js,mjs,woff,woff2,ttf,otf,eot,mp4,m4v,mov,webm,ogv,mkv,avi,flv",
                    "--timeout=120",
                    "--restrict-file-names=windows",
                    "--page-requisites",
                    "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) wget/GNU Wget 1.21.3",
                    "--compression=auto",
                    "--timestamping",
                    "https://floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1/"
                ],
                "cmd_version": "GNU Wget 1.21.3",
                "end_ts": "2026-10-05T22:10:27.756017+00:00",
                "index_texts": null,
                "output": "floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1/index.html",
                "pwd": "/data/archive/1791237914.181875",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-05T22:10:25.187164+00:00",
                "status": "succeeded"
            }
        ]
    },
    "icons": null,
    "is_archived": true,
    "is_static": false,
    "latest": {
        "archive_org": null,
        "dom": null,
        "favicon": "favicon.ico",
        "git": null,
        "media": null,
        "pdf": null,
        "screenshot": null,
        "singlefile": null,
        "title": "Making a GTK application in Haskell, part 1 \u2014 Flor\u00e9al Technologies",
        "warc": null,
        "wget": "floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1/index.html"
    },
    "link_dir": "/data/archive/1791237914.181875",
    "newest_archive_date": "2026-10-05T22:10:29.745263+00:00",
    "num_failures": 0,
    "num_outputs": 6,
    "oldest_archive_date": "2026-10-05T22:10:24.030407+00:00",
    "path": "/blog/2026/making-a-gtk-app-in-haskell-part-1/",
    "schema": "Link",
    "scheme": "https",
    "snapshot_id": "6c93be68-32e1-47d4-975f-e397c0affa82",
    "sources": [
        "/data/sources/1791237914-import.txt"
    ],
    "tags": null,
    "tags_str": "",
    "timestamp": "1791237914.181875",
    "title": "Making a GTK application in Haskell, part 1 \u2014 Flor\u00e9al Technologies",
    "updated": "2026-10-05T22:10:24.029832+00:00",
    "updated_date": "2026-10-05 22:10",
    "url": "https://floreal.tech/blog/2026/making-a-gtk-app-in-haskell-part-1/"
}