{"id":13608692,"url":"https://github.com/letoram/cat9","last_synced_at":"2025-05-15T20:07:37.815Z","repository":{"id":39513886,"uuid":"461183910","full_name":"letoram/cat9","owner":"letoram","description":"A User shell for LASH","archived":false,"fork":false,"pushed_at":"2025-05-03T14:14:07.000Z","size":2495,"stargazers_count":510,"open_issues_count":3,"forks_count":17,"subscribers_count":13,"default_branch":"sub","last_synced_at":"2025-05-14T12:03:12.075Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Lua","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/letoram.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"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}},"created_at":"2022-02-19T12:23:10.000Z","updated_at":"2025-05-11T05:40:36.000Z","dependencies_parsed_at":"2024-01-07T21:22:25.349Z","dependency_job_id":"22b8da1c-ed3a-45a5-96e4-5e2b54fb3a69","html_url":"https://github.com/letoram/cat9","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/letoram%2Fcat9","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/letoram%2Fcat9/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/letoram%2Fcat9/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/letoram%2Fcat9/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/letoram","download_url":"https://codeload.github.com/letoram/cat9/tar.gz/refs/heads/sub","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254414501,"owners_count":22067272,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2024-08-01T19:01:29.226Z","updated_at":"2025-05-15T20:07:32.771Z","avatar_url":"https://github.com/letoram.png","language":"Lua","readme":"Cat9\n====\n\nWhat is it?\n===========\n\nCat9 is a user shell script for LASH - a command-line shell that discriminates\nagainst terminal emulators, written in Lua. You probably have not heard of LASH\nbefore. If you really must know, check the Backstory section below.\n\nLASH just provides some basic shared infrastructure and a recovery shell. It\nthen runs a user provided script that actually provides most of the rules for\nhow the command line is supposed to look and behave.\n\nThat brings us back to Cat9, which is my such script. You can use it as is or\nremix it into something different that fits you - see HACKING.md for more tips.\n\nWhat can it do?\n===============\n\nOne of the bigger conveniences, on top of being quite snappy, is to run and\ncleanly separate multiple concurrent jobs asynchronously, with the results from\n'out' and 'err' being kept until you decide to reuse or forget it. At the same\ntime, traditionally noisy tasks like changing directories are blocked from\npolluting your view with irrelevant information.\n\nhttps://user-images.githubusercontent.com/5888792/161494772-2abccac4-bb92-4a12-9a69-987e66201719.mp4\n\nThis allows for neat visuals like changing layouts, reordering presentation,\nfolding and unfolding. It also allows for reusing results of a previous job\nwithout thinking much about it - caching is the default and you don't need to\nredirect results to files just for reuse or re-execute pipelines when all you\nwanted was different processing of its old outputs.\n\nIt is also designed with the intention of being able to frontend- legacy cli\ntools with little effort - the set of builtins that the shell provides can be\ncontextually swapped for something specific to working with some domain or tool\nspecific context. In this way, famously unfriendly tools can be worked around\nand integrated into your workflow as seemless as possible.\n\nIt cooperates with your desktop window manager (should you have one), letting\nit take care of splitting out things into new windows or even embedding media\nor graphical application output into clipped regions of its own window.\n\nhttps://user-images.githubusercontent.com/5888792/161494402-9e5636e3-dd78-4fcf-bff0-fe5c3dd0a369.mp4\n\nInstallation\n============\n\nBuilding and setting this up is currently not for the faint of heart. It might\nlook simple at first glance, but going against the grain of decades of\naccumulated legacy comes with some friction.\n\nFor starters you want an Arcan build that comes straight from the source. The\nthings needed here are new, unlikely covered by any release, and is actively\nworked on. [Arcan](https://github.com/letoram/arcan) is unpleasant to build\nfrom source and, if you want it to replace your entire display server, also a\npain to setup. Twice the fun.\n\nFor our ends here, it works just fine as a window that looks strangely much\nlike a terminal emulator would look, but its innards are entirely different.\n\nIf you managed to build Arcan to your liking, you then need to start Arcan\nwith a suitable window manager.\n\nThere are several to chose from, notable ones being:\n\n * Durden - Full tiling/stacking will all bells and whistles imaginable.\n * Pipeworld - ZUI dataflow which is both crazy and awesome at the same time.\n * Safespaces - VR, some say it is the future and others the future of the past.\n\nThen there is the much more humble 'console' that mimics the BSD/Linux console\nwith just fullscreen and invisible tabs. Since it comes included with Arcan\nitself, we will go for that one. The way to actually start lash from within\nthese vary, for console it is easy:\n\n    arcan console lash\n\nThis should convince Arcan to setup a simple fullscreen graphical shell that\nthen runs the textual command-line shell in lash. Alas the shell will be kindof\nuseless. This is where Cat9 comes in.\n\nUnderneath the surface it actually runs:\n\n    ARCAN_ARG=cli=lua afsrv_terminal\n\ncopy or link cat9.lua to $HOME/.arcan/lash/default.lua or cat9.lua (make the\ndirectory should it not exist) as well as the cat9 subdirectory so that there\nis a $HOME/.arcan/lash/cat9.\n\nSimilarly, in Durden it would be global/open/terminal=cli=lua and for\nsafespaces, tack on cli=lua to the terminal spawn line, e.g.\nlayers/current/terminal=cli=lua. In recent Durden versions this has a shortcut\nas global/open/lash, and is also bound to m1+m2+enter.\n\nNext time you start the arcan console like above, if you picked the default.lua\nroute it will start immediately - otherwise you have to manually tell lash to\nrun the cat9 rulset with the shell command:\n\n    shell cat9\n\nThis scans LASH\\_BASE, HOME/.arcan/lash and XDG\\_CONFIG\\_HOME/arcan/lash\nfor a matching cat9.lua and switches over to that. It is also possible to\nset LASH\\_SHELL=cat9 and cat9.lua will be tried immediately instead of\ndefault.lua\n\nThis extra set of steps is to allow multiple shells to coexist, so that there\nis a premade path for other rulesets to join the scene with less of a\ndisadvantage.\n\nDependencies\n============\n\nSome of the built-in commands rely on existing tools installed in system\nappropriate locations to work properly:\n\n    System: open(afsrv_decode), doas(for switching uid)\n    Dev: scm(fossil), debug(gdb, lldb)\n    Net: wifi(wpa_supplicant with control socket)\n\nUse\n===\n\nBy default, commands will get tracked as a 'job'.\nThese get numeric identifiers and are referenced by a pound sign:\n\n    find /tmp\n    repeat #0 flush\n\nThese pounds can also use relative addresses, #-2 would point to the second\nlatest job to be created. There are also special jobs, like #csel pointing to\nthe currently cursor-selected job, and #last pointing to the latest created\njob.\n\nMost builtin commands use job references in one way or another. The context of\nthese jobs, e.g. environment variables and path is tracked separately. By\nstarting a command with a job reference, the current context is temporarily\nset to that of a previous job.\n\n    cd /usr/share\n    find . -\u003e becomes job #0\n    cd /tmp\n    #0 pwd -\u003e /usr/share\n\nMost strings entered will be executed as non-tty jobs. To run something\nspecifically as a tty (ncurses or other 'terminal-tui' like application),\nmark it with a ! to spawn a new window bound to a terminal emulator.\n\n    !vim\n\nWindow creation and process environment can be controlled (e.g. vertical\nsplit (v) or swallow (s)):\n\n    v!vim\n\nTo forego any parsing or internal pipelineing and run the entire line verbatim\n(forwarded to sh -c) use !!:\n\n    !!find /usr |grep share\n\nThis is also useful when you need to do shell expansions:\n\n    !!rm -rf *\n\nCertain clients really want a pseudoterminal or they refuse to do anything,\nfor those clients, start with p! like so:\n\n    p!vim\n\nThese will default switch to 'view wrap vt100' that has a rudimentary terminal\nemulator state machine that need some more work (see base/vt100\\*).\n\nData can be sliced out of a job into the current command-line with ctrl+space:\n\n    rm #0(1,3)\n\nand pressing ctrl+space would copy lines 1 and 3 and expand them into the\ncurrent command line. This argument format is also supported by some builtins,\ntyping:\n\n    copy #0(1-5)\n\nand pressing enter would copy the lines 1 to 5 into a new job.\n\n# Customisation\n\nThe config/config.lua file can be edited to change presentation, layout and\nsimilar options, including the formats for prompts and titlebars. Most of these\noptions can also be reached at runtime via the 'config' builtin, further below.\n\nKeybindings and mouse button presses act just like lines typed on the prompt,\nbut are defined in the config/bindings.lua file. Keybindings are activated on\nctrl+[key]. The following:\n\n    bnd[tui.keys.A] = \"forget #-1\"\n\nwould create a binding that whenever ctrl-A is pressed, the latest job created\nwould be removed.\n\nMouse buttons are similar, but the 'key' is slightly more complex as they are\nsplit based on where you are clicking (titlebar, data body, ...). The currently\nmost complex binding looks something like this:\n\n    bnd.m2_data_col_click = \"view #csel select $=crow\"\n\nWould apply the builtin view command on the mouse-selected job at the current\nrow offset if the second mouse button was clicked on the first column\n(line-number).\n\nIt is also possible to work with 'chorded' input bindings where one binding\nsets a 'consume on use' input, e.g. Ctrl-w to Ctrl-a. See bindings.lua for\nexamples on how that is configured.\n\n# Builtins\n\nThere are a number of 'builtin' commands. This is traditionally commands\nimplemented by the shell itself rather than outsourcing it to some external\nprogram. Most shells has this as a global namespace, but in cat9 it is\nhierarchical. There is a small default set, and then a number of\ninterchangeable ones.\n\nThese are defined by a basename, that is reflected in how files are organised\nin the project:\n\n    cat9/default.lua\n    cat9/default/open.lua\n\t\tcat9/config/default.lua\n    ...\n\nThe reason for this structure is to allow lash to be reused for building CLI\nfrontends to other tools, maintain a unified look and feel and swap between\nthem quickly at will.\n\nYou can also modify/extend this to mimic the behaviour of other common shells.\n\nThese also include a set of views. A view is a script that defines how to\npresent the data within a job, and controls things like colour and formatting,\noptional elements like line numbers as well as wrapping behaviour.\n\nThese have a separate config store that follows the pattern:\n\n    cat9/config/(basedir).lua\n\nTwo sets of builtins are of additional importance, 'default' that contains cat9\nrelated commands and 'system' which contains many of the normal commands one\nwould expect from a traditional shell such as 'cd'.\n\nMany of these builtins have a toggleable help descriptor for their commands and\nsubcommands. This can be toggled by pressing F1 at any time during the readline\nprompt.\n\n## Builtins:Default\n\nThe commands included in the default set are:\nbuiltin, input, view, open, config, forget, repeat, trigger\n\n### Builtin\n\nThe command for switching between sets of builtins is also a builtin, albeit\na special one that is always present. It takes the arguments:\n\n    builtin [setname=system] [nodef]\n\nThis will cause a hot reload of the set of builtins, including the default\nunless nodef is provided as the second argument.\n\n### Input\n\nThe input command is for controlling how parts of the UI responds to mouse or\nkeyboard inputs. By default the keyboard is grabbed by the command-line. This\ngrab can be toggled with CTRL+ESCAPE.\n\n    input #jobid\n\nThis will only trigger for jobs that have a working input sink, and retain\ncurrent focus if it does not.\n\n### Config\n\n    config key value\n\nThe config options changes the runtime shell behavior configuration. It is\npopulated by the keys and values in config/config.lua.\n\nCertain targets also allow properties to set, e.g. persistence or an alias:\n\n    config #id alias myname\n    config #myname persist auto\n\nIt can be used to hot-reload settings and the code for default builtins:\n\n    config =reload\n\nIt can be used to define aliases:\n\n    config myalias \"my longer command\"\n\nWhen ctrl+space is used with readline, the last word will be swapped for the\nalias. This will not have a commit action, so the expansion is visible to avoid\nunpleasant surprises.\n\n### Open\n\n    open file or #job [hex] [new | vnew | tab | swallow]\n\nThis tries to open the contents of [file] through a designated handler. For the\njob mode specifically, it either switches the window to a text or hex buffer.\nIt is also possible to pop it out as a new window or tab.\n\n### Each\n\n    each (sequential OR merge) #job !! cmd $arg $dir\n\nThis will slice data from #job and for each line in the output, parse the\nstring after !! substituting arg for the line and and dir for the directory\nof #job, or if the line contains a path (/to/somewhere) the /to part. The\nlater form is useful when combined with `stash`. By default each runs all\ncommands in parallel.\n\n### Contain\n\n    contain new\n    contain #job add #job1 #job2 ..\n    contain #job show n1 n2 ..\n                 show append n1 n2 ..\n    contain #job release n1 n2 ..\n    contain #job forget n1 n2 ..\n    contain #job capture [on | off | toggle]\n\nThis creates a container job which absorbs other visible ones into itself. It\nis useful both for reducing visible clutter and for treating a collection of\ndata sources as a single discrete one.\n\nYou spawn a new container job via 'contain new' and either manually add the\njobs that should be treated this way into itself through add #id #job, or by\nenabling job capture via 'contain #id capture on'.\n\nIt is not possible to nest containers, and some builtins that spawn jobs will\nrefuse to be captured in this way. Jobs that are bound to a different window\nwill also refuse capture.\n\n### Forget\n\n    forget #job1 #job2\n    forget #job1  .. #job3\n    forget all-bad\n    forget all-passive\n    forget all-hard\n\nThis will remove the contents and tracking of previous jobs, either by\nspecifying one or several distinct jobs, or a range. If any marked job is still\nactive and tied to a running process, that process will be killed. There are\nalso special presets: all-bad, all-hard and all-passive; all-bad forgets the\ncompleted jobs with !0 return status code; all-hard removes all jobs, passive\nand running; all-passive removes all jobs that have completed running.\n\n### Repeat\n\n    repeat #job [flush | edit]\n\nThis will re-execute a previously job that has completed. If the flush argument\nis specified, any collected data from its previous run will be discarded. If\nthe edit argument is specified, the command-line which spawned the job will be\ncopied into the command line - and if executed, will append or flush into the\nexisting job identifier.\n\n### View\n\n    view #job stream [opt1] [opt2] .. [optn]\n\nChanges job presentation based on the provided set of options.\nThe possible options are:\n\n* scroll n - move the starting presentation offset from the last line read\n             to n lines or bytes backwards.\n\n* out or stdout - set the presentation buffer to be what is read from the\n                  standard output of the job.\n\n* err or stderr - set the presentation buffer to be what is read from the\n                  standard error output of the job.\n\n* col or collapse - only present a small number of lines.\n\n* exp or expand - present as much of the buffer as possible.\n\n* tog or toggle - switch between col and exp mode\n\n* linenumber - toggle showing a column with line numbers on/off\n\nThere are also a number of dynamic views that can apply higher level transforms\non the contents to change how it presents. One such view is 'wrap':\n\n    view #job wrap [vt100] [max-col]\n\nThis implements word wrap, optionally filtered through a terminal state machine\n(vt100) and with a custom column cap.\n\n### Copy\n\n    copy src [opts] [dst]\n\nThe copy command is used to copy data in and out of Cat9 itself, such as taking\nthe output of a previous job and storing it in a file, into another running job\nor into a new job (if dst is not provided).\n\nBy default, src and dst are treated as just names with normal semantics for\nabsolute or relative paths. Depending on a prefix used, the role can change:\n\nUsing # will treat it as a job reference.\n\nUsing pick: will treat it as a request to an outer graphical shell (i.e. your\nwm) to provide a file, optionally with an extension hint:\n\n    copy pick:iso test.iso\n\nThe optional source arguments can be used to slice out subranges, e.g.\n\n    copy #0 (1-10,20) dst\n\nWhich would copy lines 1 to 10 and line 20 of the current view buffer into\nthe destination.\n\nCopy destinations do not have to be files, they can also be other interactive\njobs, or special ones like clipboard: that would forward to the outer WM\nclipboard.\n\n### Trigger\n\n    trigger #job condition [delay n] action\n\nIt is possible to attach several event triggers to a job that has an external\naction attached. The command for that is 'trigger'. The condition can be either\n'ok' or 'fail', with an optional delay in seconds. The action is any regular\ncommand-line string (remember to encapsulate with \"\").\n\nTo remove previously set triggers, use 'flush'.\n\nA common case for trigger is to repeat a job that finished:\n\n    trigger #0 ok delay 10 run \"repeat #0\"\n\nWould keep the job #0 relaunching 10 seconds after completing until removed:\n\n    trigger #0 ok flush\n\n## Builtins:System\n\nThe commands included in the system set are:\n\n### Signal\n\n    signal [signal: kill, hup, user1, user2, stop, quit, continue] #jobid or pid\n\nThe signal commands send a signal to an active running job or a process based\non a process identifier (number).\n\n### Cd\n\n    cd directory\n    cd #job\n\nChange the current directory to the specified one, with regular absolute or\nrelative syntax. It is also possible to cd back into the directory that was\ncurrent when a specific job was created.\n\nCd also tracks which directories commands are being run from and adds them\nto a history. This can be accessed via the special:\n\n    cd f ...\n\nWhere the f will be omitted and the set of suggested completions will come\nfrom the list of favourites. This can be manually altered, using f- to\nremove a path and f+ . or f+ /some/path to add it to the favorites.\n\n### Env\n\n    env [#job] key value\n\nThis is used to change the environment for new jobs. It can also be used to\nupdate the cached environment for an existing job. This environment will be\napplied if the job is repeated, or if a new job is derived from the context\nof one:\n\n    env #0 LS_COLOR yes\n    #0 ls /tmp\n\n### List\n\n    list [path]\n\nList is similar to that of traditional 'ls', but adds mouse navigation, re-use\nof the same job window to step in / out of directories and automatic refresh on\nchanges to the directory (if inotifytools are present).\n\n### Term (prefix + \\!)\n\nThis exposes a number of ways to spawn a legacy terminal device and emulator,\nwith a prefix which hints at how the command line is translated and how the\nwindow management should work.\n\nthe \\!\\! form disables parsing, expansion and so on and forwards the line\nverbatim through /bin.sh (config:sh\\_runner).\n\nThe different prefixes are:\n* v : spawn as a new-vertical window\n* t : spawn as a new tab\n* s : spawn as a 'swallow' window that takes the place of the current until closed\n* p : spawn as a pseudoterminal embedded job with the internal vt100 view applied\n\n(a and l are reserved prefixes that will be used for arcan clients specifically)\n\n### Stash\n\nThe stash command lets you queue filenames and then applying group actions on\nthem all at once. It also monitors the queue of drag-and-drop items.\n\nThis is useful for hand-picking sets of files.\n\n    stash add [file] [file2] ...\n\nThis adds the specified files to an existing stash (or create a new if there\nisn't one) that is visible as a job.\n\nIt is also possible to detect changes to the stash.\n\n    stash verify\n\nThis runs a checksum tool (e.g. sha256sum) for each item. Repeating the\nverification would mark (-) those that have changes since last.\n\nIt is also used to build a virtual filesystem:\n\n    stash map /some/stash/entry new/path/name\n\nThen build an archive:\n\n    stash archive tar myfile.tar\n\nOmit the destination file to get the contents into a job.\n\n## Builtins:Spreadsheet\n\nThe spreadsheet is a neat way of organising and processing data, and some other\ncommands will create one automatically as needed. It has a basic programming\nlanguage with the same syntax as the regular Cat9 CLI.\n\n### New\n\nTo create a new spreadsheet you run:\n\n    new\n\n### Insert / Replace\n\nThe generic format is:\n\n    insert location item1 [...]\n\nWhere location can be a single row reference, or a cell reference (e.g. C4).\nThe item1 are subject to the same argument parsing and job slicing rules as\neverything else, but can also be populated by an external command that will\nrun through /bin/sh:\n\n    insert 4 [separate \"ptn\"=\"%s+\"] [split \"ptn\"=\"\\n\"] !shell-command\n\nWhere (ptn) is a valid Lua pattern. 'Split' will be used to split into new\nrows and separate to divide up into columns.\n\n### Set\n\nTo populate a single cell, you use the 'set' command:\n\n    set location value\n\nWhere (value) can be a string, number or expression.\nExpressions are distinguished by the '=' prefix:\n\n   set C1 =max(A1:A5)\n\nWould populate C1 with the maximum value found in the range of cells from\nA1,A2..A5.\n\n### Remove\n\nThe remove command is used to remove a full row or column.\n\n   remove [noshift] row or column [count=1]\n\nIf the shift option is specified, everything below the row or to the right of\nthe column will be shifted up/left (count) number of times.\n\n### Plot\n\nThe plot command combines embedded media job creation like 'open' would, with a\ncopy operation, forwarding the sliced data to an external renderer like\n'gnuplot' or 'dot' to generate a chart or graph and finally afsrv\\_decode to\nrender it back into the job.\n\n## Dev\n\nThe dev builtin set is experimental and ongoing. It aims to consolidate developer\ntooling such as build systems, debuggers, revision control and so on.\n\n### Debug\n\nThis implements debugger protocols such as DAP. It is a fairly complex one as\nit is a complete debugger frontend with the many views and controls that\nentails.\n\nIt starts simple enough:\n\n    debug launch /path/to/program arg1 ..\n    debug attach pid\n\nThis will spawn a number of views (or a warning in the case of attach where\npermission restrictions may prevent you from attaching to a process) and the\ncommand-line will default to forward commands to the debugger process itself.\n\n### Debug:thread\n\nMost debug commands requires a thread and a frame reference.\n\n### Debug:files\n\n### Debug:maps\n\nBackstory\n=========\n\nArcan is what some would call an ambitious project that takes more than a few\nminutes to wrap your head around. Among its many subprojects are SHMIF and TUI.\nSHMIF is an IPC system -- initially to compartment and sandbox media parsing\nthat quickly evolved to encompass all inter-process communication needed for\nsomething on the scale of a full desktop.\n\nTUI is an API layered on top of SHMIF client side, along with a text packing\nformat (TPACK). It was first used to write a terminal emulator that came\nbundled with Arcan, and then evolved towards replacing all uses of ECMA-48 and\nrelated escape codes, as well as kernel-tty and userspace layers. The end goal\nbeing completely replacing all traces of ncurses, readline, in-band signalling\nand so on -- to get much needed improved CLIs and TUIs that cooperate with an\nouter graphical desktop shell rather than obliviously combat it.\n\nThis journey has been covered in several articles, the most important ones\nbeing 'the dawn of a new command line interface' and the more recent 'the day\nof a new command line interface: shell'.\n\nThe later steps then has been a migration toggle in the previous arcan terminal\nemulator that allows a switch over to scripts with embedded Lua based bindings\nto the TUI API and its widgets. This later mode and support scripts is what we\nrefer to as Lash. Lash in turn is too barebones to be useful, and a set of user\nscripts are plugged in, Cat9 is one such set.\n","funding_links":[],"categories":["Uncategorized","others","Lua","\u003ca name=\"shells\"\u003e\u003c/a\u003eShells"],"sub_categories":["Uncategorized"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fletoram%2Fcat9","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fletoram%2Fcat9","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fletoram%2Fcat9/lists"}