<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: electron</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/electron.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-02-02T19:54:36+00:00</updated><author><name>Simon Willison</name></author><entry><title>Introducing the Codex app</title><link href="https://simonwillison.net/2026/Feb/2/introducing-the-codex-app/#atom-tag" rel="alternate"/><published>2026-02-02T19:54:36+00:00</published><updated>2026-02-02T19:54:36+00:00</updated><id>https://simonwillison.net/2026/Feb/2/introducing-the-codex-app/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://openai.com/index/introducing-the-codex-app/"&gt;Introducing the Codex app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
OpenAI just released a new macOS app for their Codex coding agent. I've had a few days of preview access - it's a solid app that provides a nice UI over the capabilities of the Codex CLI agent and adds some interesting new features, most notably first-class support for &lt;a href="https://developers.openai.com/codex/skills"&gt;Skills&lt;/a&gt;, and &lt;a href="https://developers.openai.com/codex/app/automations"&gt;Automations&lt;/a&gt; for running scheduled tasks.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a macOS desktop application with a dark sidebar and light main content area. Left sidebar shows navigation items &amp;quot;New thread&amp;quot;, &amp;quot;Automations&amp;quot;, &amp;quot;Skills&amp;quot;, and a &amp;quot;Threads&amp;quot; section containing two project folders: &amp;quot;local-codex-scratch&amp;quot; with tasks &amp;quot;Reply to greeting task 2h&amp;quot; and &amp;quot;List Codex.app contents 3h&amp;quot;, and &amp;quot;shot-scraper&amp;quot; with tasks &amp;quot;Validate tests on dependency... 3h&amp;quot;, &amp;quot;Document shot-scraper CLI usage 18h&amp;quot; (highlighted), and &amp;quot;Run this: uv run pytest -x 1d&amp;quot;. Bottom left shows &amp;quot;Personal&amp;quot;. Top bar displays &amp;quot;Document shot-scraper CLI usage shot-scraper&amp;quot; with &amp;quot;Open&amp;quot; and &amp;quot;Commit&amp;quot; dropdown buttons. Main content shows a conversation: user message &amp;quot;Build a skill describing how to use the shot-scraper CLI tool&amp;quot; followed by assistant responses: &amp;quot;I'll use the skill-creator skill, scan the repo for existing shot-scraper docs/commands, then draft a concise skill in this workspace and sanity-check it.&amp;quot; then &amp;quot;I've confirmed the skill-creator scripts exist. Next I'll inspect the CLI entrypoint and docs, then scaffold a skill under skills/ in this repo.&amp;quot; then &amp;quot;I've scaffolded skills/shot-scraper-cli/. I'll now replace the template SKILL.md with a task-focused skill and add a compact CLI reference under references/.&amp;quot; then &amp;quot;I'll run the skill packager/validator next to ensure the new skill structure is valid.&amp;quot; then &amp;quot;Done — I created and packaged the skill. Key outputs: • Skill definition: SKILL.md • Compact reference: cli-reference.md • Packaged skill: shot-scraper-cli.skill&amp;quot;. Bottom shows input field with placeholder &amp;quot;Ask for follow-up changes&amp;quot;, &amp;quot;+ Custom Medium&amp;quot; dropdown, and &amp;quot;Local&amp;quot; and &amp;quot;main&amp;quot; branch indicators." src="https://static.simonwillison.net/static/2026/codex-app.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;The app is built with Electron and Node.js. Automations track their state in a SQLite database - here's what that looks like if you explore it with &lt;code&gt;uvx datasette ~/.codex/sqlite/codex-dev.db&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Database schema documentation on light gray background showing three tables: &amp;quot;automation_runs&amp;quot; (teal underlined link) with italic columns &amp;quot;thread_id, automation_id, status, read_at, thread_title, source_cwd, inbox_title, inbox_summary, created_at, updated_at, archived_user_message, archived_assistant_message, archived_reason&amp;quot; and &amp;quot;1 row&amp;quot;; &amp;quot;automations&amp;quot; (teal underlined link) with italic columns &amp;quot;id, name, prompt, status, next_run_at, last_run_at, cwds, rrule, created_at, updated_at&amp;quot; and &amp;quot;1 row&amp;quot;; &amp;quot;inbox_items&amp;quot; (teal underlined link) with italic columns &amp;quot;id, title, description, thread_id, read_at, created_at&amp;quot; and &amp;quot;0 rows&amp;quot;." src="https://static.simonwillison.net/static/2026/codex-dev-sqlite.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Here’s an interactive copy of that database &lt;a href="https://lite.datasette.io/?url=https%3A%2F%2Fgist.githubusercontent.com%2Fsimonw%2F274c4ecfaf959890011810e6881864fe%2Fraw%2F51fdf25c9426b76e9693ccc0d9254f64ceeef819%2Fcodex-dev.db#/codex-dev"&gt;in Datasette Lite&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The announcement gives us a hint at some usage numbers for Codex overall - the holiday spike is notable:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since the launch of GPT‑5.2-Codex in mid-December, overall Codex usage has doubled, and in the past month, more than a million developers have used Codex.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Automations are currently restricted in that they can only run when your laptop is powered on. OpenAI promise that cloud-based automations are coming soon, which will resolve this limitation.&lt;/p&gt;
&lt;p&gt;They chose Electron so they could target other operating systems in the future, with Windows “&lt;a href="https://news.ycombinator.com/item?id=46859054#46859673"&gt;coming very soon&lt;/a&gt;”. OpenAI’s Alexander Embiricos noted &lt;a href="https://news.ycombinator.com/item?id=46859054#46859693"&gt;on the Hacker News thread&lt;/a&gt; that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;it's taking us some time to get really solid sandboxing working on Windows, where there are fewer OS-level primitives for it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Like Claude Code, Codex is really a general agent harness disguised as a tool for programmers. OpenAI acknowledge that here:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Codex is built on a simple premise: everything is controlled by code. The better an agent is at reasoning about and producing code, the more capable it becomes across all forms of technical and knowledge work. [...] We’ve focused on making Codex the best coding agent, which has also laid the foundation for it to become a strong agent for a broad range of knowledge work tasks that extend beyond writing code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude Code had to &lt;a href="https://simonwillison.net/2026/Jan/12/claude-cowork/"&gt;rebrand to Cowork&lt;/a&gt; to better cover the general knowledge work case. OpenAI can probably get away with keeping the Codex name for both.&lt;/p&gt;
&lt;p&gt;OpenAI have made Codex available to free and &lt;a href="https://simonwillison.net/2026/Jan/16/chatgpt-ads/"&gt;Go&lt;/a&gt; plans for "a limited time" (update: Sam Altman &lt;a href="https://x.com/sama/status/2018437537103269909"&gt;says two months&lt;/a&gt;) during which they are also doubling the rate limits for paying users.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-agents"&gt;ai-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/codex-cli"&gt;codex-cli&lt;/a&gt;&lt;/p&gt;



</summary><category term="sandboxing"/><category term="sqlite"/><category term="ai"/><category term="datasette"/><category term="electron"/><category term="openai"/><category term="generative-ai"/><category term="llms"/><category term="ai-agents"/><category term="coding-agents"/><category term="codex-cli"/></entry><entry><title>pywebview 5</title><link href="https://simonwillison.net/2024/Mar/13/pywebview-5/#atom-tag" rel="alternate"/><published>2024-03-13T14:15:46+00:00</published><updated>2024-03-13T14:15:46+00:00</updated><id>https://simonwillison.net/2024/Mar/13/pywebview-5/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://pywebview.flowrl.com/blog/pywebview5.html"&gt;pywebview 5&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
pywebview is a library for building desktop (and now Android) applications using Python, based on the idea of displaying windows that use the system default browser to display an interface to the user—styled such that the fact they run on HTML, CSS and JavaScript is mostly hidden from the end-user.&lt;/p&gt;

