Context:
Recently, I got a task of making an alarm app using Python but in CLI. This sounded interesting, so I thought of making it. Although, I was rejected, but I liked it and I made some improvements and tried to implement my imagination of what more can I do. So, this post will be about how did I proceed and my thinking on making it.
Repository:
In case, you want to view it. I am providing the link of the repo. Before, you visit the repo, fyi, the CLI app is working fine as I tried to make it small and interactive, not much of features, just adding alarms. But, I wanted it to be accessible via a web app, which I tried to make it work, but it needs improvement and whenever, I will get some time, I will improve it.
LINK: Repository
Requirements:
So, I used following libraries and packages for the app:
- uv: Package manager for our python app
- rich: For the UI elements
- blessed: For keyboard events
- typer: For making termina app
- fastapi: For web app
- granian: for websockets
- ptyprocess: For pseudo(mimics like actual) terminal interactivity
Learnings
The first thing is that I wanted to make a terminal app, for that I needed something through which I can run the code in terminal. This is where “typer” came to scene.
I can create a cli app using typer. I wanted the app to behave like the following:
- App starts
- It will ask 2 options:
- Set alarm
- Exit app
- If user chooses to set alarm, it will ask 2 things:
- alarm time
- alarm name
- If user chooses to exit, it will clear the screen and exit the process
This was very simple, so I tried to make it more user friendly(improvements). So, the current app have something liek this:
- All the above steps, but now when app starts, it has 2 panels
- Current time panel
- This shows current clock in 24 hrs format HH:MM:SS
- And the clock keeps on ticking
- The menu panel, which asks user to choose between setting up alarm or exit the app
- Current time panel
- If user selects option to set the alarm,
- it asks for the time in 24 hrs and if user provides time which already passed for the day, it will set the alarm for the next day
- then, it asks for the alrm name
- Once, user sets the alram, a countdown panel appears in between current time panel and the menu selection panel
- While the alarm countdown timer keeps on ticking, the menu panel is still active for user to choose set the alarm or exit the app
- User can set multiple alarms or can exit the app
- When an alarm finishes, it will flash for 5s to let the user know that time has reached and then gets removed from the screen.
My Approach and issues:
First thing, I needed to run a wrapper over the actual terminal wher I can add colors, lines, borders, thats where “blessed”, terminal is needed also, it helps in taking the input from the keyboard through this terminal. And then, I needed something that will take care of rendering UI elements that will keep on rendering in the terminal w/o the basic terminal activities that can make it appear like top down approach rendering, but something that renders in a loop so fast such that user would not see the flickering and re-renders. That’s where “rich” library comes up.
Just explaining the main function, all other can be viewed easily in the repository:
app = typer.Typer(help="⏰ Advanced Multi-Alarm Terminal Clock built with uv.")
# Define the exact structure of your alarm dict here!
class AlarmItem(TypedDict):
target: datetime
label: str
status: str # 2. Add status tracker ("pending" or "ringing")
ring_start: float # Tracking timestamp for when the alarm started ringing
class AlarmClock:
_console: Console = Console()
_screen_layout: Layout = Layout()
# 1. State tracking structures
_alarms: list[AlarmItem] # List of dicts: [{"target": datetime, "label": str}]
_mode: str # Options: "menu", "input_time", "input_label"
_input_buffer: str # Captures keystrokes live
_temp_target: datetime # Temporarily holds validated time mid-creation
_alert_colors: list[str]
def __init__(self):
self._console = Console()
self._screen_layout = Layout()
self._alarms = [] # Explicitly initialized fresh per instance [S1]
self._mode = "menu"
self._input_buffer = ""
self._temp_target = datetime.min.replace(tzinfo=UTC)
self._alert_colors = ["red", "yellow", "magenta", "cyan", "bright_white"]
# Construct layout nodes cleanly
self._screen_layout.split_column(
Layout(name="clock", size=4),
Layout(name="countdown_area", visible=False), # Holds stacked active alarms
Layout(name="menu_panel", size=7)
)
# rest of the code...
def run_loop(self):
"""The core single-threaded driver loop processing both animations and inputs."""
self._console.clear()
term = Terminal()
with term.cbreak(), term.hidden_cursor(), Live(self._screen_layout, auto_refresh=True, refresh_per_second=10):
while True:
now = datetime.now().astimezone()
# Check and clean completed alerts continuously
self.check_and_trigger_alarms(now)
self.update_ui_components()
key = term.inkey(timeout=0.1)
if not key:
continue
self.user_controller(key)
@app.command()
def main_menu():
clock = AlarmClock()
clock.run_loop()
def run_alarm_cli():
app()PythonExplanation:
- We have a class called “AlarmClock”, which have all the code related to alarm clock
- We create a screen layout at line #34. We will have 3 layouts primarily, the first one for curren time viewing, the last one is for menu, input alarm time and input label panel. And the middle one is actually flexible, which can contain multiple ongoing alarm panels as well as the alarm goes off panel.
- At line #47, I am creating a context which uses terminal(blessed) as well as “rich” to start the app.
- check_and_trigger_alarms(), tracks the current time with the alarm time in every render
- update_ui_components() is straightforward, it keeps on rendering the panels.
- Line #55, takes care of the key we press. each second, it checks 10 times to see whether user has pressed any key. Also, the Live() that runs refreshes 10 times per second and in background thread makes it easier to render smoothly. This might lag when there will be heavy components to be rendered, but as in my case there are not many heavy components, its smooth, else it needs to be tried and tested for different parameters.
- user_controller(), takes care of the key that is pressed by the user and accordingly performs the actions by the user and also controls which component/panel will be rendered.
I also have it exposed to a web app using fastapi and it is just in its initial phase, where I just made it render and use it as a cli as well. Once, I improve it, I will update this blog as well. Bdw, both the web app and cli can be started using different commands with the same project. All of the details are well mentioned in the README file in the repo. Let me know if you think anything can be improved or made it much better in performance. 🙂
Leave a Reply