Building a Desktop Time App in Rust with GPUI: A Beginner's Journey

Published: (January 1, 2026 at 01:00 AM EST)
7 min read
Source: Dev.to

Source: Dev.to

The Rust Time Paradox: A Developer’s Story

I set out to build a simple world‑clock app in Rust. Six weeks later I finally got it working—displaying the current time in five cities. The irony? I spent so much time making a time display that time itself seemed to stop.

What This Project Teaches You

Core Rust Concepts You’ll Master

  • Ownership & Borrowing – Every timezone card is an Entity that manages its own state.
  • Trait Implementation – Custom Render traits for UI components.
  • Type Safety – Compile‑time guarantees that prevent runtime timezone errors.
  • Lifetimes – Understanding &mut Context and window references.

GPUI Framework Fundamentals

  • Component architecture with impl IntoElement.
  • State management via Entity and Context.
  • Reactive updates using cx.observe_window_bounds().
  • Layout system with flexbox‑style APIs.

How to Build and Run This Project

Prerequisites

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Verify installation
rustc --version
cargo --version

Project Setup

Create a new project

cargo new world-time-app
cd world-time-app

Add dependencies to Cargo.toml

[dependencies]
chrono = { version = "0.4" }
gpui = "0.2"
gpui-component = "0.4.0-preview1"
  • Copy the code to src/main.rs.
  • Build and run:
cargo run --release

Common Build Issues & Solutions

  • Issue: “cannot find gpui in the registry”
    Solution: GPUI is only available via Git, not crates.io. Use the Git dependency as shown above.

  • Issue: Compilation takes 10+ minutes
    Solution: The first build is slow (downloading/compiling dependencies). Subsequent builds use Cargo’s cache. Use --release only for final builds.

  • Issue: Window doesn’t appear on Linux
    Solution: Install required system libraries:

    # Ubuntu/Debian
    sudo apt-get install libxkbcommon-dev libwayland-dev
    
    # Fedora
    sudo dnf install libxkbcommon-devel wayland-devel

Code Architecture Breakdown

The WorldTime Entity

pub struct WorldTime {
    name: String,
    time: String,        // HH:MM format
    diff_hours: i32,     // offset from home
    is_home: bool,
    timezone_id: String,
}

Key learning: Structs in Rust must own their data or explicitly borrow it. Choosing String (owned) vs &str (borrowed) was my first major decision.

Component Rendering Pattern

impl Render for WorldTime {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement
}

Key learning: The &mut self parameter confused me for days. Mutable references allow the component to update its state during render cycles.

Time Updates

if now.duration_since(self.last_update).as_secs() >= 60 {
    for city in &self.cities {
        city.update(cx, |city, _cx| {
            city.update_time();
        });
    }
}

Key learning: The update() method on Entity takes a context and a closure. The closure receives &mut T (mutable access to the entity’s internal state) and a context, allowing safe state mutations within GPUI’s ownership model.

For Rust Beginners: What I Wish I Knew

Week 1‑2: The Compiler Is Your Teacher

Challenge – “Why won’t this compile?!”

// ❌ This fails
let time = WorldTime::new(...);
render(time);
use_time_again(time); // ERROR: value moved

// ✅ This works
let time = WorldTime::new(...);
render(&time);
use_time_again(&time); // Borrowing, not moving

Lesson: Rust’s error messages are detailed. Read them carefully—they often include the fix.

Week 3‑4: Lifetimes Aren’t Scary

Challenge – Understanding 'a in trait bounds.

Breakthrough moment – Lifetimes just tell the compiler “this reference is valid for this long.” The compiler infers most of them.

// GPUI handles this complexity:
fn render(&mut self, _window: &mut Window, _cx: &mut Context)
// You don’t write the lifetimes manually

Week 5‑6: Fighting the Borrow Checker

The infamous error

error[E0502]: cannot borrow `self.cities` as mutable because it is also borrowed as immutable

My solution – Extract logic into separate functions. The borrow checker works per‑scope:

// Instead of doing everything in one method:
fn render(&mut self) {
    let cities = &self.cities; // Immutable borrow
    self.update_all(); // ❌ Mutable borrow while immutable exists
}

// Split into logical units:
fn render(&mut self) {
    self.update_if_needed(); // Mutable borrow ends here
    let cities = &self.cities; // Now we can borrow immutably
}

Debugging Strategies That Saved Me

  1. Print Debugging Still Works
dbg!(&self.time); // Rust's debug macro
println!("City: {}, Time: {}", self.name, self.time);

Lessons Learned

1. Keep the Boilerplate Minimal

fn main() {
    // Minimal setup
    let app = App::new();
    app.run();
}

2. Start Simple, Add Complexity

My first version just showed one city. Then two. Then five.
Each step compiled before moving forward.