&lt;p&gt;It’s a bit like a much simpler version of Electron. Unlike Electron it doesn’t bundle a full browser engine (Electron bundles Chromium), which reduces the size of the dependency a lot but does mean that cross-browser differences (quite rare these days) do come back into play.&lt;/p&gt;

&lt;p&gt;I tried out their getting started example and it’s very pleasant to use—import webview, create a window and then start the application loop running to display it.&lt;/p&gt;

&lt;p&gt;You can register JavaScript functions that call back to Python, and you can execute JavaScript in a window from your Python code.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=39665828"&gt;Show HN&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="python"/><category term="electron"/></entry><entry><title>Datasette Desktop 0.2.0: The annotated release notes</title><link href="https://simonwillison.net/2021/Sep/13/datasette-desktop-2/#atom-tag" rel="alternate"/><published>2021-09-13T23:30:24+00:00</published><updated>2021-09-13T23:30:24+00:00</updated><id>https://simonwillison.net/2021/Sep/13/datasette-desktop-2/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://datasette.io/desktop"&gt;Datasette Desktop&lt;/a&gt; is a new macOS desktop application version of &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt;, an "open source multi-tool for exploring and publishing data" built on top of SQLite. I released the first version &lt;a href="https://simonwillison.net/2021/Sep/8/datasette-desktop/"&gt;last week&lt;/a&gt; - I've just released version 0.2.0 (and a 0.2.1 bug fix) with a whole bunch of critical improvements.&lt;/p&gt;
&lt;p&gt;You can see the &lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.2.0"&gt;release notes for 0.2.0 here&lt;/a&gt;, but as I've done &lt;a href="https://simonwillison.net/series/datasette-release-notes/"&gt;with Datasette in the past&lt;/a&gt; I've decided to present an annotated version of those release notes providing further background on each of the new features.&lt;/p&gt;
&lt;h4&gt;The plugin directory&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;A new &lt;strong&gt;plugin directory&lt;/strong&gt; for installing new plugins and upgrading or uninstalling existing ones. Open it using the "Plugins -&amp;gt; Install and Manage Plugins..." menu item. &lt;a href="https://github.com/simonw/datasette-app/issues/74"&gt;#74&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2021/plugin-directory.gif" alt="Demo showing installing and upgrading a plugin" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This was the main focus for the release. &lt;a href="https://datasette.io/plugins"&gt;Plugins&lt;/a&gt; are a key component of both Datasette and Datasette Desktop: my goal is for Datasette to provide a robust core for exploring databases, with a wide array of plugins that support any additional kind of visualization, exploration or data manipulation capability that a user might want.&lt;/p&gt;
&lt;p&gt;Datasette Desktop goes as far as bundling &lt;a href="https://simonwillison.net/2021/Sep/8/datasette-desktop/#how-the-app-works"&gt;an entire standalone Python installation&lt;/a&gt; just to ensure that plugins will work correctly, and invisibly sets up a dedicated Python virtual environment for plugins to install into when you first run the application.&lt;/p&gt;
&lt;p&gt;The first version of the app allowed users to install plugins by pasting their name into a text input field. Version 0.2.0 is a whole lot more sophisticated: the single input field has been replaced by a full plugin directory interface that shows installed v.s. available plugins and provides "Install", "Upgrade" and "Uninstall" buttons depending on the state of the plugin.&lt;/p&gt;
&lt;p&gt;When I set out to build this I knew I wanted to hit &lt;a href="https://datasette.io/content/plugins.json?_shape=array"&gt;this JSON API&lt;/a&gt; on &lt;a href="https://datasette.io/"&gt;datasette.io&lt;/a&gt; to fetch the list of plugins, and I knew I wanted a simple searchable index page. The I realized I also wanted faceted search, so I could filter for installed vs not-yet-installed plugins.&lt;/p&gt;
&lt;p&gt;Datasette's built-in table interface already implements faceted search! So I decided to use that, with &lt;a href="https://github.com/simonw/datasette-app-support/tree/0.11.5/datasette_app_support/templates"&gt;some custom templates&lt;/a&gt; to add the install buttons and display the plugins in a more suitable format.&lt;/p&gt;
&lt;p&gt;The first challenge was getting the latest list of plugins into my Datasette instance. I built this into the &lt;code&gt;datasette-app-support&lt;/code&gt; plugin using the &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#startup-datasette"&gt;startup() plugin hook&lt;/a&gt; - every time the server starts up it hits that API and &lt;a href="https://github.com/simonw/datasette-app-support/blob/ee8a05ba1dd55734d2b99c9bd774ebb8e9790d7c/datasette_app_support/__init__.py#L17-L62"&gt;populates an in-memory table&lt;/a&gt; with the returned data.&lt;/p&gt;
&lt;p&gt;The data from the API is then extended with four extra columns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"installed"&lt;/code&gt; is set to "installed" or "not installed" depending on whether the plugin has already been installed by the user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"Installed_version"&lt;/code&gt; is the currently installed version of the plugin&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"upgrade"&lt;/code&gt; is the string "upgrade available" or None - allowing the user to filter for just plugins that can be upgraded&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"default"&lt;/code&gt; is set to 1 if the plugin is a default plugin that came with Datasette&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The data needed to build the plugin table is gathered by these three lines of code:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;plugins&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;httpx&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(
     &lt;span class="pl-s"&gt;"https://datasette.io/content/plugins.json?_shape=array"&lt;/span&gt;
).&lt;span class="pl-en"&gt;json&lt;/span&gt;()
&lt;span class="pl-c"&gt;# Annotate with list of installed plugins&lt;/span&gt;
&lt;span class="pl-s1"&gt;installed_plugins&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; {
    &lt;span class="pl-s1"&gt;plugin&lt;/span&gt;[&lt;span class="pl-s"&gt;"name"&lt;/span&gt;]: &lt;span class="pl-s1"&gt;plugin&lt;/span&gt;[&lt;span class="pl-s"&gt;"version"&lt;/span&gt;]
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;plugin&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; (&lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;.&lt;span class="pl-s1"&gt;client&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"/-/plugins.json"&lt;/span&gt;)).&lt;span class="pl-en"&gt;json&lt;/span&gt;()
}
&lt;span class="pl-s1"&gt;default_plugins&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; (&lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;environ&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"DATASETTE_DEFAULT_PLUGINS"&lt;/span&gt;) &lt;span class="pl-c1"&gt;or&lt;/span&gt; &lt;span class="pl-s"&gt;""&lt;/span&gt;).&lt;span class="pl-en"&gt;split&lt;/span&gt;()&lt;/pre&gt;
&lt;p&gt;The first line fetches the full list of known plugins from the Datasette &lt;a href="https://datasette.io/plugins"&gt;plugin directory&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The second makes an internal API call to the Datasette &lt;code&gt;/-/plugins.json&lt;/code&gt; endpoint using the &lt;a href="https://docs.datasette.io/en/stable/internals.html#datasette-client"&gt;datasette.client mechanism&lt;/a&gt; to discover what plugins are currently installed and their versions.&lt;/p&gt;
&lt;p&gt;The third line loads a space-separated list of default plugins from the &lt;code&gt;DATASETTE_DEFAULT_PLUGINS&lt;/code&gt; environment variable.&lt;/p&gt;
&lt;p&gt;That last one deserves further explanation. Datasette Desktop now ships with some default plugins, and the point of truth for what those are &lt;a href="https://github.com/simonw/datasette-app/blob/0.2.0/main.js#L34-L42"&gt;lives in the Electron app codebase&lt;/a&gt; - because that's where the code responsible for installing them is.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Five plugins are now installed by default: &lt;a href="https://datasette.io/plugins/datasette-vega"&gt;datasette-vega&lt;/a&gt;, &lt;a href="https://datasette.io/plugins/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;, &lt;a href="https://datasette.io/plugins/datasette-pretty-json"&gt;datasette-pretty-json&lt;/a&gt;, &lt;a href="https://datasette.io/plugins/datasette-edit-schema"&gt;datasette-edit-schema&lt;/a&gt; and &lt;a href="https://datasette.io/plugins/datasette-configure-fts"&gt;datasette-configure-fts&lt;/a&gt;. &lt;a href="https://github.com/simonw/datasette-app/issues/81"&gt;#81&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The plugin directory needs to know what these defaults are so it can avoid showing the "uninstall" button for those plugins. Uninstalling them currently makes no sense because Datasette Desktop installs any missing dependencies when the app starts, which would instantly undo the user's uninstall action decision.&lt;/p&gt;
&lt;p&gt;An environment variable felt like the most straight-forward way to expose that list of default plugins to the underlying Datasette server!&lt;/p&gt;
&lt;p&gt;I plan to make default plugins uninstallable in the future but doing so require a mechanism for persisting user preference state which I haven't built yet (see &lt;a href="https://github.com/simonw/datasette-app/issues/101"&gt;issue #101&lt;/a&gt;).&lt;/p&gt;
&lt;h4&gt;A log on the loading screen&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;The application &lt;strong&gt;loading screen&lt;/strong&gt; now shows a log of what is going on. &lt;a href="https://github.com/simonw/datasette-app/issues/70"&gt;#70&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first time you launch the Datasette Desktop application it creates a virtual environment and installs &lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt; and the five default plugins (plus their dependencies) into that environment.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2021/datasette-launch-log.gif" alt="Animated demo of the Datasette Desktop launch screen showing the log scrolling past" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This can take quite a few seconds, during which the original app would show an indeterminate loading indicator.&lt;/p&gt;
&lt;p&gt;Personally I hate loading indicators which don't show the difference between something that's working and something that's eternally hung. Since I can't estimate how long it will take, I decided to pipe the log of what the &lt;code&gt;pip install&lt;/code&gt; command is doing to the loading screen itself.&lt;/p&gt;
&lt;p&gt;For most users this will be meaningless, but hopefully will help communicate "I'm installing extra stuff that I need". Advanced users may find this useful though, especially for bug reporting if something goes wrong.&lt;/p&gt;
&lt;p&gt;Under the hood &lt;a href="https://github.com/simonw/datasette-app/commit/e0c899e422e60b43866551fd776b86a954deb94d"&gt;I implemented this&lt;/a&gt; using a Node.js &lt;a href="https://nodejs.org/api/events.html#events_class_eventemitter"&gt;EventEmitter&lt;/a&gt;. I use the same trick to forward server log output to the "Debug -&amp;gt; Show Sever Log" interface.&lt;/p&gt;
&lt;h4&gt;Example CSV files&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;The welcome screen now invites you to try out the application by opening interesting &lt;strong&gt;example CSV files&lt;/strong&gt;, taking advantage of the new "File -&amp;gt; Open CSV from URL..." feature. &lt;a href="https://github.com/simonw/datasette-app/issues/91"&gt;#91&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Previously Datasette Desktop wouldn't do anything at all until you opened up a CSV or SQLite database, and I have a hunch that unlike me most people don't have good examples of those to hand at all times!&lt;/p&gt;
&lt;p&gt;The new welcome screen offers example CSV files that can be opened directly from the internet. I implemented this using a new API at &lt;a href="https://datasette.io/content/example_csvs"&gt;datasette.io/content/example_csvs&lt;/a&gt; (add &lt;code&gt;.json&lt;/code&gt; for the JSON version) which is loaded by code running on that welcome page.&lt;/p&gt;
&lt;p&gt;I have two examples at the moment, for &lt;a href="https://www.thesquirrelcensus.com/"&gt;the Squirrel Census&lt;/a&gt; and the &lt;a href="https://data.london.gov.uk/dataset/animal-rescue-incidents-attended-by-lfb"&gt;London Fire Brigade's animal rescue data&lt;/a&gt;. I'll be adding more in the future.&lt;/p&gt;
&lt;p&gt;The API itself is a great example of the &lt;a href="https://simonwillison.net/2021/Jul/28/baked-data/"&gt;Baked Data architectural pattern&lt;/a&gt; in action: the data itself is stored in &lt;a href="https://github.com/simonw/datasette.io/blob/main/example_csvs.yml"&gt;this hand-edited YAML file&lt;/a&gt;, which is compiled to SQLite every time the site is deployed.&lt;/p&gt;
&lt;p&gt;To get this feature working I added a new "Open CSV from URL" capability to the app, which is also available in the File menu. Under the hood this works by passing the provided URL to the new &lt;code&gt;/-/open-csv-from-url&lt;/code&gt; API endpoint. The implementation of this &lt;a href="https://github.com/simonw/datasette-app-support/blob/0.11.5/datasette_app_support/utils.py#L52-L88"&gt;was surprisingly fiddly&lt;/a&gt; as I wanted to consume the CSV file using an asynchronous HTTP client - I ended up using an adaption of &lt;a href="https://github.com/mosquito/aiofile/blob/3.5.1/README.rst#async-csv-dict-reader"&gt;some example code&lt;/a&gt; from the &lt;a href="https://github.com/mosquito/aiofile"&gt;aiofile&lt;/a&gt; README.&lt;/p&gt;
&lt;h4&gt;Recently opened files and "Open with Datasette"&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Recently opened&lt;/strong&gt; &lt;code&gt;.db&lt;/code&gt; and &lt;code&gt;.csv&lt;/code&gt; files can now be accessed from the new "File -&amp;gt; Open Recent" menu. Thanks, &lt;a href="https://github.com/mnckapilan"&gt;Kapilan M&lt;/a&gt;! &lt;a href="https://github.com/simonw/datasette-app/issues/54"&gt;#54&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was the project's first external contribution! Kapilan M &lt;a href="https://github.com/simonw/datasette-app/pull/77"&gt;figured out a way&lt;/a&gt; to hook into the macOS "recent files" mechanism from Electron, and I expanded that to cover SQLite database in addition to CSV files.&lt;/p&gt;
&lt;p&gt;When a recent file is selected, Electron fires &lt;a href="https://www.electronjs.org/docs/api/app#event-open-file-macos"&gt;the "open-file" event&lt;/a&gt;. This same event is fired when a file is opened using "Open With -&amp;gt; Datasette" or dragged onto the application's dock.&lt;/p&gt;
&lt;p&gt;This meant I needed to tell the difference between a CSV or a SQLite database file, which I do by &lt;a href="https://github.com/simonw/datasette-app/blob/0.2.0/main.js#L95-L101"&gt;checking if&lt;/a&gt; the first 16 bytes of the file match the SQLite header of &lt;code&gt;SQLite format 3\0&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;.db and .csv files&lt;/strong&gt; can now be opened in Datasette starting from the Finder using "Right Click -&amp;gt; Open With -&amp;gt; Datasette". &lt;a href="https://github.com/simonw/datasette-app/issues/40"&gt;#40&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Registering Datasette as a file handler for &lt;code&gt;.csv&lt;/code&gt; and &lt;code&gt;.db&lt;/code&gt; was not at all obvious. It turned out to involve adding the following to the Electron app's &lt;a href="https://github.com/simonw/datasette-app/blob/0.2.0/package.json#L13-L28"&gt;package.json file&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;build&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: {
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;appId&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;io.datasette.app&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;mac&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: {
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;category&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;public.app-category.developer-tools&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;extendInfo&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: {
        &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;CFBundleDocumentTypes&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: [
          {
            &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;CFBundleTypeExtensions&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: [
              &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;csv&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
              &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;tsv&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
              &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;db&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
            ],
            &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;LSHandlerRank&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Alternate&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
          }
        ]
      }&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;The Debug Menu&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;A new &lt;strong&gt;Debug menu&lt;/strong&gt; can be enabled using Datasette -&amp;gt; About Datasette -&amp;gt; Enable Debug Menu".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The debug menu existed previously in development mode, but with 0.2.0 I decided to expose it to end users. I didn't want to show it to people who weren't ready to see it, so you have to first enable it using a button on the about menu.&lt;/p&gt;
&lt;p&gt;The most interesting option there is "Run Server Manually".&lt;/p&gt;
&lt;p&gt;Most of the time when you are using the app there's a &lt;code&gt;datasette&lt;/code&gt; Python server running under the hood, but it's entirely managed by the Node.js &lt;a href="https://nodejs.org/api/child_process.html"&gt;child_process&lt;/a&gt; module.&lt;/p&gt;
&lt;p&gt;When developing the application (or associated plugins) it can be useful to manually run that server rather than having it managed by the app, so you can see more detailed error messages or even add the &lt;code&gt;--pdb&lt;/code&gt; option to drop into a debugger should something go wrong.&lt;/p&gt;
&lt;p&gt;To run that server, you need the Electron app to kill its own version... and you then need to know things like what port it was running on and which environment variables it was using.&lt;/p&gt;
&lt;p&gt;Here's what you see when you click the "Run Server Manually" debug option:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2021/run-server-manually.png" alt="Run server manually? Clicking OK will terminate the Datasette server used by this app. Copy this command to a terminal to manually run a replacement" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Here's that command in full:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DATASETTE_API_TOKEN="0ebb45444ba4cbcbacdbcbb989bb0cd3aa10773c0dfce73c0115868d0cee2afa" DATASETTE_SECRET="4a8ac89d0d269c31d99059933040b4511869c12dfa699a1429ea29ee3310a850" DATASETTE_DEFAULT_PLUGINS="datasette datasette-app-support datasette-vega datasette-cluster-map datasette-pretty-json datasette-edit-schema datasette-configure-fts datasette-leaflet" /Users/simon/.datasette-app/venv/bin/datasette --port 8002 --version-note xyz-for-datasette-app --setting sql_time_limit_ms 10000 --setting max_returned_rows 2000 --setting facet_time_limit_ms 3000 --setting max_csv_mb 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This is a simulation of the command that the app itself used to launch the server. Pasting that into a terminal will produce an exact copy of the original process - and you can add &lt;code&gt;--pdb&lt;/code&gt; or other options to further customize it.&lt;/p&gt;
&lt;h4&gt;Bonus: Restoring the in-memory database on restart&lt;/h4&gt;
&lt;p&gt;This didn't make it into the formal release notes, but it's a fun bug that I fixed in this release.&lt;/p&gt;
&lt;p&gt;Datasette Desktop defaults to opening CSV files in an in-memory database. You can import them into an on-disk database too, but if you just want to start exploring CSV data in Datasette I decided an in-memory database would be a better starting point.&lt;/p&gt;
&lt;p&gt;There's one problem with this: installing a plugin requires a Datasette server restart, and restarting the server clears the content of that in-memory database, causing any tables created from imported CSVs to disappear. This is confusing!&lt;/p&gt;
&lt;p&gt;You can follow my progress on this in issue &lt;a href="https://github.com/simonw/datasette-app/issues/42"&gt;#42: If you open a CSV and then install a plugin the CSV table vanishes&lt;/a&gt;. I ended up solving it by adding code that dumps the "temporary" in-memory database to a file on disk before a server restart, restarts the server, then copies that disk backup into memory again.&lt;/p&gt;
&lt;p&gt;This works using two custom API endpoints added to the &lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt; plugin:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /-/dump-temporary-to-file&lt;/code&gt; with &lt;code&gt;{"path": "/path/to/backup.db"}&lt;/code&gt; dumps the contents of that in-memory temporary database to the specified file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /-/restore-temporary-from-file&lt;/code&gt; with &lt;code&gt;{"path": "/path/to/backup.db"}&lt;/code&gt; restors the content back again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These APIs are called from the &lt;a href="https://github.com/simonw/datasette-app/blob/0.2.0/main.js#L189-L221"&gt;startOrRestart()&lt;/a&gt; method any time the server restarts, using a file path generated by Electron using the following:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;backupPath&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;join&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s1"&gt;app&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getPath&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"temp"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-s"&gt;`backup-&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;crypto&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;randomBytes&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;8&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;toString&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"hex"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;.db`&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The file is deleted once it has been restored.&lt;/p&gt;
&lt;p&gt;After much experimentation, I &lt;a href="https://github.com/simonw/datasette-app-support/blob/0.11.5/datasette_app_support/__init__.py#L270-L288"&gt;ended up using&lt;/a&gt; the &lt;code&gt;db.backup(other_connection)&lt;/code&gt; method that was added to Python's &lt;code&gt;sqlite3&lt;/code&gt; module in Python 3.7. Since Datasette Desktop bundles its own copy of Python 3.9 I don't have to worry about compatibility with older versions at all.&lt;/p&gt;
&lt;h4&gt;The rest is in the milestone&lt;/h4&gt;
&lt;p&gt;If you want even more detailed notes on what into the release, each new feature is included in the &lt;a href="https://github.com/simonw/datasette-app/milestone/2?closed=1"&gt;0.2.0 milestone&lt;/a&gt;, accompanied by a detailed issue with screenshots (and even a few videos) plus links to the underlying commits.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/releasenotes"&gt;releasenotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="releasenotes"/><category term="datasette"/><category term="annotated-release-notes"/><category term="electron"/><category term="datasette-desktop"/></entry><entry><title>Datasette Desktop - a macOS desktop application for Datasette</title><link href="https://simonwillison.net/2021/Sep/8/datasette-desktop/#atom-tag" rel="alternate"/><published>2021-09-08T19:15:46+00:00</published><updated>2021-09-08T19:15:46+00:00</updated><id>https://simonwillison.net/2021/Sep/8/datasette-desktop/#atom-tag</id><summary type="html">
    &lt;p&gt;I just released &lt;a href="https://datasette.io/desktop"&gt;version 0.1.0&lt;/a&gt; of the new Datasette macOS desktop application, the first version that end-users can easily install. I would very much appreciate your help testing it out!&lt;/p&gt;
&lt;h4&gt;Datasette Desktop&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://datasette.io/static/datasette-desktop.jpg" alt="Datasette Desktop screenshot" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; is "an open source multi-tool for exploring and publishing data". It's a Python web application that lets you explore data held in SQLite databases, plus a growing &lt;a href="https://datasette.io/plugins"&gt;ecosystem of plugins&lt;/a&gt; for visualizing and manipulating those databases.&lt;/p&gt;
&lt;p&gt;Datasette is aimed at data journalists, museum curators, archivists, local governments, scientists, researchers and anyone else who has data that they wish to explore and share with the world.&lt;/p&gt;
&lt;p&gt;There's just one big catch: since it's a Python web application, those users have needed to figure out how to &lt;a href="https://docs.datasette.io/en/stable/installation.html"&gt;install and run&lt;/a&gt; Python software in order to use it. For people who don't live and breath Python and the command-line this turns out to be a substantial barrier to entry!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://datasette.io/desktop"&gt;Datasette Desktop&lt;/a&gt;&lt;/strong&gt; is my latest attempt at addressing this problem. I've packaged up Datasette, SQLite and a full copy of Python such that users can &lt;a href="https://datasette.io/desktop"&gt;download and uncompress a zip file&lt;/a&gt;, drag it into their &lt;code&gt;/Applications&lt;/code&gt; folder and start using Datasette, without needing to know that there's a Python web server running under the hood (or even understand what a Python web server is).&lt;/p&gt;
&lt;p&gt;Please try it out, and send me feedback and suggestions &lt;a href="https://github.com/simonw/datasette-app/discussions/67"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;What the app does&lt;/h4&gt;
&lt;p&gt;This initial release has a small but useful set of features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open an existing SQLite database file and offer all of Datasette's functionality, including the ability to explore tables and to execute arbitrary SQL queries.&lt;/li&gt;
&lt;li&gt;Open a CSV file and offer the Datasette table interface (&lt;a href="https://covid-19.datasettes.com/covid/economist_excess_deaths"&gt;example here&lt;/a&gt;). By default this uses an in-memory database that gets cleared when the app shuts down, or you can...&lt;/li&gt;
&lt;li&gt;Import CSV files into tables in on-disk SQLite databases (including creating a new blank database first).&lt;/li&gt;
&lt;li&gt;By default the application runs a local web server which only accepts connections from your machine... but you can change that in the "File -&amp;gt; Access Control" menu to allow connections from anyone on your network. This includes &lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt; networks too, allowing you to run the application on your home computer and then access it securely from other devices such as your mobile phone anywhere in the world.&lt;/li&gt;
&lt;li&gt;You can install plugins! This is the most exciting aspect of this initial release: it's already in a state where users can customize it and developers can extend it, either with &lt;a href="https://datasette.io/plugins"&gt;Datasette's existing plugins&lt;/a&gt; (69 and counting) or by &lt;a href="https://docs.datasette.io/en/stable/writing_plugins.html"&gt;writing new ones&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="how-the-app-works"&gt;How the app works&lt;/h4&gt;
&lt;p&gt;There are three components to the app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A macOS wrapper application&lt;/li&gt;
&lt;li&gt;Datasette itself&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;datasette-app-support&lt;/code&gt; plugin&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first is the macOS application itself. This is currently written with &lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt;, and bundles a full copy of Python 3.9 (based on &lt;a href="https://github.com/indygreg/python-build-standalone"&gt;python-build-standalone&lt;/a&gt; by Gregory Szorc). Bundling Python is essential: the principal goal of the app is to allow people to use Datasette who aren't ready to figure out how to install their own Python environment. Having an isolated and self-contained Python is also a great way of avoiding making &lt;a href="https://xkcd.com/1987/"&gt;XKCD 1987&lt;/a&gt; even worse.&lt;/p&gt;
&lt;p&gt;The macOS application doesn't actually include Datasette itself. Instead, on first launch it creates a new Python virtual environment (currently in &lt;code&gt;~/.datasette-app/venv&lt;/code&gt;, feedback on that location welcome) and installs the other two components: &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; and the &lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt; plugin.&lt;/p&gt;
&lt;p&gt;Having a dedicated virtual environment is what enables the "Install Plugin" menu option. When a plugin is installed the macOS application runs &lt;code&gt;pip install name-of-plugin&lt;/code&gt; and then restarts the Datasette server process, causing it to load that new plugin.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt; plugin is designed exclusively to work with this application. It adds API endpoints that the Electron shell can use to trigger specific actions, such as "import from this CSV file" or "attach this SQLite database" - these are generally triggered by macOS application menu items.&lt;/p&gt;
&lt;p&gt;It also adds a custom authentication mechanism. The user of the app should have special permissions: only they should be able to import a CSV file from anywhere on their computer into Datasette. But for the "network share" feature I want other users to be able to access the web application.&lt;/p&gt;
&lt;p&gt;An interesting consequence of installing Datasette on first-run rather than bundling it with the application is that the user will be able to upgrade to future Datasette releases without needing to re-install the application itself.&lt;/p&gt;
&lt;h4&gt;How I built it&lt;/h4&gt;
&lt;p&gt;I've been building this application completely in public over &lt;a href="https://simonwillison.net/2021/Aug/30/datasette-app/"&gt;the past two weeks&lt;/a&gt;, writing up my notes and research in GitHub issues as I went (here's &lt;a href="https://github.com/simonw/datasette-app/milestone/1?closed=1"&gt;the initial release milestone&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I had to figure out a lot of stuff!&lt;/p&gt;
&lt;p&gt;First, &lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt;. Since almost all of the user-facing interface is provided by the existing Datasette web application, Electron was a natural fit: I needed help powering native menus and bundling everything up as an installable application, which Electron handles extremely well.&lt;/p&gt;
&lt;p&gt;I also have ambitions to &lt;a href="https://github.com/simonw/datasette-app/issues/71"&gt;get a Windows version working&lt;/a&gt; in the future, which should share almost all of the same code.&lt;/p&gt;
&lt;p&gt;Electron also has fantastic &lt;a href="https://www.electronjs.org/docs/tutorial/quick-start"&gt;initial developer onboarding&lt;/a&gt;. I'd love to achieve a similar level of quality for Datasette some day.&lt;/p&gt;
&lt;p&gt;The single biggest challenge was figuring out how to bundle a working copy of the Datasette Python application to run inside the Electron application.&lt;/p&gt;
&lt;p&gt;My initial plan (touched on &lt;a href="https://simonwillison.net/2021/Aug/30/datasette-app/"&gt;last week&lt;/a&gt;) was to compile Datasette and its dependencies into a single executable using &lt;a href="https://www.pyinstaller.org/"&gt;PyInstaller&lt;/a&gt; or &lt;a href="https://pyoxidizer.readthedocs.io/"&gt;PyOxidizer&lt;/a&gt; or &lt;a href="https://py2app.readthedocs.io/"&gt;py2app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;These tools strip down a Python application to the minimal required set of dependencies and then use various tricks to compress that all into a single binary. They are &lt;em&gt;really clever&lt;/em&gt;. For many projects I imagine this would be the right way to go.&lt;/p&gt;
&lt;p&gt;I had one big problem though: I wanted to &lt;a href="https://github.com/simonw/datasette-app/issues/5"&gt;support plugin installation&lt;/a&gt;. Datasette plugins can have their own dependencies, and could potentially use any of the code from the Python standard library. This means that a stripped-down Python isn't actually right for this project: I need a full installation, standard library and all.&lt;/p&gt;
&lt;p&gt;Telling the user they had to install Python themselves was an absolute non-starter: the entire point of this project is to make Datasette available to users who are unwilling or unable to jump through those hoops.&lt;/p&gt;
&lt;p&gt;Gregory Szorc built PyOxidizer, and as part of that he built &lt;a href="https://python-build-standalone.readthedocs.io/"&gt;python-build-standalone&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This project produces self-contained, highly-portable Python distributions. These Python distributions contain a fully-usable, full-featured Python installation as well as their build artifacts (object files, libraries, etc).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sounds like exactly what I needed! I &lt;a href="https://github.com/simonw/datasette-app/issues/22"&gt;opened a research issue&lt;/a&gt;, built a proof-of-concept and decided to commit to that as the approach I was going to use. Here's a TIL that describes how I'm doing this: &lt;a href="https://til.simonwillison.net/electron/python-inside-electron"&gt;Bundling Python inside an Electron app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;(I find GitHub issue threads to be the ideal way of exploring these kinds of areas. Many of my repositories have a &lt;a href="https://github.com/simonw/datasette-app/labels/research"&gt;research label&lt;/a&gt; specifically to track them.)&lt;/p&gt;
&lt;p&gt;The last key step was figuring out how to sign the application, so I could distribute it to other macOS users without them facing this dreaded dialog:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Datasette.app can't be opened because Apple cannot check it for malicious software" src="https://static.simonwillison.net/static/2021/malicious-software.png" style="max-width:100%; width: 438px" /&gt;&lt;/p&gt;
&lt;p&gt;It turns out there are two steps to this these days: signing the code with a developer certificate, and then "notarizing" it, which involves uploading the bundle to Apple's servers, having them scan it for malicious code and attaching the resulting approval to the bundle.&lt;/p&gt;
&lt;p&gt;I was expecting figuring this out to be a nightmare. It ended up not too bad: I spent two days on it, but most of the work ended up being done by &lt;a href="https://www.electron.build/"&gt;electron-builder&lt;/a&gt; - one of the biggest advantages of working within the Electron ecosystem is that a lot of people have put a lot of effort into these final steps.&lt;/p&gt;
&lt;p&gt;I was adamant that my eventual signing and notarization solution should be automated using GitHub Actions: nothing defangs a frustrating build process more than good
automation! This made things a bit harder because all of the tutorials and documentation assumed you were working with a GUI, but I &lt;a href="https://github.com/simonw/datasette-app/blob/main/.github/workflows/release.yml"&gt;got there in the end&lt;/a&gt;. I wrote this all up as a TIL: &lt;a href="https://til.simonwillison.net/electron/sign-notarize-electron-macos"&gt;Signing and notarizing an Electron app for distribution using GitHub Actions&lt;/a&gt; (see also &lt;a href="https://til.simonwillison.net/github-actions/attach-generated-file-to-release"&gt;Attaching a generated file to a GitHub release using Actions&lt;/a&gt;).&lt;/p&gt;
&lt;h4&gt;What's next&lt;/h4&gt;
&lt;p&gt;I announced the release &lt;a href="https://twitter.com/simonw/status/1435457848050257925"&gt;last night on Twitter&lt;/a&gt; and I've already started getting feedback. This has resulted in a growing number of issues under the &lt;a href="https://github.com/simonw/datasette-app/issues?q=is%3Aissue+is%3Aopen+label%3Ausability"&gt;usability label&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My expectation is that most improvements made for the benefit of Datasette Desktop will benefit the regular Datasette web application too.&lt;/p&gt;
&lt;p&gt;There's also a strategic component to this. I'm investing a lot of development work in Datasette, and I want that work to have the biggest impact possible. Datasette Desktop is an important new distribution channel, which also means that any time I add a new feature to Datasette or build a new plugin the desktop application should see the same benefit as the hosted web application.&lt;/p&gt;
&lt;p&gt;If I'm unlucky I'll find this slows me down: every feature I build will need to include consideration as to how it affects the desktop application.&lt;/p&gt;
&lt;p&gt;My intuition currently is that this trade-off will be worthwhile: I don't think ensuring desktop compatibility will be a significant burden, and the added value from getting new features almost for free through a whole separate distribution channel should hopefully be huge!&lt;/p&gt;

&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/purpleair/purple-air-aqi"&gt;Calculating the AQI based on the Purple Air API for a sensor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/electron-debugger-console"&gt;Using the Chrome DevTools console as a REPL for an Electron app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/electron-external-links-system-browser"&gt;Open external links in an Electron app using the system browser&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/github-actions/attach-generated-file-to-release"&gt;Attaching a generated file to a GitHub release using Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/sign-notarize-electron-macos"&gt;Signing and notarizing an Electron app for distribution using GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/python-inside-electron"&gt;Bundling Python inside an Electron app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-import-table"&gt;datasette-import-table&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-import-table/releases/tag/0.3"&gt;0.3&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-import-table/releases"&gt;6 releases total&lt;/a&gt;) - 2021-09-08
&lt;br /&gt;Datasette plugin for importing tables from other Datasette instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app"&gt;datasette-app&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.1.0"&gt;Datasette Desktop 0.1.0&lt;/a&gt; - 2021-09-08
&lt;br /&gt;Electron app wrapping Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-app-support/releases/tag/0.6"&gt;0.6&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-app-support/releases"&gt;8 releases total&lt;/a&gt;) - 2021-09-07
&lt;br /&gt;Part of &lt;a href="https://github.com/simonw/datasette-app"&gt;https://github.com/simonw/datasette-app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/pids"&gt;pids&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/pids/releases/tag/0.1.2"&gt;0.1.2&lt;/a&gt; - 2021-09-07
&lt;br /&gt;A tiny Python library for generating public IDs from integers&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="datasette"/><category term="weeknotes"/><category term="github-actions"/><category term="electron"/><category term="datasette-desktop"/></entry><entry><title>Datasette Desktop 0.1.0</title><link href="https://simonwillison.net/2021/Sep/8/datasette-desktop-release/#atom-tag" rel="alternate"/><published>2021-09-08T05:14:32+00:00</published><updated>2021-09-08T05:14:32+00:00</updated><id>https://simonwillison.net/2021/Sep/8/datasette-desktop-release/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.1.0"&gt;Datasette Desktop 0.1.0&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is the first installable version of the new Datasette Desktop macOS application I’ve been building. Please try it out and leave feedback on Twitter or on the GitHub Discussions thread linked from the release notes.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1435457848050257925"&gt;@simonw&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="datasette"/><category term="electron"/><category term="datasette-desktop"/></entry><entry><title>Building a desktop application for Datasette (and weeknotes)</title><link href="https://simonwillison.net/2021/Aug/30/datasette-app/#atom-tag" rel="alternate"/><published>2021-08-30T05:13:54+00:00</published><updated>2021-08-30T05:13:54+00:00</updated><id>https://simonwillison.net/2021/Aug/30/datasette-app/#atom-tag</id><summary type="html">
    &lt;p&gt;This week I started experimenting with a desktop application version of &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; - with the goal of providing people who aren't comfortable with the command-line the ability to get Datasette up and running on their own personal computers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Update 8th September 2021&lt;/strong&gt;: I made a bunch more progress over the week following this post, see &lt;a href="https://simonwillison.net/2021/Sep/8/datasette-desktop/"&gt;Datasette Desktop—a macOS desktop application for Datasette&lt;/a&gt; for details or &lt;a href="https://datasette.io/desktop"&gt;download the app&lt;/a&gt; to try it out.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2021/datasette-app.png" style="max-width: 100%" alt="Screenshot of the new Datasette desktop app prototype with several open windows" /&gt;&lt;/p&gt;
