Building blocks for little desktop apps that live in the menu bar or the tray
Overview
Building blocks for little companion apps that live in the menu bar, the tray or your pocket: the parts every such app needs and nobody wants to write twice. Swift for macOS and iOS, .NET for Windows, Kotlin for Android. MIT. Deskling grew out of mybackhurts, which ships it, and sunnysays, a desk companion in development whose character became the companion layer. Mister Pagliacci, a clown who tells jokes from the menu bar, is the second companion app built on that layer (in design). It is 0.x until the second app has settled the API, so minor versions may change public types. Deskling is shared as is, for anyone to use, but it is built for these apps: its issues are the owner's own plans, issues and pull requests from others aren't monitored, and releases follow what the apps need. Kotlin, for Android (kotlin/, a Gradle build an app includes from the submodule; tools/README.md): deskling.
README
Deskling
Building blocks for little companion apps that live in the menu bar, the tray or your pocket: the parts every such app needs and nobody wants to write twice. Swift for macOS and iOS, .NET for Windows, Kotlin for Android. MIT.
Deskling grew out of mybackhurts, which ships it, and sunnysays, a
desk companion in development whose character became the companion layer. Mister Pagliacci, a
clown who tells jokes from the menu bar, is the second companion app built on that layer (in design). It is 0.x until
the second app has settled the API, so minor versions may change public types.
No support. Deskling is shared as is, for anyone to use, but it is built for these apps: its issues are the owner’s own plans, issues and pull requests from others aren’t monitored, and releases follow what the apps need.
What’s inside
| Swift product | .NET package | What it does |
|---|---|---|
DesklingCore |
Deskling.Core |
Pure logic. ReminderScheduler: an interval timer with active hours and weekdays, quiet hours, an idle reset (“you were away, the timer restarts”), a hold while the user is busy with a grace period after, snooze, “after my next call” and pause. Clock, IdleTimeProvider and BusyStateProvider protocols so it runs on an injected clock. A seeded SplitMix64 random source for deterministic jitter. No OS or UI dependencies, so it builds anywhere Swift, .NET or Kotlin runs. |
DesklingCompanion |
Deskling.Companion |
The pure core of a desk companion: a phrase bank and picker (lines, jokes told in beats, tunes; scoped by skin, flavor and language), chatter cadence presets, quiet rules and mood, write-through settings an app subclasses, a coordinator that replays a whole day from a seed, desktop presence, skins, and a rig that animates vector or raster character art. The app injects its lines, defaults, skins and palette; the .NET side loads them from the JSON the Swift side writes (PhraseBank.Parse, CharacterArt.Parse). |
DesklingSystem |
Deskling.Windows |
The real providers. Idle time from the last input event. Call detection without permissions: whether another app is using the camera or the microphone, read from device state (CoreAudio’s process list on macOS, the capability consent store on Windows), never by opening a device. Full-screen detection. Power source (battery, charging, level). Output muted and route. Screen locked. Appearance changes. |
DesklingShell |
Deskling.Windows |
Menu bar / tray app plumbing. A status item with a popover panel, a right-click menu and an animated glyph the app draws. Speech: a speaker (the system voice or a recorded clip), a sequencer that tells a joke in beats, a library of recorded voices that knows which clips are current, a chime player. A window manager that shows the Dock icon only while a window is open, a login item toggle, “was I launched at login?”, a notification poster with categories and actions, a floating desktop window (macOS), a raw Shell_NotifyIcon tray icon host with a native menu, single-instance startup, a .resw lookup, drawing helpers (SVG path sampling, keyframes, a hand-drawn wobble). |
DesklingCompanionUI |
Deskling.Companion.Shell + Deskling.Windows.Companion |
macOS: the companion on screen, over DesklingCompanion. SignalHub (the snapshot and the events from the system signals), the menu bar glyph, CharacterView (vector and image parts, the wake spring), DesktopCompanion (the character and its speech bubble on the desktop), DeliveryPerformer (bubble, blip, speech in beats, the notification), CompanionNotifier, a CompanionPalette the app sets, the speech bubble and status pill, and ScreenshotCapture for screenshot runs. Since 0.7 also the companion shell an app subclasses: CompanionAppModel (the tick, the channels, the status item, the desktop, the windows, previews, mute and undo, the status line), its delegate and menus, the Settings rows and editors, the Support window, the window chrome and a ScreenshotScript; each takes its words from the app. Since 0.8 the same shell on Windows, in two packages: Deskling.Companion.Shell (net10.0, tested on any OS) is the logic against small interfaces (the app model, the menus as data, speech in beats and recorded voices, the delivery performer, the signal hub, the desktop layout, a screenshot run’s sample environment), and Deskling.Windows.Companion (WinUI 3) implements them: the app’s Application, the tray status item with its glyph and panel, the windows, the speaker, chime and notifier, the character on the desktop, and the character canvas, speech bubble, status pill, chrome, Settings rows, Support window and screenshot capture. |
DesklingStore |
Deskling.Core (Store/) + Deskling.Windows (StoreContextBackend) |
A purchase store for one-time unlockables and tips (StoreKit 2 on the Mac, the Microsoft Store on Windows), with the backend behind a protocol so tests use a mock. |
DesklingTesting |
Deskling.Testing |
Fakes for every protocol, fixtures (a phrase bank, defaults, a vector and a raster skin) and the shared conformance vectors’ helpers for the apps’ own test targets: Swift generates an app’s companion vectors, .NET replays them (each check returns its problems, so neither side needs a particular test framework). The standard picker and quiet case lists, CompanionVectors and ConformanceSuite.companion build an app’s whole suite; CompanionReplay replays it. On .NET also the companion shell’s fakes (FakeHost, ManualTimers, FakeSpeaker…), for testing an app model without a screen. |
tools/ |
tools/ |
Not a package: Python tools, shell scripts and agent skills for companion apps, which vendor this repo as a deskling/ git submodule pinned to the release. Localization, phrase sync, recorded voices, character art compiling, sounds, fonts, screenshots, verification (including a check that every Deskling pin agrees), the store listing tools (field limits, App Store Connect, Partner Center) and agent hooks, all driven by the app’s companion.json; and @deskling/landing, the landing page’s plumbing as Vite plugins, published to npm. See tools/README.md. |
Kotlin, for Android (kotlin/, a Gradle build an app includes from the submodule; tools/README.md): deskling.core
mirrors DesklingCore (the scheduler, the random source, plurals, settings, the store, the drawing math),
deskling.android is the phone-side plumbing (Play Billing, alarms, notifications, reduce motion, the screenshot run)
and deskling.testing the fakes. No companion in Kotlin yet.
Everything in DesklingSystem reads state and never prompts. None of it talks to the network.
Install
Swift (macOS 14+), in Package.swift or Xcode’s package list, pinned to a tag:
.package(url: "https://github.com/vmihalachi/deskling", exact: "0.12.4")
.NET (10), from NuGet.org. Deskling.Windows and Deskling.Windows.Companion need Windows 11: they target
net10.0-windows10.0.22000.0 with TargetPlatformMinVersion 10.0.22000.0, so an app targets that or later (the rest
are net10.0 and run anywhere):
dotnet add package Deskling.Core
dotnet add package Deskling.Companion # the desk companion layer
dotnet add package Deskling.Windows # Windows 11; the tray, idle and busy services
dotnet add package Deskling.Companion.Shell # a companion app's shell, platform-free (0.8)
dotnet add package Deskling.Windows.Companion # Windows 11, WinUI 3: that shell on Windows (0.8)
dotnet add package Deskling.Testing # in test projects: fakes, fixtures, vector replays
Using it
The package ships no strings, product ids or window lists: the app passes them in.
- Scheduling.
ReminderScheduler(config:clock:idle:busy:calendar:random:); calltick()every 20 s or so and act on.remind.SchedulerConfig.intervalJitter(seconds, default 0) randomizes each interval from the injectedRandomSource; passSplitMix64(seed:)to replay a run. - Signals (macOS).
SystemBusyStateProvider(cameraCheck:):.avFoundation(default),.coreMediaIOfor a sandboxed app without the camera entitlement, or.off(the microphone check alone catches nearly every call).SystemPowerSource,SystemAudioOutput,AppearanceWatcherandScreenLockWatcherexpose a current value and anonChangecallback. - Shell (macOS).
WindowManagertakes an enum conforming toWindowID(title, size, placement, exclusivity) and a content provider;NotificationPoster(categories:)(also on iOS) takes action ids with already localized titles and reportsonAction(categoryID, actionID), oronAnswerwith the request and itsuserInfo; for an app the system ends between reminders,replacePending(prefix:with:)schedulesScheduledNotifications ahead;LoginItemandLaunchContext.launchedAsLoginItem()cover start-at-login.FloatingWindow(content:size:anchor:)floats SwiftUI content above other windows on every Space (not full-screen ones) without activating the app: click, drag, right-click menu and pinch callbacks, fades, a window riding along beside it (a speech bubble), andFloatingPlacementfor the math (kept on a screen when displays change, a side facing the middle, a platform-neutral top-left origin). - Store.
PurchaseStore(catalog: ProductCatalog(unlockables:tips:));owned,isOwned(_:),buy,tip,restore;error: PurchaseError?is for the app to put into words. Tests useMockStoreBackend. On Windows the same store (Deskling.Core.Store) runs overStoreContextBackend(dispatcher, windowHandle), which matches add-ons by their Partner Center Product ID and reports consumables fulfilled so tips can be bought again;MockStoreBackendships inDeskling.Corethere, for tests and an app’s preview modes. - A companion’s profile.
CompanionDefaults(skin:phraseFlavors:)states the two product choices (the default skin, the flavors on); the package’s numbers fill the rest. SubclassCompanionSettings(store:defaults:)(an@Observable open classover anySettingsStore) to add the app’s own keys beside the 22 generic ones.CompanionCoordinator(settings:bank:language:clock:idle:busy:calendar:seed:)takes yourPhraseBank; calltick(snapshot:)every 20 s and deliver what it returns (DeliveryPerformeron the Mac). - Skins.
SkinCatalog([SkinEntry(art:unlockedBy:)]): the first entry is the default, and a skin whoseunlockedByproduct isn’t owned (or was refunded) draws the default. Art is vector shapes or raster layers (tools/character.pycompiles both toCharacterArt);CharacterViewdraws image parts fromartBundle, colors vector parts through thecompanionPaletteyou set in the environment. A phrase’sskinslimits who can say it. - Sequences.
Phrase.Kind.sequence(source:)is a joke in beats (two or more parts, catalog keysphrase..1,.2…), spaced byPhraseTiming, spoken bySpeechSequencer; it counts as one phrase for recents and muting, and the notification shows it once, at the punchline. - Languages.
PhraseContexttakes the app’s language: a phrase withlanguagesset (["it"]) is only picked in those, so a joke that only works in Italian is written in Italian. The bank holds source text; the app’s String Catalog (phrase.) supplies the words on screen. - Windows. In
Deskling.Windows:TrayIconHost,AppNotifier,StartupService(taskId),LocalSettingsStore,SingleInstance.Claim(key),WavPlayer, and the signalsPowerSource,OutputMute,AppearanceWatcher,SessionLockWatcher. String lookup isLoc(Loc.Configure("Resources"),Loc.UseLanguage(...)at launch) andFormats.Configure(new FormatKeys(...))names the six catalog keys the formatters need.
A companion app on Windows
On Deskling.Windows.Companion (0.8) a WinUI 3 app is a model, its views and three small files.
dotnet/Fixture.App is the whole of one, over the fixture bank.
// Program.cs: the whole of Main (the csproj defines DISABLE_XAML_GENERATED_MAIN). One copy runs; a second launch
// reopens it. A screenshot run claims no single instance. Unpackaged, the language picked in Settings is kept in the
// app's settings file and set again here (packaged, Windows keeps it and ForApp gives null).
public static class Program
{
[STAThread]
private static void Main() =>
CompanionApp.Start(
ScreenshotCapture.IsEnabled ? null : "example.myapp",
() => new App(),
LanguageSetting.ForApp("MyApp", App.Languages));
}
// App.xaml is a (xmlns:shell="using:Deskling.Windows.Companion.Shell") with XamlControlsResources.
public partial class App : CompanionApp
{
internal static readonly IReadOnlyList Languages = ["en", "it"]; // its Strings folders, the default first
private MyModel? model;
public App() => InitializeComponent();
protected override ICompanionLaunching MakeCompanion()
{
Loc.UseLanguage(Loc.Choose(Languages)); // the strings and the culture, before anything says a word
CompanionLook.Current = new CompanionLook(MyPalette.Value); // then the look: every control paints from it
var host = new WindowsCompanionHost(
new CompanionViews(MyModel.Spec, id => MyViews.Window(model!, id), () => new MyPanel(model!))
{
DesktopCharacter = () => MyViews.Desktop(model!), // a CharacterCanvas bound to the model
},
TrayIconId,
"My app");
var settings = new MySettings(LocalSettingsStore.ForApp("MyApp"), MyDefaults.Value); // packaged or not
// The speaker, chime, notifier, signal hub and store, as dotnet/Fixture.App/App.xaml.cs makes them.
return model = new MyModel(settings, MyBank.Load(), host /* , … */); // a CompanionAppModel
}
// With DESKLING_SCREENSHOT_MODE set: the same model over a SampleEnvironment (its Seed too, so the next line's time
// is the same every run), through a ScreenshotScript.
protected override bool RunScreenshots() => false;
}
The app keeps its words (copy records, from its .resw), its look (a CompanionPalette of ThemedColors, which can
read the app’s own theme dictionaries; buttons, cards, titles as Styles), Home, onboarding, the tray panel’s layout,
its own settings and windows, and the MSIX manifest (the startup task’s id goes to LaunchAtStartupToggle). Image
parts of a raster character ship as Assets\Characters\..scale-200.png.
The project imports tools/windows/companion.targets from the app’s deskling/
submodule, last: x64 and ARM64, Debug unpackaged (dotnet run; PackageIdentity.IsPackaged is false, so the app
picks a mock store, and LocalSettingsStore.ForApp keeps a JSON file) and every other configuration an MSIX with Native
AOT, the voice clips, sounds and fonts linked from the folders the Mac app bundles.
Beside it, tools/windows/ has the screenshot runner, a package smoke test, a lint for the WinUI code that breaks only
under Native AOT, and the MSIX images from the app icon (tools/README.md, Windows apps). Keep the
model free of WinUI and its words behind IStrings, and its tests run on any OS (Deskling.Testing’s ReswStrings
reads the app’s English .resw).
The implementations stay identical
The Swift code is the reference. conformance/ holds JSON vectors generated from it (scheduler/, and companion/
over the fixture bank and skins), and dotnet/Deskling.Core.Tests and dotnet/Deskling.Companion.Tests replay every
one (kotlin/core the scheduler’s), so a change in behavior on one side fails the other sides’ tests. Apps generate their own content vectors with
DesklingTesting and replay them through Deskling.Testing. See conformance/README.md.
Develop
swift build && swift test # macOS: the package and its tests
scripts/conformance.sh # regenerate conformance/ from the Swift code
cd dotnet && dotnet test Deskling.Core.Tests # .NET: Core builds and tests anywhere; Deskling.Windows builds on Windows (its PRI tooling)
cd dotnet && dotnet test Deskling.Companion.Tests # the companion vectors, anywhere
cd dotnet && dotnet test Deskling.Companion.Shell.Tests # the companion shell's logic, anywhere
cd dotnet && dotnet test Deskling.Testing.Tests # the mocks, conformance helpers and fixtures, anywhere
cd dotnet && dotnet test Deskling.Windows.Companion.Tests # Windows: the shell's Windows half
dotnet run --project dotnet/Fixture.App # Windows: the fixture app, a companion on the fixture bank
scripts/fixture-screenshots.ps1 # Windows: its windows to PNGs, light and dark (-Only home: hidden, a few seconds)
python3 tools/tests/run.py # the tools' tests
scripts/verify.sh # the checks that apply to what you changed
Formatting: xcrun swift-format format -i --configuration .swift-format and dotnet format whitespace dotnet --folder.
Coding agents: read AGENTS.md.
Releases
One tag versions every side, and releases are automatic: every push to master that touches CHANGELOG.md runs
release.yml, which gives the entries under Unreleased the next version (scripts/release.sh), commits the bump and tags
it. By hand, git tag v0.12.4 && git push --tags (or Actions → Release → Run workflow with the version, which creates the tag)
on a commit that already carries the version does the same. Either way it packs the NuGet packages, pushes them to NuGet.org through
Trusted Publishing (no stored key: a policy on nuget.org for
repository vmihalachi/deskling and workflow release.yml, plus the NUGET_USER repository variable holding the nuget.org
username; without them it says so and attaches the packages to the release instead) and drafts a GitHub release from
CHANGELOG.md. It also stages @deskling/landing on npm through trusted publishing, which goes live when an owner
approves it with 2FA (tools/landing/README.md).
License
MIT. © 2026 Vlad Mihalachi.
Recommended Tools
Try a different keyword or remove a filter.
Install
npx skillfish add vmihalachi/deskling