3. Use cargo check Constantly

cargo check   # Fast syntax checking without building
cargo clippy  # Linting suggestions
cargo fmt     # Auto‑formatting

4. Read the Compiler Suggestions

Rust’s compiler often suggests fixes:

help: consider borrowing here
  |
5 |     render(&time);
  |            +

FAQ: Common Rust Learner Questions

Why is Rust so hard to learn?

Rust forces you to think about memory management upfront rather than debugging crashes later. The learning curve is steep but prevents entire classes of bugs (use‑after‑free, data races, null pointer dereferences).

How long until I’m productive in Rust?

  • 2–3 weeks for basic syntax
  • 2–3 months for comfortable development

This project (6 weeks) taught me more than any tutorial because I had to solve real problems.

Should I learn C++ first?

No. Rust’s ownership system is unique. C++ habits can actually make Rust harder. Start with Rust directly.

What if my code doesn’t compile?

Normal for beginners – I spent ~70 % of my time fighting compiler errors. Each error teaches you something.

When should I use .clone()?

When you need multiple owned copies. Cloning has performance costs but unblocks development. Optimize later:

// Quick solution:
let city_copy = city.clone();

// Optimized later:
let city_ref = &city;

How do I know when to use &, &mut, or no reference?

SymbolMeaning
&Read‑only borrow (most common)
&mutExclusive write access
(none)Transfer ownership (consuming the value)

Start with &, add mut when you need to modify, and transfer ownership only when necessary.

Performance Insights from This Project

Memory Usage

  • Compiled binary: ~4 MB (release mode with optimizations)
  • Runtime memory: ~8 MB for 5 timezone cards
  • No garbage collector overhead

Startup Time

  • Cold start: ~200 ms on M1 Mac
  • Hot start: ~50 ms

Update Efficiency

The 60‑second update check runs every render frame but only recalculates when needed—demonstrating Rust’s zero‑cost abstractions.

Resources That Actually Helped

Essential Learning Path

  • The Rust Book (official) – read chapters 1‑10 first
  • Rust by Example – hands‑on code snippets
  • Rustlings – interactive exercises that compile when correct
  • This project – real‑world application of concepts

When You’re Stuck

  • Rust Users Forum – friendly community for beginners
  • r/rust subreddit – daily questions thread
  • Rust Discord – real‑time help
  • Compiler errors – seriously, read them twice

GPUI‑Specific Resources

  • Zed GitHub repo – source code examples
  • GPUI examples in crates/gpui/examples/
  • Zed’s own codebase – production GPUI usage

The Unvarnished Truth: My Time Investment

Hour Breakdown (Estimated)

ActivityHours
Reading docs / tutorials40
Fighting compiler errors60
Rewriting after understanding15
Actual feature coding10
“Why isn’t the window showing?”8
Total133

Was It Worth It?

Yes.

  • The second Rust project took me 3 days.
  • The third took hours.

The compiler errors that took hours in week 1 now take minutes. Rust’s difficulty is front‑loaded; you pay the learning cost once and reap benefits forever: no segfaults, no data races, no surprise runtime panics.

Next Steps: Extending This Project

Beginner‑Friendly Additions

  • Add more cities (just copy the pattern)
  • Change colors / styling (modify RGB values)
  • Add date display (extend time to include date)

Intermediate Challenges

  • Read cities from a config file (learn file I/O)
  • Add / remove cities dynamically (learn UI state management)
  • Show analog clocks (learn drawing APIs)

Advanced Features

  • System‑tray integration
  • Daylight‑saving‑time handling
  • Multiple time zones per city
  • Network time sync

The Real Lesson: Embrace the Struggle

If your Rust code compiles on the first try, you’re not learning—you’re copying.
The borrow‑checker errors, the lifetime annotations you don’t understand, the hours debugging “why won’t this move?”—that’s the learning process.

My time app works flawlessly now. No memory leaks. No race conditions. No undefined behavior. The compiler guaranteed that before the first run.

The time I “lost” wasn’t lost—it was invested in understanding a language that respects your time in production.

Project Repository

Want to try building this yourself? The complete code is available in the document above. Start with a simple version (one city), get it compiling, then add complexity incrementally.

Remember: Every Rust developer has spent hours on compiler errors. The difference between beginners and experts is that experts have made more mistakes.

Built with Rust 1.75+, GPUI (Zed framework), determination, and a healthy relationship with the borrow checker.

“The compiler is mean to you so your users never have to be.” – Rust Community Wisdom

Back to Blog

Related posts

Read more »

I switched from VSCode to Zed

Article URL: https://tenthousandmeters.com/blog/i-switched-from-vscode-to-zed/ Comments URL: https://news.ycombinator.com/item?id=46498735 Points: 44 Comments:...