&lt;h4&gt;Why a desktop application?&lt;/h4&gt;
&lt;p&gt;On Monday I &lt;a href="https://twitter.com/simonw/status/1429931719587598346"&gt;kicked off an enormous Twitter conversation&lt;/a&gt; when I posted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I wonder how much of the popularity of R among some communities in comparison to Python comes down to the fact that with R you can install the RStudio desktop application and you're ready to go&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This ties into my single biggest complaint about Python: it's just too hard for people to get started with. Setting up a Python development environment for the first time remains an enormous barrier to entry.&lt;/p&gt;
&lt;p&gt;I later put this &lt;a href="https://twitter.com/simonw/status/1429937655450505248"&gt;in stronger terms&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The more I think about this the more frustrated I get, thinking about the enormous amount of human potential that's squandered because the barriers to getting started learning to program are so much higher than they need to be&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Which made me think of &lt;a href="https://en.wikipedia.org/wiki/Those_who_live_in_glass_houses_should_not_throw_stones"&gt;glass houses&lt;/a&gt;. My own &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; project has exactly the same problem: to run it locally you need to install Python &lt;em&gt;and then&lt;/em&gt; install Datasette! Mac users &lt;a href="https://docs.datasette.io/en/stable/installation.html"&gt;can use Homebrew&lt;/a&gt;, but telling newcomers to install Homebrew first isn't particularly welcoming either.&lt;/p&gt;
&lt;p&gt;Ideally, I'd like people to be able to install a regular desktop application and start using Datasette that way, without even needing to know that it's written in Python.&lt;/p&gt;
&lt;p&gt;There's been &lt;a href="https://github.com/simonw/datasette/issues/93"&gt;an open issue&lt;/a&gt; to get Datasette running as a standalone binary using PyInstaller since November 2017, with quite a bit of research.&lt;/p&gt;
&lt;p&gt;But I want a UI as well: I don't want to have to teach new users how to install and run a command-line application if I can avoid it.&lt;/p&gt;
&lt;p&gt;So I decided to spend some time researching &lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt; to see how hard it would be to make a basic Datasette desktop application a reality.&lt;/p&gt;
&lt;h4&gt;Progress so far&lt;/h4&gt;
&lt;p&gt;The code I've written so far can be found in the &lt;a href="https://github.com/simonw/datasette.app"&gt;simonw/datasette.app&lt;/a&gt; repository on GitHub. The app so far does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run a &lt;code&gt;datasette&lt;/code&gt; server on localhost attached to an available port (found using &lt;a href="https://www.npmjs.com/package/portfinder"&gt;portfinder&lt;/a&gt;) which terminates when the app quits.&lt;/li&gt;
&lt;li&gt;Open a desktop window showing that Datasette instance once the server has started.&lt;/li&gt;
&lt;li&gt;Allow additional windows onto the same instance to be opened using the "New Window" menu option or the Command+N keyboard shortcut.&lt;/li&gt;
&lt;li&gt;Provides an "Open Database..." menu option (and Command+O shortcut) which brings up a file picker to allow the user to select a SQLite database file to open - once selected, this is attached to the Datasette instance and any windows showing the Datasette homepage are reloaded.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's &lt;a href="https://www.youtube.com/watch?v=n90Cg_9j9XE"&gt;a video demo&lt;/a&gt; showing these features in action:&lt;/p&gt;
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="allowfullscreen" frameborder="0" height="315" src="https://www.youtube-nocookie.com/embed/n90Cg_9j9XE" style="max-width: 100%" title="YouTube video player" width="560"&gt; &lt;/iframe&gt;
&lt;p&gt;It's very much an MVP, but I'm encouraged by the progress so far. I think this is enough of a proof of concept to be worth turning this into an actual usable product.&lt;/p&gt;
&lt;h4&gt;How this all works&lt;/h4&gt;
&lt;p&gt;There are two components to the application.&lt;/p&gt;
&lt;p&gt;The first is a thin Electron shell, responsible for launching the Python server, managing windows and configuring the various desktop menu options used to configure it. The code for that lives in &lt;a href="https://github.com/simonw/datasette.app/blob/4c968ec5b2b845c644c88425f2d43821cc63c4ff/main.js"&gt;main.js&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second is a custom Datasette plugin that adds extra functionality needed by the application. Currently this consists of a tiny bit of &lt;a href="https://github.com/simonw/datasette.app/blob/4c968ec5b2b845c644c88425f2d43821cc63c4ff/datasette-app-support/datasette_app_support/static/sticky-footer.css"&gt;extra CSS&lt;/a&gt; to make the footer stick to the bottom of the window, and a &lt;a href="https://github.com/simonw/datasette.app/blob/4c968ec5b2b845c644c88425f2d43821cc63c4ff/datasette-app-support/datasette_app_support/__init__.py#L15-L56"&gt;custom API endpoint&lt;/a&gt; at &lt;code&gt;/-/open-database-file&lt;/code&gt; which is called by the menu option for opening a new database.&lt;/p&gt;
&lt;h4&gt;Initial impressions of Electron&lt;/h4&gt;
&lt;p&gt;I know it's cool to knock Electron, but in this case it feels like exactly the right tool for the job. Datasette is already a web application - what I need is a way to hide the configuration of that web application behind an icon, and re-present the interface in a way that feels more like a desktop application.&lt;/p&gt;
&lt;p&gt;This is my first time building anything with Electron - here are some of my initial impressions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The initial getting started workflow is really good. I started out with their &lt;a href="https://www.electronjs.org/docs/tutorial/quick-start"&gt;Quick Start&lt;/a&gt; and was up and running with a barebones application that I could start making changes to in just a few minutes.&lt;/li&gt;
&lt;li&gt;The documentation is pretty good, but it leans more towards being an API reference. I found myself googling for examples of different things I wanted to do pretty often.&lt;/li&gt;
&lt;li&gt;The automated testing situation isn't great. I'm using &lt;a href="https://www.electronjs.org/spectron"&gt;Spectron&lt;/a&gt; and &lt;a href="https://mochajs.org/"&gt;Mocha&lt;/a&gt; for &lt;a href="https://github.com/simonw/datasette.app/blob/4c968ec5b2b845c644c88425f2d43821cc63c4ff/test/spec.js"&gt;my initial&lt;/a&gt; (very thin) tests - I got them up and running in GitHub Actions, but I've already run into some limitations:
&lt;ul&gt;
&lt;li&gt;For some reason each time I run the tests an Electron window (and &lt;code&gt;datasette&lt;/code&gt; Python process) is left running. I can't figure out why this is.&lt;/li&gt;
&lt;li&gt;There doesn't appear to be a way for tests to trigger menu items, which is frustrating because most of the logic I've written so far deals with menu items! There is an &lt;a href="https://github.com/electron-userland/spectron/issues/21"&gt;open issue for this&lt;/a&gt; dating back to May 2016.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I haven't yet managed to package my app. This is clearly going to be the biggest challenge.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Up next: packaging the app&lt;/h4&gt;
&lt;p&gt;I was hoping to get to this before writing up my progress in these weeknotes, but it looks like it's going to be quite a challenge.&lt;/p&gt;
&lt;p&gt;In order to produce an installable macOS app (I'll dive into Windows later) I need to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build a standalone Datasette executable, complete with the custom plugin, using PyInstaller&lt;/li&gt;
&lt;li&gt;Sign that binary with an Apple developer certificate&lt;/li&gt;
&lt;li&gt;Build an Electron application that bundles a copy of that &lt;code&gt;datasette&lt;/code&gt; binary&lt;/li&gt;
&lt;li&gt;Sign the resulting Electron application&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm expecting figuring this out to be a long-winded and frustrating experience, which is more the fault of Apple than of Electron. I'm tracking my progress on this in &lt;a href="https://github.com/simonw/datasette.app/issues/7"&gt;issue #7&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Datasette 0.59a2&lt;/h4&gt;
&lt;p&gt;I pushed out &lt;a href="https://github.com/simonw/datasette/releases/tag/0.59a2"&gt;a new alpha&lt;/a&gt; of Datasette earlier this week, partly driven by work I was doing on &lt;code&gt;datasette.app&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The biggest new feature in this release is a new plugin hook: &lt;a href="https://docs.datasette.io/en/latest/plugin_hooks.html#plugin-hook-register-commands"&gt;register_commands()&lt;/a&gt; - which lets plugins add additional commands to Datasette, e.g. &lt;code&gt;datasette verify name-of-file.db&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I released a new plugin that exercises this hook called &lt;a href="https://datasette.io/plugins/datasette-verify"&gt;datasette-verify&lt;/a&gt;. Past experience has shown me that it's crucial to ship an example plugin alongside a new hook, to help confirm that the hook design is fit for purpose.&lt;/p&gt;
&lt;p&gt;It turns out I didn't need this for &lt;code&gt;datasette.app&lt;/code&gt; after all, but it's still a great capability to have!&lt;/p&gt;
&lt;h4&gt;sqlite-utils 3.17&lt;/h4&gt;
&lt;p&gt;Quoting the &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-17"&gt;release notes&lt;/a&gt; in full:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-memory"&gt;sqlite-utils memory&lt;/a&gt; command has a new &lt;code&gt;--analyze&lt;/code&gt; option, which runs the equivalent of the &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-analyze-tables"&gt;analyze-tables&lt;/a&gt; command directly against the in-memory database created from the incoming CSV or JSON data. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/320"&gt;#320&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-insert-files"&gt;sqlite-utils insert-files&lt;/a&gt; now has the ability to insert file contents in to &lt;code&gt;TEXT&lt;/code&gt; columns in addition to the default &lt;code&gt;BLOB&lt;/code&gt;. Pass the &lt;code&gt;--text&lt;/code&gt; option or use &lt;code&gt;content_text&lt;/code&gt; as a column specifier. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/319"&gt;#319&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;evernote-to-sqlite 0.3.2&lt;/h4&gt;
&lt;p&gt;As a follow-up to &lt;a href="https://simonwillison.net/2021/Aug/22/weeknotes-dogsheep/"&gt;last week's work&lt;/a&gt; on my personal Dogsheep, I decided to re-import my Evernote notes... and found out that Evernote has changed their export mechanism in ways that broke my tool. Most concerningly their exported XML is &lt;a href="https://github.com/dogsheep/evernote-to-sqlite/issues/13"&gt;even less well-formed than it used to be&lt;/a&gt;. This &lt;a href="https://github.com/dogsheep/evernote-to-sqlite/releases/tag/0.3.2"&gt;new release&lt;/a&gt; works around that.&lt;/p&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/datasette/search-all-columns-trick"&gt;Searching all columns of a table in Datasette&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-verify"&gt;datasette-verify&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-verify/releases/tag/0.1"&gt;0.1&lt;/a&gt; - 2021-08-28
&lt;br /&gt;Verify that files can be opened by Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette/releases/tag/0.59a2"&gt;0.59a2&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette/releases"&gt;97 releases total&lt;/a&gt;) - 2021-08-28
&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/dogsheep/evernote-to-sqlite"&gt;evernote-to-sqlite&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/dogsheep/evernote-to-sqlite/releases/tag/0.3.2"&gt;0.3.2&lt;/a&gt; - (&lt;a href="https://github.com/dogsheep/evernote-to-sqlite/releases"&gt;5 releases total&lt;/a&gt;) - 2021-08-26
&lt;br /&gt;Tools for converting Evernote content to SQLite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.17"&gt;3.17&lt;/a&gt; - (&lt;a href="https://github.com/simonw/sqlite-utils/releases"&gt;86 releases total&lt;/a&gt;) - 2021-08-24
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="datasette"/><category term="weeknotes"/><category term="sqlite-utils"/><category term="electron"/><category term="datasette-desktop"/></entry><entry><title>When a rewrite isn’t: rebuilding Slack on the desktop</title><link href="https://simonwillison.net/2019/Jul/22/when-a-rewrite-isnt-rebuilding-slack-on-the-desktop/#atom-tag" rel="alternate"/><published>2019-07-22T18:30:31+00:00</published><updated>2019-07-22T18:30:31+00:00</updated><id>https://simonwillison.net/2019/Jul/22/when-a-rewrite-isnt-rebuilding-slack-on-the-desktop/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://slack.engineering/rebuilding-slack-on-the-desktop-308d6fe94ae4"&gt;When a rewrite isn’t: rebuilding Slack on the desktop&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Slack appear to have pulled off the almost impossible: finishing a complete, incremental rewrite of their core product. They moved from jQuery to React over the course of two years, constantly shipping new features as they went along. The biggest gain was in rewriting their code to support multiple workspaces, which means desktop client users no longer have to run a separate copy of Electron for every workspace they are signed into.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/jquery"&gt;jquery&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rewrites"&gt;rewrites&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/slack"&gt;slack&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/react"&gt;react&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;&lt;/p&gt;



