{"id":49548864,"url":"https://github.com/quincyjo/continuity","last_synced_at":"2026-05-02T21:01:06.602Z","repository":{"id":355277555,"uuid":"1227433278","full_name":"quincyjo/continuity","owner":"quincyjo","description":"An event-driven and performance oriented library to power your AwesomeWM configuration.","archived":false,"fork":false,"pushed_at":"2026-05-02T20:16:23.000Z","size":206,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-02T20:24:35.012Z","etag":null,"topics":["awesomewm","lua","luajit"],"latest_commit_sha":null,"homepage":"","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/quincyjo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-02T17:16:10.000Z","updated_at":"2026-05-02T20:13:29.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/quincyjo/continuity","commit_stats":null,"previous_names":["quincyjo/continuity"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/quincyjo/continuity","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quincyjo%2Fcontinuity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quincyjo%2Fcontinuity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quincyjo%2Fcontinuity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quincyjo%2Fcontinuity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/quincyjo","download_url":"https://codeload.github.com/quincyjo/continuity/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quincyjo%2Fcontinuity/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32549387,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-02T19:18:06.202Z","status":"ssl_error","status_checked_at":"2026-05-02T19:16:21.335Z","response_time":132,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["awesomewm","lua","luajit"],"created_at":"2026-05-02T21:00:30.771Z","updated_at":"2026-05-02T21:01:06.586Z","avatar_url":"https://github.com/quincyjo.png","language":"Lua","funding_links":[],"categories":[],"sub_categories":[],"readme":"# continuity\n\nAn event-driven and performance oriented library to power your AwesomeWM\nconfiguration.\n\nIncludes system monitoring, media integration, audio and backlight control,\nclient switching, and async CLI tool wrappers. Modules use a declarative\nconfiguration setup and provide lifecycle subscription callbacks to keep\nconsumers up to date.\n\nThis is a backend library that provides an event driven interface for building\nyour own UIs. Check out the docs for each module for some examples.\n\n## Contents\n\n- [Principles](#principles)\n- [Compatibility](#compatibility)\n- [Installation](#installation)\n- [Conventions](#conventions)\n- [sysinfo](#sysinfo)\n  - [bat](#bat--battery)\n  - [cpu](#cpu--cpu)\n  - [mem](#mem--memory)\n  - [net](#net--network)\n  - [temp](#temp--temperature)\n- [media](#media)\n- [audio](#audio)\n- [backlight](#backlight)\n- [alttab](#alttab)\n- [tools](#tools)\n  - [find](#find)\n  - [grep](#grep)\n- [Contributing](#contributing)\n- [Notes](#notes)\n\n## Principles\n\n- **Event Driven**: Asynchronous, push-based updates, no polling for\n  consumers. This carries all the way down to the underlying CLI commands\n  wherever possible.\n- **Performance Oriented**: Built to be fast and efficient, minimizing\n  interruptions to the glib loop and focused lifecycle callbacks allowing\n  targeted updates.\n- **Consistent**: Modules share a uniform interface across their categories;\n  streamed data (sysinfo), control oriented (audio), and transient resources\n  (media).\n- **Pluggable**: Every module can be given a target backend at `setup` time.\n  Want to use a different backend than is provided? Implement the backend\n  and provide it to the module, or open a PR to add it to the library.\n- **Non-Prescriptive**: No pre-defined widgets or UI. Built to drive your UI\n  with a consistent, intuitive, and a functionally pure interface.\n\n## Compatibility\n\nThis library **is built for**:\n- AwesomeWM 4.3 and later (git master).\n- Lua 5.3 and LuaJIT 2.1.\n\nThis library **should** work with:\n- Lua 5.1+.\n\nThis library does **not** support:\n- AwesomeWM 4.2 and earlier, but your mileage may vary.\n\nThe unit test matrix is lua5.3 and luajit2.1. My personal runtime is luajit2.1\nand awesome git master.\n\nIf you encounter any issues with lua5.1+/luajit2.1 and awesome 4.3+, please\nopen an issue. There is currently no plan to support environments outside of\nthat matrix.\n\n## Installation\n\n**Via LuaRocks:**\n\n```bash\nluarocks install continuity\n```\n\nThen add the LuaRocks loader at the top of your `rc.lua`:\n\n```lua\nrequire(\"luarocks.loader\")\n```\n\n**Via git:**\n\n```bash\ngit clone https://github.com/quincyjo/continuity.git\nln -s continuity/lua/continuity ~/.config/awesome/continuity\n```\n\n## Conventions\n\n**Streamed modules** (`bat`, `cpu`, `mem`, `net`, `temp`) provide a uniform interface:\n\n```lua\nmodule.setup(opts)              -- call once in rc.lua; opts.backend overrides default\nlocal unsub = module:subscribe( -- push-based updates; fires immediately if state is known\n  function(state) ... end\n)\nunsub()                         -- stop receiving updates\nmodule.state                    -- last-known state (may be nil)\nmodule.stop()                   -- tear down backend and clear subscribers\n```\n\n**Control modules** (`audio`, `backlight`) provide a uniform interface:\n\n```lua\naudio.Volume.state                      -- last-known state (may be placeholder values)\naudio.Volume:on_ready(                  -- fires exactly once when state is first known or immediately.\n  function(state) ... end\n)\nlocal unsub1 = audio.Volume:subscribe(  -- push-based updates; only fires when state changes\n  function(state) ... end\n)\nlocal unsub2 = audio.Volume:on_control( -- fires on every control call, regardless of state change\n  function(state) ... end\n)\nunsub1()                                -- stop receiving updates\nunsub2()\naudio.Volume:adjust_perc(-5)            -- relative\naudio.Volume:set_perc(50)               -- absolute\naudio.Volume:toggle_mute()\n```\n\n**Transient resources** (`media.sources`, `audio.inputs`) provide a uniform interface:\n\n```lua\nmedia.sources.all()                     -- snapshot of all sources\nmedia.sources.on_added(                 -- fires when a new source is added\n    function(source) ... end\n)\nmedia.sources.on_updated(               -- fires when a source is updated\n    function(source) ... end\n)\nmedia.sources.on_removed(               -- fires when a source is removed\n    function(id) ... end\n)\n```\n\n---\n\n## sysinfo\n\nThe `sysinfo` module provides system-level information, such as battery status,\nCPU usage, and network rates. Subscribers receive updates regularly via push-based\ncallbacks, and new subscribers are notified immediately if the state is already\nknown.\n\n### bat — Battery\n\nMonitors battery charge, status, and power draw across all batteries via `udevadm`.\nMaintains an EMA-smoothed power average for time-remaining estimates.\n\n```lua\nlocal bat = require(\"continuity.sysinfo.bat\")\nbat.setup()\nbat:subscribe(function(state)\n    -- state.perc, state.status (\"Charging\"|\"Discharging\")\n    -- state.power_now, state.power_average (watts)\n    -- state.ac_online, state.batteries (per-battery breakdown)\nend)\n\nlocal seconds = bat.time_remaining()  -- nil unless discharging\nlocal seconds = bat.time_until_full() -- nil unless charging\n```\n\n[Full documentation](docs/sysinfo/bat.md)\n\n---\n\n### cpu — CPU\n\nReads `/proc/stat` on a timer and reports aggregate and per-core usage.\n\n```lua\nlocal cpu = require(\"continuity.sysinfo.cpu\")\ncpu.setup()\ncpu:subscribe(function(state)\n    -- state.usage  (aggregate %)\n    -- state.cores[n].usage, .user, .system, .idle, .iowait, .steal\nend)\n```\n\n[Full documentation](docs/sysinfo/cpu.md)\n\n---\n\n### mem — Memory\n\nParses `/proc/meminfo` and reports RAM and swap usage.\n\n```lua\nlocal mem = require(\"continuity.sysinfo.mem\")\nmem.setup()\nmem:subscribe(function(state)\n    -- state.total, state.used, state.free (MiB)\n    -- state.perc  (used / total * 100)\n    -- state.swap_total, state.swap_used, state.swap_free (MiB)\nend)\n```\n\n[Full documentation](docs/sysinfo/mem.md)\n\n---\n\n### net — Network\n\nTracks per-interface TX/RX rates, link state, and WiFi signal strength.\n\n```lua\nlocal net = require(\"continuity.sysinfo.net\")\nnet.setup()\nnet:subscribe(function(state)\n    -- state.tx_rate, state.rx_rate  (aggregate bytes/s)\n    -- state.devices[\"eth0\"].tx_rate, .rx_rate, .state, .carrier, .wifi, .signal\nend)\n```\n\n[Full documentation](docs/sysinfo/net.md)\n\n---\n\n### temp — Temperature\n\nReads thermal zone temperatures from sysfs.\n\n```lua\nlocal temp = require(\"continuity.sysinfo.temp\")\ntemp.setup()\ntemp:subscribe(function(state)\n    -- state.avg                  (mean °C across all zones)\n    -- state.zones[\"/sys/...\"]    (°C per zone path)\nend)\n```\n\n[Full documentation](docs/sysinfo/temp.md)\n\n---\n\n## media\n\nAggregates playback state from one or more backends (MPD, MPRIS) into\nindividual observable and controllable playback sources. Provides configurable\nnotifications, remote art resolution, and playback control.\n\nWhile the default configuration will work for the majority of cases out of the\nbox, this module is by the far the most complex and supports a wide range of\ncustomization. Refer to the [full documentation](docs/media.md) for more\ninformation.\n\n```lua\nlocal media  = require(\"continuity.media\")\nlocal mpd    = require(\"continuity.media.backends.mpd\")\nlocal mpris  = require(\"continuity.media.backends.mpris\")\n\nmedia.setup({\n    -- backends = { mpris(), mpd { host = \"localhost\", port = 6600 } }, -- Configure backends. Default is MPRIS only.\n    -- notifications = false  -- disable OSD notifications\n    -- notifications = theme.media_notification  -- provide a custom notification handler.\n})\n\n-- Each returns an unsub function if used in a temporary context, such as a popup or notification.\nmedia.sources.on_added(function(source) ... end)\n-- Updates *should* only be fired when something has changed, but exact behaviour depends\n-- on the specific backend and upstream application.\nmedia.sources.on_updated(function(source)\n    -- source.state.title, .artist, .album, .art_path\n    -- source.playback.play_pause(), .next(), .previous(), .stop()\nend)\nmedia.sources.on_removed(function(source_id) ... end)\n\nmedia.play_pause() -- play/pause most-recent source\nmedia.next()       -- skip forward\nmedia.previous()   -- skip backward\n```\n\n[Full documentation](docs/media.md)\n\n---\n\n## audio\n\nTracks volume and mute state for the default output (sink) and input (source)\ndevices via push-based event subscription — no polling. Out-of-band changes\n(hardware keys, other applications) are detected immediately via `pactl subscribe`\nor `amixer sevents`. The PulseAudio/PipeWire backend is the default; an ALSA\nbackend is also provided.\n\nTwo pre-created handles, `Audio.Volume` (sink) and `Audio.Capture` (source), are\navailable after `setup`. Subscribers receive a full `AudioState` on each change,\nincluding port type (speaker, headphones, headset, …) and connection type (analog,\nbluetooth, …) when the backend can derive them.\n\n```lua\nlocal audio = require(\"continuity.audio\")\naudio.setup()\n\nlocal volume  = audio.Volume   -- default sink\nlocal capture = audio.Capture  -- default source\n\n-- Current readable state. May be (0, false) if not yet known, i.e., before\n-- the first backend event has been received.\nlocal current_level, current_muted = volume.state.level, volume.state.muted\n\n-- Alternatively, seed initial state with on_ready:\n-- Fires exactly once when state is first known, or immediately if already known.\n-- Any on_ready hooks are guaranteed to run before any subscribe hooks registered\n-- during the same execution cycle.\nvolume:on_ready(function(state)\n    -- Concrete known values.\n    current_level, current_muted = state.level, state.muted\nend)\n\n-- Fires whenever a material change is observed (out-of-band or from a control call).\nvolume:subscribe(function(state)\n    require(\"naughty\").notify {\n        title   = \"Volume\",\n        message = state.muted and \"Muted\" or string.format(\"%d%%\", state.level),\n    }\nend)\n\n-- Fires on every control call, regardless of whether the value changed.\n-- Use this for immediate UI feedback (e.g. a transient popup).\nvolume:on_control(function(state)\n    show_volume_popup(state)\nend)\n\n-- Subscribers are informed immediately:\nvolume:adjust_perc(5)    -- +5% (also unmutes on PulseAudio backend)\nvolume:adjust_perc(-5)   -- -5%\nvolume:set_perc(50)      -- absolute\nvolume:toggle_mute()\n\n-- Switch to the ALSA backend:\naudio.setup({ backend = require(\"continuity.audio.backends.alsa\")() })\n```\n\n[Full documentation](docs/audio.md)\n\n---\n\n## backlight\n\nControls and monitors display (and keyboard) brightness via sysfs, acpilight, or\nxbacklight. A `primary_display` handle is pre-created and wired to the first\ndiscovered display device. `on_control` fires on every control call for transient\nUI feedback; `subscribe` fires only on out-of-band changes.\n\n```lua\nlocal backlight = require(\"continuity.backlight\")\nbacklight.setup({\n    -- backend = require(\"continuity.backlight.backends.acpilight\")(), -- default: sysfs\n})\n\n-- Current readable state. May be 0 until first discovery poll completes.\nlocal current = backlight.primary_display.state.brightness\n\n-- Fires once when state is first known or immediately if already known.\nbacklight.primary_display:on_ready(function(state)\n    current = state.brightness\nend)\n\n-- Fires on every change (e.g. control API, hardware key, another application).\nbacklight.primary_display:subscribe(function(state)\n    print(\"brightness changed:\", state.brightness)\nend)\n\n-- Fires on every control call, even when value is unchanged.\n-- Use this for transient UI such as a popup.\nbacklight.primary_display:on_control(function(state)\n    show_brightness_popup(state.brightness)\nend)\n\nbacklight.primary_display:adjust_perc(10)   -- +10%\nbacklight.primary_display:adjust_perc(-10)  -- -10%\nbacklight.primary_display:set_perc(75)      -- absolute\n\n-- Step-based control (sysfs and acpilight backends):\nbacklight.primary_display:adjust(2)         -- +2 raw steps\nbacklight.primary_display:adjust(-2)        -- -2 raw steps\nbacklight.primary_display:set(10)           -- absolute raw step\n```\n\n[Full documentation](docs/backlight.md)\n\n---\n\n## alttab\n\nA keyboard-driven client switcher that maintains a focus-ordered stack and hands\noff to a `keygrabber` during an active switch session. The UI is provided by the\ncaller via an `AlttabUI` callback table; a notification-based fallback is used if\nnone is supplied.\n\nAll keybinds require `held_key` (default \"Mod1\") to be held. Releasing the held key\nfocuses the selected client's screen, switches to that client's tag, and focuses\nand raises the client. Using the `pull_key` also closes the session.\n\n- `select_key` to select next client\n- `mod_key + select_key` to select previous client\n- `pull_key` close the session and moves the selected client to the current\n  screen and current primary tag\n- `mod_key + pull_key` close the session and moves the selected client to the current\n  screen and appends the current primary tag without removing other tags.\n- `1,2,...9` moves the selected client to the corresponding tag\n- `mod_key + 1,2,...9` Toggles the corresponding tag on the selected client\n- `escape` close the session without doing anything\n\n```lua\nlocal alttab = require(\"continuity.alttab\")\n\nalttab.setup({\n    ui = my_ui,          -- AlttabUI implementation (optional)\n    held_key  = \"Mod1\",  -- key held while switcher is open (default: \"Mod1\" eg alt)\n    select_key = \"Tab\",  -- key to cycle through clients     (default: \"Tab\")\n\tmod_key = \"Shift\",   -- key to modify actions, eg index -1 (default \"Shift\")\n\tpull_key = \"Space\",  -- key to pull selected client to current screen and tag (default: \"Space\")\n\tnumber_shift_mappings = { ... }, -- If you don't use QWERTY, tell the keygrabber what the shift values for numbers keys are, 1-9.\n})\n\n-- Bind in rc.lua globalkeys:\nawful.key({ \"Mod1\" },         \"Tab\", function() alttab.switch(1)  end)\nawful.key({ \"Mod1\", \"Shift\"}, \"Tab\", function() alttab.switch(-1) end)\n```\n\n[Full documentation](docs/alttab.md)\n\n---\n\n## tools\n\nAsync wrappers around system CLI tools. Backend is selected at load time from\nwhatever is available on `PATH` (prefers the faster/richer option).\n\n### find\n\nWraps `fd` (preferred) or `find`. Accepts a pattern string or an options table.\n\n```lua\nlocal find = require(\"continuity.tools.find\")\n\n-- Collect all results then call cb once:\nfind(\"config\", function(results, exit_code)\n    for _, path in ipairs(results) do ... end\nend)\n\n-- Stream results in batches as they arrive, batched per glib loop:\nfind.stream({ pattern = \"continuity\", type = \"file\", extension = \"lua\", path = \"~/.config\" }, function(batch, exit_code)\n    -- Batch is nil once the stream is closed.\n    if batch then\n        for _, path in ipairs(batch) do ... end\n    end\nend)\n```\n\n[Full documentation](docs/tools/find.md)\n\n---\n\n### grep\n\nWraps `rg` (preferred) or `grep`. Results are structured `{ filepath, line_number, text }`.\n\n```lua\nlocal grep = require(\"continuity.tools.grep\")\n\ngrep({ \"setup\", \"~/.config/awesome\" }, function(results, exit_code)\n    for _, r in ipairs(results) do\n        print(string.format(\"%s:%d: %s\", r.filepath, r.line_number, r.text))\n    end\nend)\n\n-- Streaming variant:\ngrep.stream({ pattern = \"todo\", path = \"~\", case_insensitive = true }, function(batch, exit_code)\n    -- Batch is nil once the stream is closed.\n    if batch then\n        for _, path in ipairs(batch) do ... end\n    end\nend)\n```\n\n[Full documentation](docs/tools/grep.md)\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, branching\nconventions, and the local verification checklist.\n\n## Notes\n\nYou will likely see some Awesome warning pairs when reloading like this:\n\n```\n2026-04-23 16:23:28 W: awesome: sysinfo.bat.udevadm: Shutting down process group 1224241. A following unknown child exited with signal 15 may occur and can be ignored.\n2026-04-23 16:23:28 W: awesome: sysinfo.temp.sysfs: Shutting down process group 1224246. A following unknown child exited with signal 15 may occur and can be ignored.\n2026-04-23 16:23:28 W: awesome: sysinfo.cpu.procstat: Shutting down process group 1224249. A following unknown child exited with signal 15 may occur and can be ignored.\n2026-04-23 16:23:28 W: awesome: sysinfo.mem.procmeminfo: Shutting down process group 1224253. A following unknown child exited with signal 15 may occur and can be ignored.\n2026-04-23 16:23:28 W: awesome: sysinfo.net.ipmonitor: Shutting down process group 1224390. A following unknown child exited with signal 15 may occur and can be ignored.\n2026-04-23 16:23:28 W: awesome: backlight.acpilight: Shutting down process group 1224259. A following unknown child exited with signal 15 may occur and can be ignored.\n2026-04-23 16:23:28 W: awesome: media.mpris.monitor: Shutting down process group 1224338. A following unknown child exited with signal 15 may occur and can be ignored.\n2026-04-23 16:23:28 W: awesome: media.mpris.lifecycle: Shutting down process group 1224339. A following unknown child exited with signal 15 may occur and can be ignored.\n2026-04-23 16:23:28 W: awesome: spawn_child_exited:388: Unknown child 1224241 exited with signal 15\n2026-04-23 16:23:28 W: awesome: spawn_child_exited:388: Unknown child 1224246 exited with signal 15\n2026-04-23 16:23:28 W: awesome: spawn_child_exited:388: Unknown child 1224249 exited with signal 15\n2026-04-23 16:23:28 W: awesome: spawn_child_exited:388: Unknown child 1224253 exited with signal 15\n2026-04-23 16:23:28 W: awesome: spawn_child_exited:388: Unknown child 1224259 exited with signal 15\n2026-04-23 16:23:28 W: awesome: spawn_child_exited:388: Unknown child 1224338 exited with signal 15\n2026-04-23 16:23:28 W: awesome: spawn_child_exited:388: Unknown child 1224339 exited with signal 15\n2026-04-23 16:23:28 W: awesome: spawn_child_exited:388: Unknown child 1224390 exited with signal 15\n```\n\nThis is expected, and can be ignored. Many of the backends use long-lived\nprocesses instead of a `gears.timer`. When Awesome reloads, the process groups\nare killed, and if this happens after the reload transfers all subprocesses to\nthe new Awesome instance, the child exits will be reported as\n`Unknown child ... exited with signal 15`. This is confirmation that the\nprocess properly cleaned itself up, and it is not an error.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquincyjo%2Fcontinuity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquincyjo%2Fcontinuity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquincyjo%2Fcontinuity/lists"}