{"id":17151224,"url":"https://github.com/soda480/list2term","last_synced_at":"2026-01-22T02:20:59.099Z","repository":{"id":60314356,"uuid":"542323271","full_name":"soda480/list2term","owner":"soda480","description":"A lightweight tool to mirror and dynamically update a Python list in your terminal, with built-in support for concurrent output (asyncio / threading / multiprocessing).","archived":false,"fork":false,"pushed_at":"2026-01-19T19:20:17.000Z","size":2281,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-20T00:56:50.151Z","etag":null,"topics":["asyncio","multiprocessing","pybuilder","python","terminal"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soda480.png","metadata":{"files":{"readme":"README.md","changelog":null,"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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-09-27T23:12:51.000Z","updated_at":"2026-01-19T19:19:24.000Z","dependencies_parsed_at":"2023-11-24T05:21:14.065Z","dependency_job_id":"8019db51-2e91-451d-abe7-2835c41ec911","html_url":"https://github.com/soda480/list2term","commit_stats":{"total_commits":10,"total_committers":2,"mean_commits":5.0,"dds":0.09999999999999998,"last_synced_commit":"9258cd675c77c62e933501993712dcd6ed81bd23"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/soda480/list2term","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soda480%2Flist2term","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soda480%2Flist2term/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soda480%2Flist2term/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soda480%2Flist2term/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soda480","download_url":"https://codeload.github.com/soda480/list2term/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soda480%2Flist2term/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28651286,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-22T01:17:37.254Z","status":"online","status_checked_at":"2026-01-22T02:00:07.137Z","response_time":144,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["asyncio","multiprocessing","pybuilder","python","terminal"],"created_at":"2024-10-14T21:37:32.144Z","updated_at":"2026-01-22T02:20:59.092Z","avatar_url":"https://github.com/soda480.png","language":"Python","readme":"[![ci](https://github.com/soda480/list2term/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/soda480/list2term/actions/workflows/ci.yml)\n![Coverage](https://raw.githubusercontent.com/soda480/list2term/main/badges/coverage.svg)\n[![PyPI version](https://badge.fury.io/py/list2term.svg?icon=si%3Apython)](https://badge.fury.io/py/list2term)\n\n# list2term\n\nA lightweight tool to mirror and dynamically update a Python list in your terminal, with built-in support for concurrent output (asyncio / threading / multiprocessing).\n\n## Why use `list2term`?\n\n- **Live list reflection**: keep a list’s contents in sync with your terminal display — updates, additions, or removals are reflected in place.\n- **Minimal dependencies**: not a full TUI framework—just what you need to display and update lists.\n- **Concurrency-aware**: includes helpers for safely displaying progress or status messages from `asyncio` tasks, `multiprocessing.Pool` workers or threads.\n- **TTY-aware fallback**: detects when output isn’t a terminal (e.g. piped logs) and disables interactive behavior gracefully.\n- **Thread safety**: all public mutating operations are serialized with a re-entrant lock, ensuring atomic updates to internal state and terminal output when called from multiple threads.\n\n\n## Installation\n\n```bash\npip install list2term\n```\n\n## Key Concepts \u0026 API\n\nLines — main class\n\nlist2term revolves around the Lines class, a subclass of collections.UserList, which you use to represent and display a list in the terminal.\n\nConstructor signature (default values shown):\n\n```\nLines(\n    data=None,\n    size=None,\n    lookup=None,\n    show_index=True,\n    show_x_axis=True,\n    max_chars=None,\n    use_color=True,\n    y_axis_labels=None,\n    x_axis=None)\n```\n\n**Parameters**\n\n| Parameter     | Description                                                                                                                        |\n| ------------- | ---------------------------------------------------------------------------------------------------------------------------------- |\n| `data`        | The initial list or iterable containing the items to display and sync with the terminal.                                           |\n| `size`        | Integer specifying the initial length of the list. When provided, the list is pre-populated with empty strings. Use this when you know the desired list size but not the initial values.  |\n| `lookup`      | A list of unique string identifiers used to route messages from concurrent workers to specific lines. Each identifier in the lookup list corresponds to one line in the display (default: `None`). |\n| `show_index`  | Boolean flag to display line indices or labels on the left side of each line (default: `True`).                                   |\n| `show_x_axis` | Boolean flag to display an X-axis ruler above the data for reference (default: `True`).                                           |\n| `max_chars`   | Maximum character width allowed per line; text exceeding this limit is truncated and suffixed with `...` (default: 150).         |\n| `use_color`   | Boolean flag to apply terminal color styling to line indices and labels (default: `True`).                                        |\n| `y_axis_labels` | A list of custom labels to display on the Y-axis (left side), replacing default numeric indices. Must match the length of `data`. Labels are right-justified before each line (default: `None`, uses numeric indices). |\n| `x_axis`     | A string or list of strings to display as X-axis ruler(s) above the data. Accepts a single string for one line or a list for multiple lines. If not provided, a default numbered ruler is auto-generated (default: `None`). |\n\n\nInternally, `Lines` is backed by its `.data` attribute (like any UserList). You can mutate it:\n\n```\nlines[index] = \"new value\"\nlines.append(\"another\")\nlines.pop(2)\n```\n\nThese updates automatically refresh the terminal.\n\n**Concurrent Workers \u0026 Message Routing**\n\nWhen running tasks concurrently (via `asyncio` or `multiprocessing.Pool`), you often want each worker to report status lines. list2term supports that via:\n\n`Lines.write(...)` — accepts strings in the form \"{identifier}-\u003e{message}\". The identifier is looked up in lookup to decide which line to update.\n\nMultiprocessing helpers — the package offers `pool_map` and other abstractions in `list2term.multiprocessing` to simplify running functions in parallel and routing their messages.\n\nYour worker functions must accept a logging object (e.g. `LinesQueue`) and use logger.write(...) to send messages back.\n\n## Examples\n\n### Display list - [example1](https://github.com/soda480/list2term/blob/main/examples/example1.py)\n\nStart with a list of 15 items containing random sentences, then update sentences at random indexes. As items in the list are updated the respective line in the terminal is updated to show the current contents of the list.\n\n\u003cdetails\u003e\u003csummary\u003eCode\u003c/summary\u003e\n\n```Python\nimport time\nimport random\nfrom faker import Faker\nfrom list2term import Lines\n\ndef main():\n    print('Generating random sentences...')\n    docgen = Faker()\n    with Lines(size=15, show_x_axis=True, max_chars=100) as lines:\n        for _ in range(200):\n            index = random.randint(0, len(lines) - 1)\n            lines[index] = docgen.sentence()\n            time.sleep(.05)\n\nif __name__ == '__main__':\n    main()\n```\n\n\u003c/details\u003e\n\n![example1](https://raw.githubusercontent.com/soda480/list2term/main/docs/images/example1.gif)\n\n### Display list of dynamic size - [example2](https://github.com/soda480/list2term/blob/main/examples/example2.py)\n\nStart with a list of 10 items containing random sentences, then add sentences to the list, update existing sentences or remove items from the list at random indexes. As items in the list are added, updated, and removed the respective line in the terminal is updated to show the current contents of the list.\n\n\u003cdetails\u003e\u003csummary\u003eCode\u003c/summary\u003e\n\n```Python\nimport time\nimport random\nfrom faker import Faker\nfrom list2term import Lines\n\ndef main():\n    print('Generating random sentences...')\n    docgen = Faker()\n    with Lines(data=[''] * 10, max_chars=100) as lines:\n        for _ in range(100):\n            index = random.randint(0, len(lines) - 1)\n            lines[index] = docgen.sentence()\n        for _ in range(100):\n            update = ['update'] * 18\n            append = ['append'] * 18\n            pop = ['pop'] * 14\n            clear = ['clear']\n            choice = random.choice(append + pop + clear + update)\n            if choice == 'pop':\n                if len(lines) \u003e 0:\n                    index = random.randint(0, len(lines) - 1)\n                    lines.pop(index)\n            elif choice == 'append':\n                lines.append(docgen.sentence())\n            elif choice == 'update':\n                if len(lines) \u003e 0:\n                    index = random.randint(0, len(lines) - 1)\n                    lines[index] = docgen.sentence()\n            else:\n                if len(lines) \u003e 0:\n                    lines.pop()\n                if len(lines) \u003e 0:\n                    lines.pop()\n            time.sleep(.1)\n\nif __name__ == '__main__':\n    main()\n```\n\n\u003c/details\u003e\n\n![example2](https://raw.githubusercontent.com/soda480/list2term/main/docs/images/example2.gif)\n\n### Display messages from `asyncio` processes - [example3](https://github.com/soda480/list2term/blob/main/examples/example3.py)\n\nThis example demonstrates how `list2term` can be used to display messages from asyncio processes to the terminal. Each item of the list represents a asnycio process.\n\n\u003cdetails\u003e\u003csummary\u003eCode\u003c/summary\u003e\n\n```Python\nimport asyncio\nimport random\nfrom faker import Faker\nfrom list2term import Lines\n\nasync def do_work(worker, lines):\n    total = random.randint(10, 65)\n    for _ in range(total):\n        # mimic an IO-bound process\n        await asyncio.sleep(random.choice([.05, .1, .025]))\n        lines[worker] = f'processed {Faker().name()}'\n    return total\n\nasync def run(workers):\n    y_axis_labels = [f'Worker {str(i + 1).zfill(len(str(workers)))}' for i in range(workers)]\n    with Lines(size=workers, y_axis_labels=y_axis_labels) as lines:\n        return await asyncio.gather(*(do_work(worker, lines) for worker in range(workers)))\n\ndef main():\n    workers = 15\n    print(f'Total of {workers} workers working concurrently')\n    results = asyncio.run(run(workers))\n    print(f'The {workers} workers processed a total of {sum(results)} items')\n\nif __name__ == '__main__':\n    main()\n```\n\n\u003c/details\u003e\n\n![example3](https://raw.githubusercontent.com/soda480/list2term/main/docs/images/example3.gif)\n\n\n### Display messages from multiprocessing pool processes - [example4](https://github.com/soda480/list2term/blob/main/examples/example4.py)\n\nThis example demonstrates how `list2term` can be used to display messages from processes executing in a [multiprocessing Pool](https://docs.python.org/3/library/multiprocessing.html#using-a-pool-of-workers). Each item of the list represents a background process. The `list2term.multiprocessing` module contains a `pool_map` method that fully abstracts the required multiprocessing constructs, you simply pass it the function to execute, an iterable of arguments to pass each process, and an optional instance of `Lines`. The method will execute the functions asynchronously, update the terminal lines accordingly and return a multiprocessing.pool.AsyncResult object. Each line in the terminal represents a background worker process.\n\nIf you do not wish to use the abstraction, the `list2term.multiprocessing` module contains helper classes that facilitates communication between the worker processes and the main process; the `QueueManager` provide a way to create a `LinesQueue` queue which can be shared between different processes. Refer to [example4b](https://github.com/soda480/list2term/blob/main/examples/example4b.py) for how the helper methods can be used.\n\n**Note** the function being executed must accept a `LinesQueue` object that is used to write messages via its `write` method, this is the mechanism for how messages are sent from the worker processes to the main process, it is the main process that is displaying the messages to the terminal. The messages must be written using the format `{identifier}-\u003e{message}`, where {identifier} is a string that uniquely identifies a process, defined via the lookup argument to `Lines`.\n\n\u003cdetails\u003e\u003csummary\u003eCode\u003c/summary\u003e\n\n```Python\nimport time\nfrom list2term import Lines\nfrom list2term.multiprocessing import pool_map\nfrom list2term.multiprocessing import CONCURRENCY\n\ndef is_prime(num):\n    if num == 1:\n        return False\n    for i in range(2, num):\n        if (num % i) == 0:\n            return False\n    else:\n        return True\n\ndef count_primes(start, stop, logger):\n    worker_id = f'{start}:{stop}'\n    primes = 0\n    for number in range(start, stop):\n        if is_prime(number):\n            primes += 1\n            logger.write(f'{worker_id}-\u003e{worker_id} {number} is prime')\n    logger.write(f'{worker_id}-\u003e{worker_id} processing complete')\n    return primes\n\ndef main(number):\n    step = int(number / CONCURRENCY)\n    print(f\"Distributing {int(number / step)} ranges across {CONCURRENCY} workers running concurrently\")\n    iterable = [(index, index + step) for index in range(0, number, step)]\n    lookup = [':'.join(map(str, item)) for item in iterable]\n    # print to screen with lines context\n    results = pool_map(count_primes, iterable, context=Lines(lookup=lookup))\n    return sum(results.get())\n\nif __name__ == '__main__':\n    start = time.perf_counter()\n    number = 100_000\n    result = main(number)\n    stop = time.perf_counter()\n    print(f\"Finished in {round(stop - start, 2)} seconds\\nTotal number of primes between 0-{number}: {result}\")\n```\n\n\u003c/details\u003e\n\n![example4](https://raw.githubusercontent.com/soda480/list2term/main/docs/images/example4.gif)\n\n### Displaying messages from threads - [example5](https://github.com/soda480/list2term/blob/main/examples/example5.py)\n\n\u003cdetails\u003e\u003csummary\u003eCode\u003c/summary\u003e\n\n```Python\nimport time\nimport random\nimport threading\nfrom faker import Faker\nfrom concurrent.futures import ThreadPoolExecutor\nfrom list2term import Lines\n\ndef process_item(item, lines):\n    thread_name = threading.current_thread().name\n    lines.write(f'{Faker().name()} processed item {item}', line_id=thread_name)\n    seconds = random.uniform(.04, .3)\n    time.sleep(seconds)\n    return seconds\n\ndef main():\n    items = 500\n    num_threads = 10\n    with ThreadPoolExecutor(max_workers=num_threads, thread_name_prefix='thread') as executor:\n        lookup = [f'thread_{index}' for index in range(num_threads)]\n        with Lines(lookup=lookup) as lines:\n            futures = [executor.submit(process_item, item, lines) for item in range(items)]\n            return [future.result() for future in futures]\n\nif __name__ == \"__main__\":\n    main()\n```\n\n\u003c/details\u003e\n\n![example5](https://raw.githubusercontent.com/soda480/list2term/main/docs/images/example5.gif)\n\n\n### Other examples\n\nA Conway [Game-Of-Life](https://github.com/soda480/game-of-life) implementation that uses `list2term` to display game to the terminal.\n\n\n## Caveats \u0026 Notes\n\n* Best for small to medium lists — `list2term` is optimized for relatively compact lists (e.g. dozens to low hundreds of lines). Very large lists (\u003e thousands) may overwhelm the terminal.\n\n* Printable elements — items must be convertible to str.\n\n* Non-TTY fallback — if the terminal output is not a TTY (e.g. piped to a file), interactive updates are disabled automatically.\n\n* Worker message format — when using concurrency, messages must either follow the pattern `\"{identifier}-\u003e{message}\"` so that `Lines.write()` can route updates to the correct line. Or pass in `lines_id` argument to `Lines.write()`.\n\n\n## Development\n\nClone the repository and ensure the latest version of Docker is installed on your development server.\n\nBuild the Docker image:\n```sh\ndocker image build \\\n-t list2term:latest .\n```\n\nRun the Docker container:\n```sh\ndocker container run \\\n--rm \\\n-it \\\n-v $PWD:/code \\\nlist2term:latest \\\nbash\n```\n\nExecute the dev pipeline:\n```sh\nmake dev\n```","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoda480%2Flist2term","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoda480%2Flist2term","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoda480%2Flist2term/lists"}