</summary><category term="jquery"/><category term="rewrites"/><category term="slack"/><category term="react"/><category term="electron"/></entry><entry><title>Monaco Editor</title><link href="https://simonwillison.net/2019/May/21/monaco-editor/#atom-tag" rel="alternate"/><published>2019-05-21T20:47:12+00:00</published><updated>2019-05-21T20:47:12+00:00</updated><id>https://simonwillison.net/2019/May/21/monaco-editor/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://microsoft.github.io/monaco-editor/"&gt;Monaco Editor&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
VS Code is MIT licensed and built on top of Electron. I thought “huh, I wonder if I could run the editor component embedded in a web app”—and it turns out Microsoft have already extracted out the code editor component into an open source JavaScript package called Monaco. Looks very slick, though sadly it’s not supported in mobile browsers.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/editor"&gt;editor&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/microsoft"&gt;microsoft&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vs-code"&gt;vs-code&lt;/a&gt;&lt;/p&gt;



</summary><category term="editor"/><category term="javascript"/><category term="microsoft"/><category term="open-source"/><category term="electron"/><category term="vs-code"/></entry><entry><title>gillyb/sensitive: A native desktop version of the kibana sense plugin</title><link href="https://simonwillison.net/2017/Nov/4/sensitive/#atom-tag" rel="alternate"/><published>2017-11-04T19:35:41+00:00</published><updated>2017-11-04T19:35:41+00:00</updated><id>https://simonwillison.net/2017/Nov/4/sensitive/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/gillyb/sensitive"&gt;gillyb/sensitive: A native desktop version of the kibana sense plugin&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I love using the Sense UI for developing against Elasticsearch, but it’s infuriatingly hard to obtain these days. You can install it as a Kibana plugin but I work with multiple Elasticsearch instances and I don’t want to have to get it installed on all of them. Until recently I was using a Chrome extension for it, but that’s now been disabled as containing malware and removed from the Chrome extension store. I’ve now switched to Sensitive, which packages Sense up as a native OS X application using Electron.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/elasticsearch"&gt;elasticsearch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;&lt;/p&gt;



</summary><category term="elasticsearch"/><category term="electron"/></entry></feed>