[{"content":"Hi there. My name is Navdeep and I\u0026rsquo;m an engineering graduate and early stage engineer from India.\nGet in touch\n","permalink":"https://inverted-tree.pages.dev/about/","summary":"\u003cp\u003eHi there. My name is Navdeep and I\u0026rsquo;m an engineering graduate and early stage engineer from India.\u003c/p\u003e\n\u003cp\u003e\u003c!-- raw HTML omitted --\u003eGet in touch\u003c!-- raw HTML omitted --\u003e\u003c/p\u003e","title":"About"},{"content":"Table of Contents Introduction Main Concepts asgi Websockets Channel Implementation Chat Testing Final Thoughts This tutorial is for django developers who are comfortable with the primitives and want to build real time applications using django + channels. We\u0026rsquo;ll be building a minimal intercom clone(backend only) to simulate chat support provided to client websites who embed our widget for website visitors as illustrated\nIntroduction Real time systems differ from traditional systems by the constraint of response time - typically they need to respond to changes in state immediately or within a guaranteed time frame, often dictated by the system\u0026rsquo;s operation requirements. Unlike traditional request-response systems, real time systems prioritise timely and predictable execution, often at the expense of throughput or flexibility. For ex in a ticket booking website like Ticketmaster, the typical request-response flow comprises of fetching all the available tickets for the user\u0026rsquo;s search criteria, booking them etc, all of which can tolerate brief delays. Contrast it with a real time system like a trading app where trades have to be executed immediately as soon as a request is made and execution time is a governing factor in the quality and success of this service.\nFor this tutorial, we\u0026rsquo;ll build a very basic intercom clone backend that allows website admins to talk to the visitors. At the end I hope you will have a solid understanding of how to mold django to handle real time services on top of a traditional request-response architecture. Let\u0026rsquo;s dive in by setting up a basic django project-\nWe have initialized the chat app which will take care of our real time chatting functionality using django channels. Let\u0026rsquo;s go ahead and install the requisite packages using uv\nuv add channels[daphne]\nBut what is this package anyway and how does it help implement real time functionality? Let\u0026rsquo;s discuss that after visiting some important concepts-\nConcepts asgi Firstly, we need a new webserver for handling the websocket connections since traditional wsgi servers like gunicorn can\u0026rsquo;t. We\u0026rsquo;ll add daphne which is an excellent asgi server to our INSTALLED_APPS and it takes over the runserver command enabling websocket connections / async views in dev mode. Also, asgi servers can handle both http and ws protocols so we won\u0026rsquo;t need separate servers which is neat (ASGI is a standard for Python asynchronous web apps and servers to communicate with each other, and positioned as an asynchronous successor to WSGI. It can handle both synchronous / asynchronous views along with the websockets protocol. You can read more at asgi).\nINSTALLED_APPS = [ \u0026#34;daphne\u0026#34;, \u0026#34;chat\u0026#34;, \u0026#34;django.contrib.admin\u0026#34;, \u0026#34;django.contrib.auth\u0026#34;, \u0026#34;django.contrib.contenttypes\u0026#34;, \u0026#34;django.contrib.sessions\u0026#34;, \u0026#34;django.contrib.messages\u0026#34;, \u0026#34;django.contrib.staticfiles\u0026#34;, ] Let\u0026rsquo;s run the dev server with python manage.py runserver\nWatching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). April 09, 2025 - 15:22:58 Django version 4.2, using settings \u0026#39;core.settings\u0026#39; Starting ASGI/Daphne version 4.1.2 development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. As you can see now daphne is listening for http/ websocket connections and supports async request handling in dev mode itself.\nWebsockets Websockets are the underlying OS constructs that allow this bi directional flow of information via long running connections. These are built on top of operating system sockets and leveraged to allow communication between different processes and machines.\nA websocket connection (ws://) starts off as a normal http connection which is then upgraded to a bidirectional long running connection. You can see this upgrade in the server logs, along with websocket connection events-\nNote: For production you would use the wss:// protocol which is secure, TLS encrypted counterpart to ws:// (like HTTPS is to HTTP).\nChannel In computing, a channel is a model for interprocess communication and synchronization via message passing. A message may be sent over a channel, and another process or thread is able to receive messages sent over a channel it has a reference to, as a stream. Different implementations of channels may be buffered or not, and either synchronous or asynchronous.^1^\nImplementation Let\u0026rsquo;s start implementing the chat application as we would any other django application with its own routes / models/ views etc. Primarily there are two kinds of users - agents and visitors. (We also have in built superusers that can overlook the entire system with django admin if needed). In the most minimal implementation, the models can look like the follows-\n#chat/models.py from django.contrib.auth.models import User from django.db import models class Visitor(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = models.EmailField(blank=True, null=True) name = models.CharField(max_length=100, blank=True, null=True) first_seen = models.DateTimeField(auto_now_add=True) last_seen = models.DateTimeField(auto_now=True) def __str__(self): return (f\u0026#34;Visitor: {self.name or self.email or self.id}\u0026#34;) class Agent(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name=\u0026#39;agent_profile\u0026#39;) is_online = models.BooleanField(default=False) display_name = models.CharField(max_length=100, blank=True, null=True) def __str__(self): return f\u0026#34;Agent: {self.user.username}\u0026#34; @property def name(self): return self.display_name or self.user.get_full_name() or self.user.username We can also have the Conversation and Message models as reference to understand how channels / async route handlers may have to deal with data persistence as part of business logic.\nWith the models out of the way, we also have to make routing arrangements for the chatting functionality. As mentioned previously, the websocket protocol actually starts off as http but is then upgraded to ws protocol which is a continuous running connection so as to avoid the (TCP) overhead of opening a new connection for each request. To that end, we need to configure our asgi application to handle both http and ws requests. Django channels provides just the needed methods to provision the same. Let\u0026rsquo;s modify our core/asgi.py:\n#core/asgi.py import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack import chat.routing os.environ.setdefault(\u0026#34;DJANGO_SETTINGS_MODULE\u0026#34;, \u0026#34;core.settings\u0026#34;) application = ProtocolTypeRouter( { \u0026#34;http\u0026#34;: get_asgi_application(), \u0026#34;websocket\u0026#34;: AuthMiddlewareStack(URLRouter(chat.routing.websocket_urlpatterns)), } ) Since django channels is tightly coupled with django (maintained by the same project as django web framework), it can be easily patch into django core functionality like auth, url routing etc out of the box. One example of this is evident in the code above where AuthMiddlewareStack actually takes care of the authentication using existing/familiar django primitives.\nBut what\u0026rsquo;s chat.routing? By definition, the chat routes should be separated from the REST endpoint routes because of the need for a different kind of connection. We\u0026rsquo;ll thus define them in chat/routing.py\n# chat/routing.py from django.urls import re_path from . import consumers websocket_urlpatterns = [ re_path( r\u0026#34;ws/chat/visitor/(?P\u0026lt;conversation_id\u0026gt;[^/]+)/$\u0026#34;, consumers.VisitorConsumer.as_asgi(), ), re_path( r\u0026#34;ws/chat/agent/(?P\u0026lt;conversation_id\u0026gt;[^/]+)/$\u0026#34;, consumers.AgentConsumer.as_asgi(), ), ] Note that websocket_urlpatterns is imported into core/asgi.py to create the composite application with ProtocolTypeRouter. Also note the use of re_path in place of path (due to a limitation in nested routes in the URLRouter).\nLet\u0026rsquo;s move on to implementing the core functionality of chats between different kinds of users. The python package channels provides the Channel interface that is the common interface between different consumers that push the relevant messages forward to whatever clients that are subscribed. Before we can use it, we have to set up the CHANNELS_LAYERS to our settings. For our example we can use the in memory channels store that exposes this message queue to us in order to listen to websocket events and transmit data. Here\u0026rsquo;s how it\u0026rsquo;s configured\n# settings.py CHANNEL_LAYERS = { \u0026#39;default\u0026#39;: { \u0026#39;BACKEND\u0026#39;: \u0026#39;channels.layers.InMemoryChannelLayer\u0026#39;, }, } Channels is basically an abstraction layer built on top of a pub/sub architecture - it keeps track of which clients are connected to the websocket and where to deliver messages.\nNote: InMemoryChannelLayer is only suitable for development or single-process deployments. For production, use RedisChannelLayer by setting \u0026ldquo;BACKEND\u0026rdquo;: \u0026ldquo;channels_redis.core.RedisChannelLayer\u0026rdquo; and configuring a Redis instance.\nChat To setup the chat we need to do more than a few things. Firstly, we need to create websocket endpoints for both agents and visitors.\nSo far we have the chat app with the resident models which we\u0026rsquo;ll use to persist the chat. One might argue that we don\u0026rsquo;t need sockets at all - chatting can be implemented with POST api requests to something like POST /conversation/message. Which is technically correct but it\u0026rsquo;s not real time - for every chat message a new TCP connection needs to be opened creating an overhead on top of the network + other delays and thus it\u0026rsquo;s not real time, at least relative to websockets. On the other hand, channels uses websockets to open up long running, bi directional connections to clients and thus real time chat is possible on the persistent connection.\nSince this app is decidedly real time, we must now talk in terms of events - there are some events that are going to be common to all kinds of real time functionality users. For every stakeholder who wants to chat - they have to connect to the system and disconnect when desired. And just like that, we have discussed 2 of the 3 main events that constitute a websocket connection - connect and disconnect. The final event is receive which basically is a kind of a super event that takes care of all the business / domain logic - you could send different kinds of messages (in our case they are - text messages between admin and visitor, but they could be of other types like typing indicators etc). To facilitate this, django channels provides abstraction called Consumers. In the future you might want to add read receipts, auto reply etc. - all of which will be built on this foundation of consumers.receive function.\nConsumers Consumers are the real time counterpart to django REST api views. Just like django views map to urls then patched into urlpatterns, so do consumers with their own urlpatterns as seen in routing.py.\nA chat consumer essentially wraps up the websocket api to provide an interface for when clients connect, disconnect and send messages. In this example, in the receive function we get the raw text string, convert it to json and send it back to the connected client (this is the idiomatic way and sending it back to the client is actually optional).\nHere\u0026rsquo;s what a basic synchronous Consumer interface looks like -\nclass ChatConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() async def disconnect(self, close_code): # do cleanup or something pass async def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json[\u0026#34;message\u0026#34;] await self.send(text_data=json.dumps({\u0026#34;message\u0026#34;: message})) Let\u0026rsquo;s look at the desired chatting flow in detail-\nClient generates a conversation id and connects to websocket consumer; waits for agent A group is created with this conversation id and it is added to the pool of available conversation groups Agent chooses a conversation id from a pool of ids Agent joins the same conversation via the room via its own endpoint Every client that\u0026rsquo;s connected to websocket server is served by its own instance of the consumer. Since each connection maintains its own state and coroutine, it could lead to high memory usage under load, which should be considered in production.\nLet\u0026rsquo;s start implementing the chat consumers one by one-\nclass VisitorConsumer(AsyncWebsocketConsumer): async def connect(self): # Extract the conversation id from the routing url self.conversation_id = self.scope[\u0026#34;url_route\u0026#34;][\u0026#34;kwargs\u0026#34;][\u0026#34;conversation_id\u0026#34;] # Create and join a group based on the conversation name for agents to join self.conversation_group_name = f\u0026#34;chat_{self.conversation_id}\u0026#34; await self.channel_layer.group_add( self.conversation_group_name, self.channel_name ) await self.accept() # add to conversation pool add_to_available_pool(self.conversation_id) async def disconnect(self, close_code): # Leave room group await self.channel_layer.group_discard( self.conversation_group_name, self.channel_name ) The code follows the exact flow described above. Here are the similar methods for AgentConsumer:\nclass AgentConsumer(AsyncWebsocketConsumer): async def connect(self): self.conversation_id = self.scope[\u0026#34;url_route\u0026#34;][\u0026#34;kwargs\u0026#34;][\u0026#34;conversation_id\u0026#34;] self.conversation_group_name = f\u0026#34;chat_{self.conversation_id}\u0026#34; # Join room group await self.channel_layer.group_add( self.conversation_group_name, self.channel_name ) await self.accept() # remove this conversation from the pool remove_from_available_pool(self.conversation_id) async def disconnect(self, close_code): # Leave room group await self.channel_layer.group_discard( self.conversation_group_name, self.channel_name ) Note that once an agent finishes connecting, the conversation id is removed from the list of ids in the availability pool. The disconnect method is idiomatic and just removes the client from the group. Eventually if both the visitor / agent clients disconnect from a channel group, the group practically vanishes (no manual cleanup needed).\nMessaging The only remaining method to implement is the receive method where most of the chatting magic happens. Let\u0026rsquo;s implement receive for VisitorConsumer:\nclass VisitorConsumer(AsyncWebsocketConsumer): # .. connect, disconnect .. # async def receive(self, text_data): \u0026#34;\u0026#34;\u0026#34; Handles incoming JSON messages. Expects: { \u0026#34;type\u0026#34;: \u0026#34;chat.message\u0026#34; | \u0026#34;typing\u0026#34; | \u0026#34;identify\u0026#34; | ... \u0026#34;content\u0026#34;: { ... } } \u0026#34;\u0026#34;\u0026#34; content = json.loads(text_data) msg_type = content.get(\u0026#34;type\u0026#34;) msg_content = content.get(\u0026#34;content\u0026#34;, {}) if msg_type == \u0026#34;chat.message\u0026#34;: await self.channel_layer.group_send( self.conversation_group_name, { \u0026#34;type\u0026#34;: \u0026#34;chat_message\u0026#34;, \u0026#34;sender\u0026#34;: \u0026#34;visitor\u0026#34;, \u0026#34;message\u0026#34;: msg_content.get(\u0026#34;message\u0026#34;, \u0026#34;\u0026#34;), }, ) elif msg_type == \u0026#34;typing\u0026#34;: await self.channel_layer.group_send( self.conversation_group_name, {\u0026#34;type\u0026#34;: \u0026#34;typing_event\u0026#34;, \u0026#34;sender\u0026#34;: \u0026#34;visitor\u0026#34;}, ) else: logger.warning(f\u0026#34;Unknown message type received: {msg_type}\u0026#34;) # Handlers for group messages async def chat_message(self, event): await self.send( text_data=json.dumps( { \u0026#34;type\u0026#34;: \u0026#34;chat.message\u0026#34;, \u0026#34;sender\u0026#34;: event[\u0026#34;sender\u0026#34;], \u0026#34;message\u0026#34;: event[\u0026#34;message\u0026#34;], } ) ) async def typing_event(self, event): await self.send( text_data=json.dumps({\u0026#34;type\u0026#34;: \u0026#34;typing\u0026#34;, \u0026#34;sender\u0026#34;: event[\u0026#34;sender\u0026#34;]}) ) This method basically receives messages that aren\u0026rsquo;t connect/disconnect events, which is why this method can act as a router to handle different kinds of messages. As mentioned already, the messages can be of any type as per the business logic requirements - for ex. chat messages, typing indicator messages (indicating the user is typing), identification etc. At its core, the receive method just parses the message string, identifies the type of message and handles it accordingly. In the case of chat messages, the event is published to the relevant group with group_send, where the channels layer makes sure to deliver it to all the clients connected to the group. The low level interface provided for this is the self.send() function that forwards the message to each connected client (If there were no groups, the message would just go to the client connected to the current instance of the consumer). A similar implementation is provided for Agent consumer that handles same message types, only with the sender key set to agent in the receive method.\nHere\u0026rsquo; the chat message flow for reference:\nNote how a message that originates in the visitor finds its way back to them (called echo message). This is a design pattern and can be disabled by clearing the self.send() call in VisitorConsumer. Also, channels automatically looks for a method named my_method for a message of type my.method published to the group, and that\u0026rsquo;s how chat.message gets routed to chat_message method.\nYou can also call business logic methods, like saving a chat message to database, assigning to Conversations, right here after handling the message flow.\nAnd that wraps up the implementation. Let\u0026rsquo;s test everything!\nTesting Let\u0026rsquo;s test our backend by simulating client connections to our websocket server interface. I\u0026rsquo;m going to use the websockets package to simulate browser clients in python to see everything in action (with the added benefit of not leaving the terminal for the browser). Here\u0026rsquo;s the code for a basic client that simulates clients for both agents and visitors and haves them communicate on a hard coded conversation id.\n# chat_client.py import asyncio import websockets import json import sys # Constants BASE_WS_URL = \u0026#34;ws://localhost:8000/ws/chat\u0026#34; CONVERSATION_ID = \u0026#34;test_conv_id\u0026#34; # Hardcoded conversation ID def make_payload(event_type, content=None): return json.dumps({\u0026#34;type\u0026#34;: event_type, \u0026#34;content\u0026#34;: content or \u0026#34;\u0026#34;}) async def chat_loop(role: str): ws_url = f\u0026#34;{BASE_WS_URL}/{role}/{CONVERSATION_ID}/\u0026#34; async with websockets.connect(ws_url) as ws: print(f\u0026#34;[{role.upper()}] connected to {ws_url}\u0026#34;) async def send_loop(): while True: msg = await asyncio.get_event_loop().run_in_executor( None, sys.stdin.readline ) msg = msg.strip() if msg: await ws.send(make_payload(\u0026#34;chat.message\u0026#34;, {\u0026#34;message\u0026#34;: msg})) async def receive_loop(): async for message in ws: data = json.loads(message) print(f\u0026#34;[RECEIVED] {data}\u0026#34;) await asyncio.gather(send_loop(), receive_loop()) if __name__ == \u0026#34;__main__\u0026#34;: import argparse parser = argparse.ArgumentParser(description=\u0026#34;Python WebSocket Chat Client\u0026#34;) parser.add_argument( \u0026#34;--role\u0026#34;, choices=[\u0026#34;visitor\u0026#34;, \u0026#34;agent\u0026#34;], required=True, help=\u0026#34;Role to connect as\u0026#34; ) args = parser.parse_args() asyncio.run(chat_loop(args.role)) In order to test it out, start the django server with python manage.py runserver and then connect as a visitor using python chat_client.py --role visitor and as an agent using python chat_client.py --role agent and type the messages in both terminals to see the flow in action.\nFinal Thoughts In this tutorial, you:\nLearned what ASGI, websockets, and channels are. Set up daphne to run an ASGI-compatible Django app. Configured websocket routes using Django Channels. Created minimal consumers for real-time bi-directional messaging. Used channel_layer to broadcast messages to participants in a shared conversation group. This real-time architecture provides a strong base for any live feature – from intercoms, to multiplayer games, to collaborative editors.\nWebsockets are widely supported by browsers and are a mature technology so much so that anyone can build resilient real time systems and scale them to a large number of concurrent connections. For further reading, you could go through the official tutorial and read the official docs. You can find the full source code of the application here.\nhttps://en.wikipedia.org/wiki/Channel_(programming) ","permalink":"https://inverted-tree.pages.dev/posts/using-django-channels-to-build-real-time-applications/","summary":"\u003ch2 id=\"table-of-contents\"\u003eTable of Contents\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#introduction\"\u003eIntroduction\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#main-concepts\"\u003eMain Concepts\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#asgi\"\u003easgi\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#websockets\"\u003eWebsockets\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#channel\"\u003eChannel\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#implementation\"\u003eImplementation\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#chat\"\u003eChat\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#testing\"\u003eTesting\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#final-thoughts\"\u003eFinal Thoughts\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis tutorial is for django developers who are comfortable with the primitives and want to build real time applications using django + channels. We\u0026rsquo;ll be building a minimal intercom clone(backend only) to simulate chat support provided to client websites who embed our widget for website visitors as illustrated\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Schematic of channels-chat\" loading=\"lazy\" src=\"https://pub-610e38254acb47cea52a77d9d4b58499.r2.dev/blog/d4e16965-18.webp\"\u003e\u003c/p\u003e\n\u003ch3 id=\"introduction\"\u003eIntroduction\u003c/h3\u003e\n\u003cp\u003eReal time systems differ from traditional systems by the constraint of response time - typically they need to respond to changes in state immediately or within a guaranteed time frame, often dictated by the system\u0026rsquo;s operation requirements. Unlike traditional request-response systems, real time systems prioritise timely and predictable execution, often at the expense of throughput or flexibility. For ex in a ticket booking website like Ticketmaster, the typical request-response flow comprises of fetching all the available tickets for the user\u0026rsquo;s search criteria, booking them etc, all of which can tolerate brief delays. Contrast it with a real time system like a trading app where trades have to be executed immediately as soon as a request is made and execution time is a governing factor in the quality and success of this service.\u003c/p\u003e","title":"Using django channels to build real time applications"},{"content":"Note: This post was not reviewed/edited/written by any LLM/\u0026ldquo;AI\u0026rdquo;.\nPrelude Prior to joining Widgetic I was a Java/Android developer turned Node.js / backend developer, had worked at a Swiss cryptocurrency startup as a backend/desktop developer, and had built more than a few MVPs in the years prior(mobile apps, electron apps etc but not a full stack application). At this point I was relatively inexperienced with web technologies and wanted to work at a product company to understand how these skills/eager learning attitude translated into the real world, especially in the challenging world of business/startups. I coveted more learning and responsibility, so when an opportunity came along to work at Widgetic, I was more than happy jump on it!\nI joined Widgetic in 2019 as a backend/devops developer in a team of 3 - the other two team members - an iOS turned frontend/javascript developer and a product manager. The team was fully remote with members in India(myself), Romania, and Spain. To protect their privacy, I have referred to my colleagues with their initials - AP(product manager) and AN.\nIntroduction Widgetic emerged at at a web development agency in Iasi, Romania after the they received multiple requests by different clients for similar kinds of websites sections like image carousels, image hotspots, lightweight audio players, video players etc. They decided to call them widgets. Right from the get go, they understood that website builder platforms like Wix, Weebly, Shopify etc were open to third party \u0026ldquo;plugins\u0026rdquo; where the widgets could be sold for monthly/yearly subscriptions. With the semblance of a product emerging, the web agency was wound down and was left with 3 people - a product lead, a frontend developer and myself, a backend+devops engineer (at this point I only knew node.js / hapi.js / REST/ postgresql /linux and that was enough as far as requirements went for the role). Widgets were served as iframes to client websites and integrations with different website builders provided a way to customize the widgets and embed them directly onto the client platform generated websites via native/\u0026ldquo;raw\u0026rdquo; web builder interfaces.\nTech Debt: Origins and Lifecycle Nothing can be said about Widgetic without talking about the technical debt. As alluded to earlier, the antecedent to Widgetic was this web agency that decided to abstract out the frequent requests for same sort of functional websites into pluggable \u0026ldquo;widgets\u0026rdquo;. Since this was a relatively successful agency, they had a lot of interns coming in; who were eventually assigned the work of building these widgets, as a way of contributing and learning new frameworks. The interns were apparently given developer freedom to choose a framework, and interestingly, eventually all the codebases started diverging on framework/ library choice with the prevailing technologies of the time(circa 2014 iirc), including-\ncoffeescript backbone.js spine.js svelte 1,2 angular 1, 2 react As the widgets started materializing the agency owners were quick to start integrating with website builder platforms. However, these integrations were developed haphazardly, to reduce the time to GTM(as I reflected on this later, this was great choice business wise because they reached ramen profitability quickly). There was no architectural planning - interns/employees were building integrations and a dedicated veteran of the agency was assigned to build the \u0026ldquo;GCD\u0026rdquo; of the widget server, a massive monolith. Now all of this is far from ideal world of careful planning, architecting, securing and provisioning but this wasn\u0026rsquo;t a complete disaster too - things worked gracefully most of the time(we didn\u0026rsquo;t really measure the uptime but there was no significant downtitme either). Also, a dedicated app was created with an editor to help non integration users to create and configure widget specifications directly and then these be embedded on any html website from the embed code generated there. So, the backend integrations were created to ease the distribution on the web builder / ecommerce platforms, but with different node.js based micro applications that would basically implement oauth and other necessary functionality to bring these widgets to sale. Here\u0026rsquo;s what the comprehensive technical stack comprised of when I joined-\nnode.js + express.js/ hapi.js/ koa.js php+symfony mongodb 2.4.x/sqlite Vagrant based dev env Varnish cache digitalocean Unfortunately, the lead developer who had created the monolith backend left the agency after creating the meat of the system with proper tests and some decoupled documentation. This is the exact point in time that I joined. I had no idea what I was signing up for, never even had heard the term techincal debt at that point because of relative inexperience(only ~2 years until now, very little exposure to production load bearing systems).\nWith the stage set for an epic showdown between developers and technical debt by the way of missing documentation, unpredictable implementations, stack fragmentation, we were ready to bring the annual revenue (already in high 5 digits) to the next meaningful milestone and not perish under the weight of instability and frequent crashes, which were a big big problem at the time and heavily detrimental to the our clients\u0026rsquo; experience(mostly small business owners on the web). However, our clients loved us because the widgets were designed beautifully(by AP) and were highly customizable. At this point I think I should point out that the number of end users was in the mid 6 figures on account of compounding - our clients who installed widgets did so on their own portfolio / shop websites that were their storefronts, where thousands of their own customers would visit and make purchases.\nTech debt is a financial problem The system was spread across multiple services across even older machines that hadn\u0026rsquo;t been updated in years. As mentioned previously, there were different frameworks, different versions, different languages using just a handful of common web technologies working in tandem to produce a working but unreliable system. An unexpected benefit of this mess was that the problem of triaging just wasn\u0026rsquo;t there- once you\u0026rsquo;re done with your assigned tickets, you can find something to work on easily. Although I was working in a hierarchy, and some issues were more pressing than the others, the number of issues(not catastrophic, most of them anti patterns, fragile code, incoherent architecture, the absence of an architecture) were too many. These, on top of the feature requests we would keep getting from our customers, needed to be triaged, and were slowly passed down to us, but eventually we did keep up.\nThis was also the time I had the \u0026ldquo;breakthrough idea\u0026rdquo; of \u0026ldquo;just rewrite everything\u0026rdquo; several times, but the finances just didn\u0026rsquo;t permit it - or maybe because I was a developer with limited production experience, I couldn\u0026rsquo;t rewrite the integrations quickly enough and still achieve our day to day targets. Just after a couple of months, I learned to tolerate the chaos and just make do with what I had. This was fun in its own way(just ignore the code that doesn\u0026rsquo;t work and test manually) for a while but becomes dull once you just have to keep doing this without a discernible change in system quality (of course you\u0026rsquo;re slowly imparting more stability to the system but the effects won\u0026rsquo;t be readily visible). For me this period lasted about 6 months, after which we achieved a baseline stability as opposed to the constant crashes that were normal before/ immediately after I joined. But that\u0026rsquo;s the thing about technical debt\u0026ndash; it keeps piling up and you\u0026rsquo;re not gonna completely get rid of it all, ever, just that it\u0026rsquo;ll become manageable at some point.\nEven if you don\u0026rsquo;t have technical debt yet, you still have code which will go through semantic drift and thus become ready to refactor. Managing expectations is prime, but it really doesn\u0026rsquo;t hurt when you\u0026rsquo;re making bank and the system isn\u0026rsquo;t breaking down frequently to the point that it\u0026rsquo;s unusable. At one point, you can start making enough revenue to hire further talent to take care of the technical debt for you and not go under. Until that time, one needs to learn to live with it.\nThere\u0026rsquo;s no such thing as too much documentation Widgetic had a love/hate relationship with documentation. Although my colleagues relied heavily on documentation that was created by themselves to effectively discharge their own responsibilities, the codebases were glaringly missing any and all documentation, especially tests.\nThis is one of the first things I learned - documentation, even if little, is really useful in any form/shape. When I joined, we had (non code)documentation in several places - READMEs, dedicated doc repositories like notion/coda, API documentation etc.; all scattered without a structure, some of it served as a database of quick recipes for common problems around the architecture and how to avert them for the foreseeable future. But a lack of documentation also poses risks- you might not even know what\u0026rsquo;s missing from the system, especially one that\u0026rsquo;s been developed chaotically and without a process to reach the market quickly.\nAs a naive programmer, I had the tendency to avoid documenting. But as anyone with a semblance of a skill in programming will tell you, this is the exact thought process you need to avoid. So I eventually learned to like documenting after a few hard lessons, and the team was really helpful in this aspect - they documented everything vigorously despite the glaring lack in the codebases as mentioned previously. In the end, it left such a deep impact on me that I started thinking in documentation terms, and this would go on to help me enormously in my future projects(think of what you can do by over documenting if you plan to use high context length llms).\nA lack of documentation makes a system unpredictable, and although all systems fail - good ones fail predictably.\nI was amazed to experience that even a system as unstable as widgetic was when I joined can be handled pretty well, with all its bugs / quirks, by just writing about them to have a quick reference if things break / are close to breaking. Apparently, I switched polarities vis-a-vis documentation and started every mortal effort to document as much as possible, which I believe was one of the biggest improvements to my development workflow. Writing clarifies your mind, and eventually you start writing better code with a clearer head. Slowly I also realized the truth of another adage in the programming world-\nDocumentation is a love letter you write to your future self.\nSmall teams make big dents After the success of Midjourney ($200 million revenue, 40 people team in 2023), Cursor ($100 million revenue, \u0026lt; 20 people team), it might seem obvious that hyper-productive small teams are the future of startups with their default lean mindset and lean execution. But back at Widgetic, we took this for granted. Small teams are efficient by definition - there is no communication overhead, no complex management required and they can run extremely lean financially(as we were doing at the time). When Covid hit in 2020- like everything else on the internet, the traffic to our clients\u0026rsquo; websites increased organically, so much so that our revenue rose ~40% and we were overjoyed. While this was a remarkable milestone in itself, all of the work we had put in the previous months hardening and cleaning the architecture / infrastructure paid off as our system were able to handle the gradual spike in traffic smoothly and had baseline security measures at least(no one could hack us just for the heck of it).\nAt this point I find myself obliged to mention some of the \u0026ldquo;issues\u0026rdquo; we worked on that led to measurable results later, much to our delight-\nUpgrade old ubuntu 14.04 running instances to the latest LTS versions Tighten the firewall rules Create VPCs for tighter security within attached resources Pump in more logging so as to make the system more predictable Improve and augment documentation all around Move related services to a single VPS Create and implement a firewall policy Write tests wherever possible Another unforeseen benefit of a small team - AP would set us developers to resolve customer support tickets which would put us in touch directly with our customers(not sure if this was deliberate or because he was shorthanded trying to actively raise funds / other things that PMs do) - and it was a uniquely beautiful experience. If nothing else, it reinforces your mission and provides meaning to the work you do on a daily basis since you can see it directly affecting someone\u0026rsquo;s livelihood.\nDon\u0026rsquo;t fix something that\u0026rsquo;s not broken So far I\u0026rsquo;ve discussed in length how fixing bugs and patching etc was such a fun experience for me, yet not everything was flowery with our development process. There were some lessons we learned the hard way, trying to replace software / versions which merited different approaches-\nmongodb When I inherited the system, it comprised of a mongodb database at version 2.4.x. This was way past its official support timeline but didn\u0026rsquo;t really matter to us because all the content we store on our clients\u0026rsquo; behalf was public anyway- except there was no UI client that worked with this version that let us monitor the data. The REST API backend that was doing the heavy lifting was built with php / symfony which was documentation less and test less (there were tests sprinkled here and there but nothing that could act as a cohesive documentation with flow that could be useful to maintainers).\nLong story short, we wasted a lot of time trying to migrate to mongo 4.x which was the supported version at the time, but we never could muster the confidence because of the lack of tests to prevent regression. Eventually we resorted to hacking together a really old version of robomongo to monitor our database. And it was the right decision all along, in retrospect.\nvagrant Docker had made footfall and we tried to replace vagrant with the same but with little to no success since nothing really worked and we did not have much time to spend on it anyway. In retrospect, we did waste precious time trying to stay up to date with the latest and greatest technologies - docker was noticeably faster than vagrant once setup (by definition) and that speed attracted us but this is the wrong kind of optimization, as we realized later.\nRemote work works One unexpected side effect of this strife with technical debt, and providing great customer experience and implementing customer requests was that the team was really efficient. Now it could be because the original founders were highly motivated and I have a passion for programming, with an obsession with learning by doing, we never felt like remote work could be a detriment to progress. And it wasn\u0026rsquo;t. We the developers, leveraged Zoom meetings to jump on calls and debug issues together and connect with each other when stuck. Pair programming on these calls, we would end up solving multiple issues in a few hours, although the number of issues seemed to be countless at the time. Of course doubts are thrown at the efficacy of remote work, but for us, having a distributed team working in 3 timezones, it was never a factor. There was usual friction in the team about outcomes, the direction etc which are the hallmarks of every team, but in hindsight, remote work never limited us.\nChoose boring technology At the time I had the opportunity to read about choosing boring technology. The article was a quick hit for me (it basically argues that you have but a finite number of innovation tokens and if you spend them all on the shiny new way of doing things, you\u0026rsquo;re left with very few to spend on the actual product that you\u0026rsquo;re building which might need those innovation tokens). This lesson hit home with me and taught me further invaluable lessons. In the same vein, the most stark and valuable lesson came from our push to expand Widgetic to WordPress. I had never had experience with complex software development lifecycles, and a decision was made to target WordPress to deliver our now (relatively stable) widgets, as opposed to other dedicated website builders like Squarespace etc which were more attuned to our product with their interactive editors (Wordpress with its far reaching audience seemed like the ideal choice for expansion) but there was an issue- they were slowly transitioning into js plugin based gutenberg editor which was far from stable at the time. Despite my (late) misgivings (which ultimately led to my resignation) it was decided to target the gutenberg editor at Wordpress to supply the widgets. This is a crucial business decision because of our scarce resources and need for growth for survival and possible fundraising. In the broader market, we had competitors who were doing much better revenue wise without a better product. A team of two engineers with a product that was a relatively complex potpourri of frontend and backend features, neither of the developers were ready/confident for a claim on taking full ownership of the same- I lacked experience on the frontend part of the stack(I had gravitated towards react by now as opposed to AN who was mostly a svelte developer), and AN didn\u0026rsquo;t have much experience with complex backend/full stack architectures.\nThese two seem to be complementary skills, and they were yet after some serious effort put into developing the wordpress integration, while maintaining bandwidth for obscure bugfixes / feature requests on old widgets with legacy codebases / almost eol integrations- we weren\u0026rsquo;t able to implement a reasonably good wordpress integration, even after a considerable span of time. Trying to get to the bottom of it - I think gutenberg kept breaking/APIs not stable, and sometimes our system would break, because under no circumstance would I assert that Widgetic was v1 by that time. This led to friction in the team- I was really excited to be working with react on this but was violating my own principle of not trusting pre v1.0 software (gutenberg at the time). The continued stress from this eventually broke the team after no meaningful result came to be after a long time. Only after a few months of hindsight did I realize that the problem was this obvious - the established players in the website builder markets with their stable and feature rich editors were more suited to our product than the relatively up and coming gutenberg, despite WordPress\u0026rsquo;s formidable reach.\nConclusion It\u0026rsquo;s always sad to leave a team that you\u0026rsquo;ve worked closely with, solved hard problems with, and learned from, for years. But in my experience, I\u0026rsquo;ve found failure to be far more truthful than success, so I consider it a great privilege to have been able to collaborate so effectively on a shared vision with my equally if not more passionate counterparts.\nMy 2 years and 4 months at Widgetic were a rollercoaster ride with innumerable challenges, overcomes, and lessons that would have taken me way more time learn otherwise at a traditional corporation by all indications. I joined as a 2yoe developer and was eventually promoted to cofounder. I was constantly pushing multiple commits a day of quality code, bug fixes, patches, new features etc. In retrospect, it was a stepping stone for me to level up my skills by shipping consistently and solving a myriad of problems at each end of the stack. Since I learn by doing, I came into contact with some germinal web development concepts viz. oauth, iframes, postmessage api, document databases, managing and securing servers in production via practice and a feel for what a real world product looks like. Although I don\u0026rsquo;t place considerable weights on titles, some devs argue that you cannot be considered a senior developer without having worked on a legacy system, I definitely would recommend that every developer work on at least one legacy project, although not as extreme as Widgetic.\nPostlogue I wrote this in 2025 and it stayed in my drafts for a year before being published this year. A lot has changed in this time, but hopefully someone gleans something valuable out of my experience. Happy coding!\n","permalink":"https://inverted-tree.pages.dev/posts/lessons-from-founding-tech-debt-ridden-startup/","summary":"\u003cp\u003eNote: This post was not reviewed/edited/written by any LLM/\u0026ldquo;AI\u0026rdquo;.\u003c/p\u003e\n\u003ch3 id=\"prelude\"\u003ePrelude\u003c/h3\u003e\n\u003cp\u003ePrior to joining Widgetic I was a Java/Android developer turned Node.js / backend developer, had worked at a Swiss cryptocurrency startup as a backend/desktop developer, and had built more than a few MVPs in the years prior(mobile apps, electron apps etc but not a full stack application). At this point I was relatively inexperienced with web technologies and wanted to work at a product company to understand how these skills/eager learning attitude translated into the real world, especially in the challenging world of business/startups. I coveted more learning and responsibility, so when an opportunity came along to work at Widgetic, I was more than happy jump on it!\u003c/p\u003e","title":"Lessons from late founding a tech debt ridden online startup"},{"content":"\ntl;dr: Neovim has come a long way from an experimental vim fork that it was at inception and is veritably, in my opinion, the hotbed for open source editor innovation currently. However customising it can feel daunting especially since things are changing rapidly.\nHaving lived through vi vs emacs I was a full on emacs person jumping through my ocaml code with the venerable tuareg mode which is loved and maintained to this day. At that time, vim support for ocaml didn\u0026rsquo;t exist, thus emacs came out on top for me.\nAfter, I tried to setup emacs for javascript / css / web development and everything was as smooth as ever. However I got tired of the chord keybindings and I started using (evil/viper) mode and eventually switched to vim. Meanwhile vimscript was off putting for me on top of the immense configuration options that vim comes with. All of this was solved when lua was introduced as first class config language in neovim, and since then I\u0026rsquo;ve never looked back. At this point my editor config is quite stable and integral part of my workflow.\nPerfect is the enemy of good - Voltaire\nFor me, my text editor needs just the basic set up so that I can get productive with it from day one. For me they are\nLoad time First and foremost, the most off-putting thing about IDEs of yore(Eclipse) and IDEs of today(Jetbrains etc) is the load time. Although it\u0026rsquo;s very much subjective what is high, it can never match up to neovim\u0026rsquo;s \u0026lt;100ms that I get with minimum optimisation.\nThe max load time for me is 500ms which is still 1/3 of pycharm community edition(~1.5s) on my system.\nBig files Big files aka unwanted surprises. Imagine a text editor that can\u0026rsquo;t open text files. A lot of editors have failed this benchmark not too far back in the past. I recall Github\u0026rsquo;s now abandoned Atom that would crash for files bigger than 20MB.\nNot neovim though. I\u0026rsquo;ve tested and opened files as big as 1GB and everything in the editor stays stable although the load time could be significant (~10s).\nStability vim has traditionally been a stable software, and carrying this into neovim must not have been an easy task for neovim contributors and maintainers, especially when they were trying to improve upon the codebase to ease contributions. But they\u0026rsquo;ve done a great job by being laser focused on the core mission which was to ease of contribution. (Personally I\u0026rsquo;ve been waiting for this to be merged for a decade seems like but it only recently found its way into v0.11 after it was put on low priority since inception. Much discipline.) First class lua support can be a boon or a curse the way you look at it. For me it works really well as lua is an approachable little language that\u0026rsquo;s understood by tools I use all over (say awesomewm, hammerspoon,). One can even use lua as a scripting language with nginx to create dynamic responses.\nChecking all these boxes at once is rare but the team as quite deftly handled the transition. Because of this uber reliability and stability and ease of configuration, the community members have created a wide variety of distributions that decisively turn neovim to a full fledged ide.\nlazyvim Although there are many awesome and older config distributions like astrovim / nvchad, I prefer lazyvim. It\u0026rsquo;s a comprehensive plugin management system that gives you exactly what you need and can save hours of configuration hell. It\u0026rsquo;s highly approachable and my configuration builds on top of it. Props to folke for creating some awesome plugins and now this distro that he maintains with a passion.\nConfiguration One of the more controversial aspects of nvim, the achilles\u0026rsquo; heel of productivity, the bottomless pit that some people find themselves in. Particularly I didn\u0026rsquo;t think much about my time spent on configuration until it was already too late. Yet I\u0026rsquo;m glad to have found this balanced config such that the editor just gets out of the way. On top of the afore mentioned minimum features, my editor also does the following with Python/Web:\nTight integration with git with inbuilt mergetool Task runner (with overseer.nvim) Tightly integrated testing (with neotest) Debugger support Automatic annotations Opt in AI code completion support Inline markdown preview(with markview.nvim) And many other Check out my full config here\nConclusion The neovim community is a welcoming, dynamic and helpful one, which has led to a lot of innovation in the editor / ide space. Plugins like markview.nvim (inline markdown preview), hardtime.nvim (helps you get used to healthy vim motions), minty (modern color tools) further improve one\u0026rsquo;s experience with neovim as the editor and are constantly pushing the boundaries of what neovim can do. Which is why I\u0026rsquo;ve taken to calling my particular configuration neo development environment in place of ide. Although plugins / neovim core are quite stable at this point, I believe the best of neovim is yet to come.\n","permalink":"https://inverted-tree.pages.dev/posts/an-adequate-neovim-config/","summary":"\u003cp\u003e\u003cimg alt=\"nvim_perennial\" loading=\"lazy\" src=\"https://pub-610e38254acb47cea52a77d9d4b58499.r2.dev/blog/a111564c-nvim_perennial.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003etl;dr\u003c/strong\u003e: Neovim has come a long way from an experimental vim fork that it was at inception and is veritably, in my opinion, the hotbed for open source editor innovation currently. However customising it can feel daunting especially since things are changing rapidly.\u003c/p\u003e\n\u003cp\u003eHaving lived through \u003ca href=\"https://en.wikipedia.org/wiki/Editor_war\"\u003evi vs emacs\u003c/a\u003e I was a full on emacs person jumping through my ocaml code with the venerable \u003ca href=\"https://github.com/ocaml/tuareg\"\u003etuareg mode\u003c/a\u003e which is loved and maintained to this day. At that time, vim support for ocaml didn\u0026rsquo;t exist, thus emacs came out on top for me.\u003c/p\u003e","title":"An adequate neovim config"},{"content":"\n(screenshot taken from diffview.nvim repository)\nOver the years, I\u0026rsquo;ve gone through so many merge tools - kdiff3, meld, beyond compare, Kaleidoscope. Nvim being my editor of choice, fortunately, has a rich ecosystem of plugins where I can set up an uber merge workflow using diffview.nvim and avoid using external tools altogether.\nGetting started Make sure you have the latest nvim installed (I have v0.10.4). Although I use lazyvim as the base to provide the basic (and some advanced) editor features, I have to set recourse to diffview.nvim to ease my merging process.\nThe first and only step is to edit the .gitconfig file to add these options\n[merge] tool =nvim-diffview [mergetool \u0026#34;nvim-diffview\u0026#34;] cmd = \u0026#34;nvim -c \u0026#39;DiffviewOpen\u0026#39;\u0026#34; prompt = false Et voila. Now each merge conflict that opens automatically in a diff view with simple keybindings, a brief overview of which is provided below:\n[x , ]x jump to different previous / next conflict marker \u0026lt;leader\u0026gt;co: Choose the OURS version of the conflict. \u0026lt;leader\u0026gt;ct: Choose the THEIRS version of the conflict. \u0026lt;leader\u0026gt;cb: Choose the BASE version of the conflict. \u0026lt;leader\u0026gt;ca: Choose all versions of the conflict (effectively just deletes the markers, leaving all the content). dx: Choose none of the versions of the conflict (delete the conflict region). As usual, you can just :h diffview anytime you need to refer to some config / keybinding.\nYou can also check out my full .gitconfig here and my full neovim config here.\n","permalink":"https://inverted-tree.pages.dev/posts/resolve-merge-conflicts-like-a-pro-with-neovim/","summary":"\u003cp\u003e\u003cimg alt=\"diffview\" loading=\"lazy\" src=\"https://pub-610e38254acb47cea52a77d9d4b58499.r2.dev/blog/312b56e5-diffview.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e(screenshot taken from diffview.nvim repository)\u003c/p\u003e\n\u003cp\u003eOver the years, I\u0026rsquo;ve gone through so many merge tools - kdiff3, meld, beyond compare, Kaleidoscope. Nvim being my editor of choice, fortunately, has a rich ecosystem of plugins where I can set up an uber merge workflow using \u003ccode\u003ediffview.nvim\u003c/code\u003e and avoid using external tools altogether.\u003c/p\u003e\n\u003ch5 id=\"getting-started\"\u003eGetting started\u003c/h5\u003e\n\u003cp\u003eMake sure you have the latest nvim installed (I have v0.10.4). Although I use \u003ca href=\"https://lazyvim.org\"\u003elazyvim\u003c/a\u003e as the base to provide the basic (and some advanced) editor features, I have to set recourse to \u003cem\u003ediffview.nvim\u003c/em\u003e to ease my merging process.\u003c/p\u003e","title":"Resolve merge conflicts like a pro with neovim"},{"content":"I\u0026rsquo;ve been learning haskell for some time now and it has been amazing grappling with pure functional programming concepts. Although it\u0026rsquo;s not for everyone, I believe its potential is too much in terms purity.\nThe best way for me to learn anything is to just build something and i\u0026rsquo;m quite infatuated with yaml right now, so I suppose i\u0026rsquo;ll build a very basic yaml parser to learn about parsing concepts and how haskell helps with its functional prowess. Since the markup language has just data and no instructions, it\u0026rsquo;s suited for a recursive data definition which Haskell excels in, among other things.\nPrelude A markup language is designed for humans to show data in human readable format while being machine compatible reliably. Yaml stands for yet another markup language and is quite simple. Here\u0026rsquo;s an example\nservices: web: build: . ports: - \u0026#34;8000:5000\u0026#34; redis: image: \u0026#34;redis:alpine\u0026#34; This small example shows what makes up yaml\u0026rsquo;s constituents - scalar values, mappings and sequences.\nInterface Let\u0026rsquo;s say we create yamlParser.hs and this is what it\u0026rsquo;s going to look like when it\u0026rsquo;s done.\nimport parse, YamlVal from yamlParser input_str = \u0026#34;- item1\\n- item2\\nkey: value\u0026#34; val = parse(input_str) We have to implement the parse function that takes an arbitrary string and converts it to YamlVal, which is a native custom datatype we\u0026rsquo;re gonna define as part of the parser. Let\u0026rsquo;s dive in\nStep 1: define the custom data type that represents a yaml value in our program\ndata YamlVal = YamlScalar String | YamlSequence [YamlVal] | YamlMapping [(String, YamlVal)] deriving (Show, Eq) This data type will take care of our basic example parsing as described above. (Side note: We can define numbers / bools etc as YamlScalars as actual yaml does but we\u0026rsquo;ll get to that after the basic implementation). The Show and Eq typeclasses are derived from for obvious reasons.\nNow that we have the \u0026lsquo;interface\u0026rsquo; and definition of structure to be parsed, let\u0026rsquo;s go on implementing a parser. Ordinarily, one would use dedicated library (say parsec / megaparsec) but since the point is to learn how the parser works, we\u0026rsquo;re gonna implement the parsing by hand.\nParsing functions Since we have defined our custom yaml data type to be comprised of scalars, sequences, and mappings, we have to write 3 functions separately that handle the respective scenarios and later compose them into a parent parser function.\nParsing Scalars Let\u0026rsquo;s start with parsing scalars. Scalars, for now, are only strings, so it should be easy. Here\u0026rsquo;s an implementation\nparseScalar :: String -\u0026gt; YamlVal parseScalar s = let (scalar, _) = break (== \u0026#39;\\n\u0026#39;) s in (YamlScalar scalar) We\u0026rsquo;ve considered that every scalar is delimited by a new line, so we\u0026rsquo;re gonna split the input string by \\n as split index, and throw away the rest of the string.\nWe can define a helper function trim which trims the whitespace around the scalar if that is not desired-\ntrim :: String -\u0026gt; String trim = t . t where t = reverse . dropWhile (== \u0026#39; \u0026#39;) This function removes the white space from the front / end of the string. So let\u0026rsquo;s add trim to our definiton of parseScalar\nparseScalar :: String -\u0026gt; YamlVal parseScalar s = let (scalar, _) = break (== \u0026#39;\\n\u0026#39;) s in (YamlScalar trim scalar) Parsing Sequences Sequences in yaml are basically list of yaml values (YamlVal in our definition). Let\u0026rsquo;s take a dig\nparseSequence :: String -\u0026gt; YamlVal parseSequence s = let items = parseItems s in YamlSequence items Define a helper function parseItems that\u0026rsquo;ll do the heavy lifting for processing the input string recursively for items.\nparseItems :: String -\u0026gt; [YamlVal] parseItems [] = [] -- strings are but list of chars in Haskell parseItems (x:xs) | x == \u0026#39;-\u0026#39; = -- line starts with - means it\u0026#39;s a list item let (scalar, rest) = break (== \u0026#39;\\n\u0026#39;) xs in (YamlVal (trim scalar) : parseItems (drop 1 rest)) | otherwise = [] Pretty straightforward. We break the string at every new line, inspect the first char as - or not, and act accordingly. This is where the power of descriptive haskell takes reins.\nParsing Key Val Mapping In yaml, the mappings have a key followed by color (:) and a value. We can easily write a recursive parsing function for the same. Following the previous recipe\nparseMapping :: String -\u0026gt; YamlVal parseMapping s = let pairs = parsePairs s in YamlMapping pairs and the attendant helper function parsePairs\nparsePairs :: String -\u0026gt; [(String, YamlVal)] parsePairs [] = [] parsePairs line:lines = | \u0026#39;:\u0026#39; `elem` line = let (key, value) = break (== \u0026#39;:\u0026#39;) line trimmedKey = trim key trimmedVal = trim (drop 1 value) -- drop the : in (trimmedKey, YamlVal trimmedVal) : parsePairse lines | otherwise = parsePairs lines Now we have three different functions that parse different components of yaml. How to put them together? The most obvious way is to\nparseYaml :: String -\u0026gt; YamlVal parseYaml input | isSequence input = parseSequence input | isMapping input = parseMapping input | otherwise = parseScalar input with isSequence , isMapping as helper functions. But there are some limitations - this function cannot handle mixed input / nested structures. How to go about it? This brings us to the nested part of the yaml structure, a hint of which is there in our data definition for YamlVal which is recursive.\nTo be continued..\n","permalink":"https://inverted-tree.pages.dev/posts/learning-haskell-and-yaml-by-building-a-yaml-parser/","summary":"\u003cp\u003eI\u0026rsquo;ve been learning haskell for some time now and it has been amazing grappling with pure functional programming concepts. Although it\u0026rsquo;s not for everyone, I believe its potential is too much in terms purity.\u003c/p\u003e\n\u003cp\u003eThe best way for me to learn anything is to just build something and i\u0026rsquo;m quite infatuated with yaml right now, so I suppose i\u0026rsquo;ll build a very basic yaml parser to learn about parsing concepts and how haskell helps with its functional prowess. Since the markup language has just data and no instructions, it\u0026rsquo;s suited for a recursive data definition which Haskell excels in, among other things.\u003c/p\u003e","title":"Learning haskell and yaml by building a yaml parser"},{"content":"In my previous post, I discussed some general purpose command line utilities that help ease your way around the command line. In this post we\u0026rsquo;ll discuss some command line utilities that are indispensable to my web development workflow, and might come in handy for you too :)\npgcli/mycli All web apps depend on a database of some kind. Thus, communicating with a database is one of the most common tasks a web developer has to perform. Although MySQL and PostgreSQL come bundled with their own CLI tools, there are options like pgcli/mycli which provide more powerful interfaces to the same databases, with features like auto completion, command history, syntax highlighting and many more.\npgcli mycli\nlive-server Every once in a while, one has to deploy a static site locally to test out ideas(say fonts etc). Live-Server is my tool of choice in such cases. It supports live reload, and works without installing any browser plugins. So simple and minimalist.\nlive-server\njson-server If you do frontend development with React/Angular, you probably need to intnegrate a REST API backend; which might not be ready from the get go. Enter json-server, a handy tool which allows you to spin up an API server from a JSON file. For eg. to define a endpoint, you just need a key in the json file by the same name. Quite simple, eh?\njson-server\nhttpie If you develop REST APIs, sometimes you need a tool to manually test out the responses from the same. Although there are some GUI applications available like Postman/Insomnia, I rely on this beautiful and popular command line application. It has a simple syntax and sane defaults which make it quite easy to get started. For ex. making a GET request is as easy as\nhttp example.com\nIt supports a tonne of options which are quite straightforward and easy to remember.\nhttpie\nbpython/pry/quokka.js If you program in an interpreted language, it\u0026rsquo;s worthwhile to find a \u0026lsquo;super\u0026rsquo; shell for it, that has more/better features than the native interpreter shell. For ex. there\u0026rsquo;s bpython for Python, pry for Ruby etc. Some common features that they include are syntax completion, interactive history, syntax highlighting etc.\nQuokka.js is a tool to test out an npm package without creating a new project. It\u0026rsquo;s quite handy and there\u0026rsquo;s also a VS Code extension available.\nSnyk Snyk allows you to do check for/fix security vulnerabilities in your app\u0026rsquo;s dependencies. It\u0026rsquo;s open source and available for a variety of platforms, such as Node.js/ Python/PHP etc. I use it regularly with my projects and highly recommend it.\nsnyk\n","permalink":"https://inverted-tree.pages.dev/posts/cli-tools-web-dev/","summary":"\u003cp\u003eIn my \u003ca href=\"https://inverted-tree.com/command-line-tools-to-make-your-life-easier/\"\u003eprevious post\u003c/a\u003e, I discussed some general purpose command line utilities that help ease your way around the command line. In this post we\u0026rsquo;ll discuss some command line utilities that are indispensable to my web development workflow, and might come in handy for you too :)\u003c/p\u003e\n\u003ch4 id=\"pgclimycli\"\u003epgcli/mycli\u003c/h4\u003e\n\u003cp\u003eAll web apps depend on a database of some kind. Thus, communicating with a database is one of the most common tasks a web developer has to perform. Although MySQL and PostgreSQL come bundled with their own CLI tools, there are options like pgcli/mycli which provide more powerful interfaces to the same databases, with features like auto completion, command history, syntax highlighting and many more.\u003c/p\u003e","title":"Command line tools for the productive web developer"},{"content":"In the previous post I profiled my zsh load to debug the monumental shell load time which is really annoying for me, since I spend all my time in the shell.\nI think I\u0026rsquo;m gonna get rid of compinit totally and look at the profiling data. Let\u0026rsquo;s dive in\n# autoload -Uz compinit # if [[ -n ${HOME}/.zcompdump(#qN.mh+24) ]]; then # compinit; # else # compinit -C; # fi; Launching the profiler now, the results are-\nnum calls time self name ----------------------------------------------------------------------------------- 1) 3 48.43 16.14 33.28% 26.07 8.69 17.92% compinit 2) 4 22.36 5.59 15.36% 22.36 5.59 15.36% compaudit 3) 1 12.94 12.94 8.89% 12.94 12.94 8.89% __zplug::utils::awk::available 4) 1 12.32 12.32 8.46% 12.27 12.27 8.43% __zplug::base::base::git_version 5) 6 13.51 2.25 9.29% 9.35 1.56 6.42% __zplug::core::load::as_plugin 6) 1 15.70 15.70 10.79% 7.92 7.92 5.45% __check__ 7) 5 7.34 1.47 5.04% 7.34 1.47 5.04% __zplug::sources::github::check 8) 5 8.29 1.66 5.70% 6.61 1.32 4.54% __zplug::core::sources::call 9) 1 48.17 48.17 33.11% 6.59 6.59 4.53% __zplug::core::core::prepare 10) 5 12.58 2.52 8.65% 4.29 0.86 2.95% __zplug::core::sources::use_default 11) 3 5.50 1.83 3.78% 4.26 1.42 2.93% __zplug::core::core::get_interfaces 12) 5 16.01 3.20 11.00% 3.39 0.68 2.33% __zplug::core::add::to_zplugs 13) 1 2.18 2.18 1.50% 2.18 2.18 1.50% __zplug::core::cache::diff 14) 6 2.01 0.33 1.38% 2.01 0.33 1.38% github.zsh 15) 1 7.26 7.26 4.99% 1.74 1.74 1.20% __zplug::core::core::variable 16) 7 8.54 1.22 5.87% 1.55 0.22 1.06% __zplug::base 17) 1 1.41 1.41 0.97% 1.41 1.41 0.97% async_init Looks like the calls to compinit have reduced by 1. Can\u0026rsquo;t tell if the shell feels snappy or not. Launching bash based time evaluator again: time zsh -i -c exit\nreal 0m0.831s user 0m0.521s sys 0m0.267s Looks like it increased? I think i\u0026rsquo;m gonna ditch this bash based benchmarks for the time being.\nPost disabling the profiler, the shell does feel snappier and looks like the completions work fine too. So I guess that\u0026rsquo;s enough for me for the time being. One of these days I\u0026rsquo;m gonna get dissatisfied with the current load time and continue again on this path. Until then, I\u0026rsquo;m sorted.\nBTW you can look at my dotfiles here\n","permalink":"https://inverted-tree.pages.dev/posts/speeding-up-my-zsh-prompt-part-2/","summary":"\u003cp\u003eIn the previous \u003ca href=\"https://inverted-tree.com/speeding-up-my-zsh-prompt-part-1/\"\u003epost\u003c/a\u003e I profiled my zsh load to debug the monumental shell load time which is really annoying for me, since I spend all my time in the shell.\u003c/p\u003e\n\u003cp\u003eI think I\u0026rsquo;m gonna get rid of \u003ccode\u003ecompinit\u003c/code\u003e totally and look at the profiling data. Let\u0026rsquo;s dive in\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#  autoload -Uz compinit\n#  if [[ -n ${HOME}/.zcompdump(#qN.mh+24) ]]; then\n #  compinit;\n#  else\n #  compinit -C;\n#  fi;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eLaunching the profiler now, the results are-\u003c/p\u003e","title":"Speeding up my zsh prompt: Part 2"},{"content":"I\u0026rsquo;m a big fan of the z shell. I use it with the spaceship prompt and zplug for plugin management.\nRight now, it takes an uncomfortable amount of time to load up the prompt on terminal emulator launch, which is annoying for me since I like everything to be snappy, at least on the command line.\nOverview of my zshrc It\u0026rsquo;s pretty basic, boasts of a zplug installation for plugin management and some attendant plugins.\n[[ -f ~/.aliases ]] \u0026amp;\u0026amp; . ~/.aliases # zmodload zsh/zprof # set emacs mode as default bindkey -e source $ZPLUG_HOME/init.zsh zplug \u0026#34;spaceship-prompt/spaceship-prompt\u0026#34; zplug \u0026#34;zsh-users/zsh-autosuggestions\u0026#34; zplug \u0026#34;Valiev/almostontop\u0026#34; zplug \u0026#34;MichaelAquilina/zsh-autoswitch-virtualenv\u0026#34; eval \u0026#34;$(rbenv init - zsh)\u0026#34; # Install plugins if there are plugins that have not been installed if ! zplug check --verbose; then printf \u0026#34;Install? [y/N]: \u0026#34; if read -q; then echo; zplug install fi fi # Then, source plugins and add commands to $PATH zplug load if type brew \u0026amp;\u0026gt;/dev/null; then FPATH=$(brew --prefix)/share/zsh-completions:$FPATH autoload -Uz compinit compinit fi # heroku autocomplete setup HEROKU_AC_ZSH_SETUP_PATH=/Users/nav/Library/Caches/heroku/autocomplete/zsh_setup \u0026amp;\u0026amp; test -f $HEROKU_AC_ZSH_SETUP_PATH \u0026amp;\u0026amp; source $HEROKU_AC_ZSH_SETUP_PATH; function gi() { curl -sLw n https://www.toptal.com/developers/gitignore/api/$@ ;} setopt autocd autopushd # pomodoro timer # usage pomo \u0026lt;task\u0026gt; function pomo() { arg1=$1 shift args=\u0026#34;$*\u0026#34; min=${arg1:?Example: pomo 15 Take a break} sec=$((min * 60)) msg=\u0026#34;${args:?Example: pomo 15 Take a break}\u0026#34; while true; do sleep \u0026#34;${sec:?}\u0026#34; \u0026amp;\u0026amp; echo \u0026#34;${msg:?}\u0026#34; \u0026amp;\u0026amp; say \u0026#34;${msg:?}\u0026#34; done } [ -f /opt/homebrew/etc/profile.d/autojump.sh ] \u0026amp;\u0026amp; . /opt/homebrew/etc/profile.d/autojump.sh function take() { mkdir $1; cd $1; } eval \u0026#34;$(jenv init -)\u0026#34; # zprof The culprit could be any one of those.\nDiagnosing The first step in any speed based diagnoses is profiling. I need to convert the subjective speed rush into tangible numbers and then try to reduce the load time by an order of magnitude\nUsing bash inbuilt utility time\ntime zsh -i -c exit\nzsh -i -c exit 0.17s user 0.17s system 86% cpu 0.402 total It returned 0.17 seconds, and a long 0.17 seconds it is.\nThis happens when the said command is launched from zsh itself. Trying to launch it from bash\nreal 0m0.892s user 0m0.579s sys 0m0.278s user time is 0.578s which is a lot again. Apparently it\u0026rsquo;s even higher because zsh takes time to load the first time. Debugging it is gonna be interesting.\nProfiling zsh contains a profiling tool called zprof bundled. Let\u0026rsquo;s figure out how to inject it.\nYou just use it this way it appears in my zshrc:\nzmodload zsh/zprof\n\u0026lt;rest of my zshrc\u0026gt;\nzprof\nLaunched zshell and here\u0026rsquo;s an extract from the output:\nnum calls time self name ----------------------------------------------------------------------------------- 1) 2 346.23 173.12 52.57% 346.23 173.12 52.57% compdump 2) 4 574.15 143.54 87.17% 116.10 29.02 17.63% compinit 3) 1825 75.05 0.04 11.39% 75.05 0.04 11.39% compdef 4) 6 36.77 6.13 5.58% 36.77 6.13 5.58% compaudit 5) 1 12.63 12.63 1.92% 12.58 12.58 1.91% __zplug::base::base::git_version 6) 1 9.53 9.53 1.45% 9.53 9.53 1.45% __zplug::utils::awk::available 7) 4 10.50 2.62 1.59% 7.63 1.91 1.16% __zplug::core::load::as_plugin 8) 1 12.50 12.50 1.90% 6.27 6.27 0.95% __check__ 9) 4 5.87 1.47 0.89% 5.87 1.47 0.89% __zplug::sources::github::check 10) 1 43.06 43.06 6.54% 5.24 5.24 0.80% __zplug::core::core::prepare 11) 4 6.49 1.62 0.98% 5.15 1.29 0.78% __zplug::core::sources::call 12) 2 4.65 2.32 0.71% 4.65 2.32 0.71% __zplug::io::file::rm_touch 13) 4 9.82 2.46 1.49% 3.34 0.83 0.51% __zplug::core::sources::use_default 14) 3 4.29 1.43 0.65% 3.16 1.05 0.48% __zplug::core::core::get_interfaces 15) 4 12.40 3.10 1.88% 2.55 0.64 0.39% __zplug::core::add::to_zplugs 16) 4 2.39 0.60 0.36% 2.39 0.60 0.36% __zplug::job::handle::flock 17) 1 1.90 1.90 0.29% 1.90 1.90 0.29% __zplug::core::cache::diff 18) 5 1.69 0.34 0.26% 1.69 0.34 0.26% github.zsh 19) 7 7.45 1.06 1.13% 1.57 0.22 0.24% __zplug::base 20) 1 5.82 5.82 0.88% 1.51 1.51 0.23% __zplug::core::core::variable 21) 29 1.13 0.04 0.17% 1.13 0.04 0.17% regexp-replace 22) 1 0.76 0.76 0.12% 0.76 0.76 0.12% colors 23) 1 0.44 0.44 0.07% 0.44 0.44 0.07% git.zsh 24) 1 0.44 0.44 0.07% 0.44 0.44 0.07% handle.zsh 25) 1 0.39 0.39 0.06% 0.39 0.39 0.06% core.zsh 26) 1 31.61 31.61 4.80% 0.36 0.36 0.05% __zplug::core::load::from_cache 27) 1 0.34 0.34 0.05% 0.34 0.34 0.05% theme.zsh 28) 6 0.28 0.05 0.04% 0.28 0.05 0.04% add-zsh-hook 29) 1 0.28 0.28 0.04% 0.28 0.28 0.04% cache.zsh 30) 1 0.27 0.27 0.04% 0.27 0.27 0.04% load.zsh 31) 1 0.26 0.26 0.04% 0.26 0.26 0.04% oh-my-zsh.zsh 32) 2 0.25 0.13 0.04% 0.25 0.13 0.04% prezto.zsh 33) 1 0.23 0.23 0.04% 0.23 0.23 0.04% shell.zsh Looks like compdump and compinit are taking up most of the initialization time. Don\u0026rsquo;t exactly remember what they are.. gonna have to reinvestigate zshrc\nThis also gives me an opportunity to clean up my zshrc.\nAfter a brief research I come across this gist that recommends checking a .zcompdump file only once a day for improved performance.\n# On slow systems, checking the cached .zcompdump file to see if it must be # regenerated adds a noticable delay to zsh startup. This little hack restricts # it to once a day. It should be pasted into your own completion file. # # The globbing is a little complicated here: # - \u0026#39;#q\u0026#39; is an explicit glob qualifier that makes globbing work within zsh\u0026#39;s [[ ]] construct. # - \u0026#39;N\u0026#39; makes the glob pattern evaluate to nothing when it doesn\u0026#39;t match (rather than throw a globbing error) # - \u0026#39;.\u0026#39; matches \u0026#34;regular files\u0026#34; # - \u0026#39;mh+24\u0026#39; matches files (or directories or whatever) that are older than 24 hours. autoload -Uz compinit if [[ -n ${ZDOTDIR}/.zcompdump(#qN.mh+24) ]]; then compinit; else compinit -C; fi; After incorporating this, time to try my tests again. And the results are in:\nWith bash:\nreal 0m0.449s user 0m0.190s sys 0m0.217s Looks like the init time has almost halved for user. Good enough for me I guess.\nHowever the profiling tells another story.\nnum calls time self name ----------------------------------------------------------------------------------- 1) 3 426.16 142.05 54.35% 426.16 142.05 54.35% compdump 2) 4 697.40 174.35 88.95% 137.28 34.32 17.51% compinit 3) 2312 99.99 0.04 12.75% 99.99 0.04 12.75% compdef 4) 6 34.01 5.67 4.34% 34.01 5.67 4.34% compaudit 5) 1 12.43 12.43 1.59% 12.39 12.39 1.58% __zplug::base::base::git_version 6) 1 8.87 8.87 1.13% 8.87 8.87 1.13% __zplug::utils::awk::available 7) 6 11.56 1.93 1.47% 7.94 1.32 1.01% __zplug::core::load::as_plugin 8) 1 15.24 15.24 1.94% 7.73 7.73 0.99% __check__ 9) 5 7.10 1.42 0.91% 7.10 1.42 0.91% __zplug::sources::github::check 10) 5 8.41 1.68 1.07% 6.70 1.34 0.85% __zplug::core::sources::call 11) 1 42.80 42.80 5.46% 6.47 6.47 0.82% __zplug::core::core::prepare 12) 5 12.70 2.54 1.62% 4.29 0.86 0.55% __zplug::core::sources::use_default 13) 5 16.08 3.22 2.05% 3.34 0.67 0.43% __zplug::core::add::to_zplugs 14) 3 4.41 1.47 0.56% 3.19 1.06 0.41% __zplug::core::core::get_interfaces 15) 6 1.99 0.33 0.25% 1.99 0.33 0.25% github.zsh 16) 1 1.97 1.97 0.25% 1.97 1.97 0.25% __zplug::core::cache::diff The time taken by compinit has increased from 574.15 ms to 697 ms and it doesn\u0026rsquo;t feel snappy either.\nThe plot thickens. To be continued..\n","permalink":"https://inverted-tree.pages.dev/posts/speeding-up-my-zsh-prompt-part-1/","summary":"\u003cp\u003eI\u0026rsquo;m a big fan of the \u003ccode\u003ez shell\u003c/code\u003e. I use it with the \u003ca href=\"https://github.com/spaceship-prompt/spaceship-prompt\"\u003espaceship prompt\u003c/a\u003e and \u003ca href=\"https://github.com/zplug/zplug\"\u003ezplug\u003c/a\u003e for plugin management.\u003c/p\u003e\n\u003cp\u003eRight now, it takes an uncomfortable amount of time to load up the prompt on terminal emulator launch, which is annoying for me since I like everything to be snappy, at least on the command line.\u003c/p\u003e\n\u003ch4 id=\"overview-of-my-zshrc\"\u003eOverview of my zshrc\u003c/h4\u003e\n\u003cp\u003eIt\u0026rsquo;s pretty basic, boasts of a \u003ccode\u003ezplug\u003c/code\u003e installation for plugin management and some attendant plugins.\u003c/p\u003e","title":"Speeding up my zsh prompt - part 1"},{"content":"If you\u0026rsquo;re like me, you spend most of your time in the command line, where saving even a few keystrokes can help you become significantly more productive (and happier :). Here are some tools that I\u0026rsquo;ve found to be incredibly helpful and have augmented my love for the command line through their sheer awesomeness\nBat Bat is a successor to the unix cat command that boasts many superpowers, like preview with a pager, syntax highlighting, and even git changes. It supports most of the programming languages out of the box. You could just alias cat to point to bat.\nHere\u0026rsquo;s a screenshot of it in action with my bashrc: z.lua Typing full directory paths on the command line is a chore. Which is why I keep track of my recently visited directories with a handy program called z.lua, which is a lua re-write of the namesake z.sh written in bash.\nz.lua tracks a parameter called frecency which is a combination of frequency and recent visits to directories. Once you\u0026rsquo;ve visited a directory, it\u0026rsquo;s added to the list of visited directories sorted by the frecency and you can visit it just by providing a simple fuzzy argument to the z command, however nested the target directory. Check out the project homepage for more details.\nix A number of times you need to share a local text file on your system with the world, be it some config file or code file etc. ix uploads whatever text you pipe into it and returns a URL of the uploaded text. For ex. to share your bashrc with the world, just issue\ncat ~/.bashrc | ix\nThis will output a URL to STDOUT that you can visit to access the uploaded content. You can also create an account to keep track of your uploads and manage them.\nnmtui If you use Network Manager for network configuration, you\u0026rsquo;re gonna love the bundled nmtui program. It lets you manage your wireless(or wired) connections with a breeze with a sweet terminal UI.\nnnn No list of command line tools would be complete without a cli file manager, which is one of the most used apps on any operating system. Enter nnn , a fast and simple file manager written in C++. It has a lot of features- viz. vim like keybindings, search as you type, opening files with the default/custom app, disk usage reporting etc.\nAlternatives to nnn are fff, ranger and midnight commander\nborg You should back up your system regularly to protect your data from unexpected hardware failures. Enter borg, a command line backup tool written in Python. It is fully featured, has a lot of encryption options and is quite stable and popular based on de-duplication. It\u0026rsquo;s pretty straight forward to use too.\num Even the best of us can forget details of how some of our favorite cli tool options work. Um helps you to create and manage your own manpages so you could track the commonly used commands with options until you memorize them. Creating and updating manpages is as easy as um edit and recalling them is as easy as um \u0026lt;target\u0026gt;. For ex. I can never remember the different options I use for my borg backups, so I\u0026rsquo;ve created an um page for the purpose-\ntldr pages It\u0026rsquo;s usually quite daunting and hectic to go through a command\u0026rsquo;s man page for a simple option you want to find. Tldr pages provide a quick summary of any command(s) you\u0026rsquo;re not familiar with. It lists the most common options for tools with examples of how they work, significantly easing your way around it. Here\u0026rsquo;s an example description of curl using tldr curl\ntldr has community pages for almost any command you could come across.\nBro is an alternative written in ruby.\nshellcheck Bash is a really powerful tool to further expand your mastery of the command line and Linux in general. shellcheck is a handy tool that helps to lint/analyze your bash scripts.\nBonus: pscircle Like the name says, this nifty utility draws a circular process tree which you can set as your desktop wallpaper. It has a lot of options and is quite straightforward to use. Here\u0026rsquo;s an example of its usage-\npscircle\nConclusion This list just covers the tools I use on a day to day basis. If you\u0026rsquo;re itching for more, check out this awesome list or this subreddit dedicated solely to command line applications.\n","permalink":"https://inverted-tree.pages.dev/posts/command-line-tools-to-make-your-life-easier/","summary":"\u003cp\u003eIf you\u0026rsquo;re like me, you spend most of your time in the command line, where saving even a few keystrokes can help you become significantly more productive (and happier :). Here are some tools that I\u0026rsquo;ve found to be incredibly helpful and have augmented my love for the command line through their sheer awesomeness\u003c/p\u003e\n\u003ch4 id=\"bat\"\u003eBat\u003c/h4\u003e\n\u003cp\u003eBat is a successor to the unix \u003ccode\u003ecat\u003c/code\u003e command that boasts many superpowers, like preview with a pager, syntax highlighting, and even git changes. It supports most of the programming languages out of the box. You could just alias \u003ccode\u003ecat\u003c/code\u003e to point to \u003ccode\u003ebat\u003c/code\u003e.\u003c/p\u003e","title":"Command line tools to make your life easier"}]