git.lucas.co / cce-cloud
cloud storage client
git clone https://git.lucas.co/cce-cloud.git

commit18b07148cf4b13dcdffdaa5511c63e4bc0d6f5e0
authorLucas Galante <[email protected]>
date2026-05-24 23:23
Initial commit of clear-cloud fuzzel-style program using clear-ui

 .gitignore  |   2 +
 Cargo.toml  |  13 +
 src/main.rs | 903 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 918 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96ef6c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..4eeaea8
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "clear-cloud"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clear-ui = { path = "../clear-ui" }
+winit = "0.30"
+wgpu = "24"
+bytemuck = { version = "1", features = ["derive"] }
+pollster = "0.4"
+glyphon = "0.8"
+tokio = { version = "1", features = ["full"] }
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..03b988c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,903 @@
+use std::sync::{Arc, Mutex};
+use std::io::{self, BufRead, IsTerminal};
+use winit::application::ApplicationHandler;
+use winit::event::{ElementState, WindowEvent};
+use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
+use winit::window::{Window, WindowAttributes};
+use winit::keyboard::{Key, NamedKey};
+use winit::dpi::{PhysicalSize, LogicalSize, LogicalPosition};
+
+use clear_ui::widget::{Widget, TextLabel};
+
+use glyphon::{
+    Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
+    TextBounds, TextRenderer, Viewport,
+};
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+struct Vertex {
+    position: [f32; 2],
+    color: [f32; 4],
+}
+
+impl Vertex {
+    const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
+        0 => Float32x2,
+        1 => Float32x4,
+    ];
+
+    fn desc() -> wgpu::VertexBufferLayout<'static> {
+        wgpu::VertexBufferLayout {
+            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
+            step_mode: wgpu::VertexStepMode::Vertex,
+            attributes: &Self::ATTRIBS,
+        }
+    }
+}
+
+fn quad_vertices(
+    x: f32, y: f32, w: f32, h: f32,
+    surface_w: f32, surface_h: f32,
+    color: [f32; 4],
+) -> [Vertex; 6] {
+    let x0 = (x / surface_w) * 2.0 - 1.0;
+    let y0 = 1.0 - (y / surface_h) * 2.0;
+    let x1 = ((x + w) / surface_w) * 2.0 - 1.0;
+    let y1 = 1.0 - ((y + h) / surface_h) * 2.0;
+
+    [
+        Vertex { position: [x0, y0], color },
+        Vertex { position: [x1, y0], color },
+        Vertex { position: [x0, y1], color },
+        Vertex { position: [x1, y0], color },
+        Vertex { position: [x1, y1], color },
+        Vertex { position: [x0, y1], color },
+    ]
+}
+
+fn widget_vertices(w: &dyn Widget, sw: f32, sh: f32) -> Vec<Vertex> {
+    let (x, y, ww, h) = w.rect();
+    let mut verts = quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec();
+    for (qx, qy, qw, qh, qc) in w.extra_quads() {
+        verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
+    }
+    verts
+}
+
+fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
+    let metrics = Metrics::new(size, size * 1.4);
+    let mut buffer = Buffer::new(font_system, metrics);
+    buffer.set_text(font_system, text, Attrs::new(), glyphon::Shaping::Advanced);
+    buffer.shape_until_scroll(font_system, true);
+    buffer
+}
+
+fn filter_and_sort_items(items: &[String], query: &str) -> Vec<String> {
+    if query.is_empty() {
+        return items.to_vec();
+    }
+    let query_lower = query.to_lowercase();
+    
+    let mut scored: Vec<(i32, &String)> = items
+        .iter()
+        .filter_map(|item| {
+            let item_lower = item.to_lowercase();
+            if item_lower == query_lower {
+                Some((100, item))
+            } else if item_lower.starts_with(&query_lower) {
+                Some((80 - (item.len() as i32), item))
+            } else if item_lower.contains(&query_lower) {
+                Some((50 - (item.len() as i32), item))
+            } else {
+                // Character sequence match
+                let mut query_chars = query_lower.chars().peekable();
+                for c in item_lower.chars() {
+                    if let Some(&qc) = query_chars.peek() {
+                        if c == qc {
+                            query_chars.next();
+                        }
+                    }
+                }
+                if query_chars.peek().is_none() {
+                    Some((10 - (item.len() as i32), item))
+                } else {
+                    None
+                }
+            }
+        })
+        .collect();
+        
+    scored.sort_by(|a, b| b.0.cmp(&a.0));
+    scored.into_iter().map(|(_, item)| item.clone()).collect()
+}
+
+fn scan_path() -> Vec<String> {
+    let mut executables = std::collections::BTreeSet::new();
+    if let Ok(path_var) = std::env::var("PATH") {
+        for dir in path_var.split(':') {
+            if let Ok(entries) = std::fs::read_dir(dir) {
+                for entry in entries {
+                    if let Ok(entry) = entry {
+                        let path = entry.path();
+                        if path.is_file() {
+                            #[cfg(unix)]
+                            {
+                                use std::os::unix::fs::PermissionsExt;
+                                if let Ok(metadata) = entry.metadata() {
+                                    if metadata.permissions().mode() & 0o111 != 0 {
+                                        if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
+                                            executables.insert(name.to_string());
+                                        }
+                                    }
+                                }
+                            }
+                            #[cfg(not(unix))]
+                            {
+                                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
+                                    executables.insert(name.to_string());
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    executables.into_iter().collect()
+}
+
+pub struct FuzzelWidget {
+    x: f32,
+    y: f32,
+    w: f32,
+    h: f32,
+    prompt: String,
+    query: String,
+    all_items: Vec<String>,
+    filtered_items: Vec<String>,
+    selected: usize,
+    scroll_offset: usize,
+}
+
+impl FuzzelWidget {
+    pub fn new(prompt: String) -> Self {
+        Self {
+            x: 0.0,
+            y: 0.0,
+            w: 0.0,
+            h: 0.0,
+            prompt,
+            query: String::new(),
+            all_items: Vec::new(),
+            filtered_items: Vec::new(),
+            selected: 0,
+            scroll_offset: 0,
+        }
+    }
+
+    pub fn set_items(&mut self, items: Vec<String>) {
+        self.all_items = items;
+        self.filter();
+    }
+
+    pub fn filter(&mut self) {
+        self.filtered_items = filter_and_sort_items(&self.all_items, &self.query);
+        if self.selected >= self.filtered_items.len() {
+            self.selected = self.filtered_items.len().saturating_sub(1);
+        }
+        self.update_scroll();
+    }
+
+    pub fn update_scroll(&mut self) {
+        let visible_items = 13;
+        if self.selected >= self.scroll_offset + visible_items {
+            self.scroll_offset = self.selected - visible_items + 1;
+        } else if self.selected < self.scroll_offset {
+            self.scroll_offset = self.selected;
+        }
+    }
+}
+
+impl Widget for FuzzelWidget {
+    fn rect(&self) -> (f32, f32, f32, f32) {
+        (self.x, self.y, self.w, self.h)
+    }
+
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.x = x;
+        self.y = y;
+        self.w = w;
+        self.h = h;
+    }
+
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        let pad = 15.0;
+        let search_h = 35.0;
+        
+        // Search Bar Background
+        quads.push((
+            self.x + pad,
+            self.y + pad,
+            self.w - pad * 2.0,
+            search_h,
+            [0.10, 0.10, 0.14, 1.0],
+        ));
+
+        // Search Bar Border
+        let border_color = [0.25, 0.45, 0.85, 1.0];
+        let bx = self.x + pad;
+        let by = self.y + pad;
+        let bw = self.w - pad * 2.0;
+        let bh = search_h;
+        quads.push((bx, by, bw, 1.0, border_color));
+        quads.push((bx, by + bh - 1.0, bw, 1.0, border_color));
+        quads.push((bx, by, 1.0, bh, border_color));
+        quads.push((bx + bw - 1.0, by, 1.0, bh, border_color));
+
+        // Items list background highlight
+        let list_y = self.y + pad + search_h + 10.0;
+        let row_h = 25.0;
+        let visible_items = 13;
+
+        if !self.filtered_items.is_empty() && self.selected >= self.scroll_offset {
+            let relative_selected = self.selected - self.scroll_offset;
+            if relative_selected < visible_items {
+                let y = list_y + relative_selected as f32 * row_h;
+                quads.push((
+                    self.x + pad,
+                    y,
+                    self.w - pad * 2.0,
+                    row_h - 2.0,
+                    [0.20, 0.35, 0.65, 0.9],
+                ));
+            }
+        }
+
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        let pad = 15.0;
+        let search_h = 35.0;
+
+        let query_text = if self.query.is_empty() {
+            format!("{}{}", self.prompt, "Type to search...")
+        } else {
+            format!("{}{}", self.prompt, self.query)
+        };
+        let query_color = if self.query.is_empty() {
+            [0x66, 0x66, 0x77]
+        } else {
+            [0xcc, 0xff, 0xcc]
+        };
+
+        labels.push(TextLabel {
+            text: query_text,
+            x: self.x + pad + 10.0,
+            y: self.y + pad + 9.0,
+            font_size: 14.0,
+            color: query_color,
+        });
+
+        let list_y = self.y + pad + search_h + 10.0;
+        let row_h = 25.0;
+        let visible_items = 13;
+
+        let start = self.scroll_offset;
+        let end = self.filtered_items.len().min(start + visible_items);
+
+        for (i, idx) in (start..end).enumerate() {
+            let item_text = &self.filtered_items[idx];
+            let y = list_y + i as f32 * row_h + 4.0;
+            let color = if idx == self.selected {
+                [0xff, 0xff, 0xff]
+            } else {
+                [0xbb, 0xbb, 0xc5]
+            };
+
+            labels.push(TextLabel {
+                text: item_text.clone(),
+                x: self.x + pad + 10.0,
+                y,
+                font_size: 13.0,
+                color,
+            });
+        }
+
+        if self.filtered_items.is_empty() {
+            labels.push(TextLabel {
+                text: "No matches found".to_string(),
+                x: self.x + pad + 10.0,
+                y: list_y + 4.0,
+                font_size: 13.0,
+                color: [0x88, 0x88, 0x99],
+            });
+        }
+
+        labels
+    }
+
+    fn mouse_input(&mut self, button: winit::event::MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if button == winit::event::MouseButton::Left && state == ElementState::Pressed {
+            let pad = 15.0;
+            let search_h = 35.0;
+            let list_y = self.y + pad + search_h + 10.0;
+            let row_h = 25.0;
+            let visible_items = 13;
+
+            if px >= self.x + pad && px <= self.x + self.w - pad {
+                if py >= list_y && py < list_y + visible_items as f32 * row_h {
+                    let clicked_row = ((py - list_y) / row_h).floor() as usize;
+                    let target_idx = self.scroll_offset + clicked_row;
+                    if target_idx < self.filtered_items.len() {
+                        self.selected = target_idx;
+                        return true;
+                    }
+                }
+            }
+        }
+        false
+    }
+}
+
+struct StdinState {
+    items: Vec<String>,
+    new_data: bool,
+}
+
+struct State {
+    window: Arc<Window>,
+    surface: wgpu::Surface<'static>,
+    device: wgpu::Device,
+    queue: wgpu::Queue,
+    config: wgpu::SurfaceConfiguration,
+    render_pipeline: wgpu::RenderPipeline,
+    vertex_buffer: wgpu::Buffer,
+    vertex_count: u32,
+
+    fuzzel: FuzzelWidget,
+    font_system: FontSystem,
+    swash_cache: SwashCache,
+    text_atlas: TextAtlas,
+    text_renderer: TextRenderer,
+    text_viewport: Viewport,
+
+    cursor_x: f32,
+    cursor_y: f32,
+    width: f32,
+    height: f32,
+    physical_width: u32,
+    physical_height: u32,
+    scale: f64,
+
+    stdin_state: Arc<Mutex<StdinState>>,
+}
+
+impl State {
+    async fn new(window: Arc<Window>, prompt: String) -> Self {
+        let scale = window.scale_factor();
+        let physical_size = window.inner_size();
+        let pw = physical_size.width.max(1);
+        let ph = physical_size.height.max(1);
+        let lw = pw as f32 / scale as f32;
+        let lh = ph as f32 / scale as f32;
+
+        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
+            backends: wgpu::Backends::VULKAN,
+            ..Default::default()
+        });
+
+        let surface = instance
+            .create_surface(window.clone())
+            .expect("Failed to create surface");
+
+        let adapter = instance
+            .request_adapter(&wgpu::RequestAdapterOptions {
+                power_preference: wgpu::PowerPreference::HighPerformance,
+                compatible_surface: Some(&surface),
+                force_fallback_adapter: false,
+            })
+            .await
+            .expect("Failed to find adapter");
+
+        let (device, queue) = adapter
+            .request_device(
+                &wgpu::DeviceDescriptor {
+                    label: Some("GPU Device"),
+                    required_features: wgpu::Features::empty(),
+                    required_limits: wgpu::Limits::downlevel_webgl2_defaults()
+                        .using_resolution(adapter.limits()),
+                    memory_hints: wgpu::MemoryHints::MemoryUsage,
+                },
+                None,
+            )
+            .await
+            .expect("Failed to create device");
+
+        let mut config = surface
+            .get_default_config(&adapter, pw, ph)
+            .expect("Failed to get surface config");
+
+        let capabilities = surface.get_capabilities(&adapter);
+        let alpha_mode = if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
+            wgpu::CompositeAlphaMode::PreMultiplied
+        } else if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
+            wgpu::CompositeAlphaMode::PostMultiplied
+        } else {
+            capabilities.alpha_modes[0]
+        };
+        config.alpha_mode = alpha_mode;
+        surface.configure(&device, &config);
+
+        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("Shader"),
+            source: wgpu::ShaderSource::Wgsl(clear_ui::SHADER.into()),
+        });
+
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("Pipeline Layout"),
+            bind_group_layouts: &[],
+            push_constant_ranges: &[],
+        });
+
+        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("Render Pipeline"),
+            layout: Some(&pipeline_layout),
+            vertex: wgpu::VertexState {
+                module: &shader,
+                entry_point: Some("vs_main"),
+                buffers: &[Vertex::desc()],
+                compilation_options: Default::default(),
+            },
+            fragment: Some(wgpu::FragmentState {
+                module: &shader,
+                entry_point: Some("fs_main"),
+                targets: &[Some(wgpu::ColorTargetState {
+                    format: config.format,
+                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+                    write_mask: wgpu::ColorWrites::ALL,
+                })],
+                compilation_options: Default::default(),
+            }),
+            primitive: wgpu::PrimitiveState {
+                topology: wgpu::PrimitiveTopology::TriangleList,
+                strip_index_format: None,
+                front_face: wgpu::FrontFace::Ccw,
+                cull_mode: None,
+                polygon_mode: wgpu::PolygonMode::Fill,
+                unclipped_depth: false,
+                conservative: false,
+            },
+            depth_stencil: None,
+            multisample: wgpu::MultisampleState {
+                count: 1,
+                mask: !0,
+                alpha_to_coverage_enabled: false,
+            },
+            multiview: None,
+            cache: None,
+        });
+
+        let font_system = FontSystem::new();
+        let swash_cache = SwashCache::new();
+        let cache = Cache::new(&device);
+        let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
+        let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
+
+        let mut text_viewport = Viewport::new(&device, &cache);
+        text_viewport.update(&queue, Resolution { width: pw, height: ph });
+
+        let mut fuzzel = FuzzelWidget::new(prompt);
+        fuzzel.set_rect(0.0, 0.0, lw, lh);
+
+        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+            label: Some("Vertex Buffer"),
+            size: 1,
+            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+            mapped_at_creation: false,
+        });
+
+        let stdin_state = Arc::new(Mutex::new(StdinState {
+            items: Vec::new(),
+            new_data: false,
+        }));
+
+        if !io::stdin().is_terminal() {
+            let stdin_state_clone = stdin_state.clone();
+            let window_clone = window.clone();
+            std::thread::spawn(move || {
+                let stdin = io::stdin();
+                for line in stdin.lock().lines() {
+                    if let Ok(line) = line {
+                        if let Ok(mut lock_state) = stdin_state_clone.lock() {
+                            lock_state.items.push(line);
+                            lock_state.new_data = true;
+                        }
+                        window_clone.request_redraw();
+                    }
+                }
+            });
+        } else {
+            let path_items = scan_path();
+            if let Ok(mut lock_state) = stdin_state.lock() {
+                lock_state.items = path_items;
+                lock_state.new_data = true;
+            }
+        }
+
+        let mut state = Self {
+            window,
+            surface,
+            device,
+            queue,
+            config,
+            render_pipeline,
+            vertex_buffer,
+            vertex_count: 0,
+            fuzzel,
+            font_system,
+            swash_cache,
+            text_atlas,
+            text_renderer,
+            text_viewport,
+            cursor_x: 0.0,
+            cursor_y: 0.0,
+            width: lw,
+            height: lh,
+            physical_width: pw,
+            physical_height: ph,
+            scale,
+            stdin_state,
+        };
+
+        state.check_stdin_updates();
+        state.apply_layout();
+        state.upload_vertices();
+        state
+    }
+
+    fn check_stdin_updates(&mut self) -> bool {
+        if let Ok(mut lock) = self.stdin_state.lock() {
+            if lock.new_data {
+                lock.new_data = false;
+                let items = lock.items.clone();
+                self.fuzzel.set_items(items);
+                return true;
+            }
+        }
+        false
+    }
+
+    fn apply_layout(&mut self) {
+        let (w, h) = (self.width, self.height);
+        self.fuzzel.set_rect(0.0, 0.0, w, h);
+    }
+
+    fn collect_vertices(&self) -> Vec<Vertex> {
+        let sw = self.width;
+        let sh = self.height;
+        widget_vertices(&self.fuzzel, sw, sh)
+    }
+
+    fn upload_vertices(&mut self) {
+        let verts = self.collect_vertices();
+        self.vertex_count = verts.len() as u32;
+        if self.vertex_count == 0 {
+            return;
+        }
+        let data = bytemuck::cast_slice(&verts);
+        let needed = data.len() as wgpu::BufferAddress;
+        if needed > self.vertex_buffer.size() {
+            self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
+                label: Some("Vertex Buffer"),
+                size: needed,
+                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                mapped_at_creation: false,
+            });
+        }
+        self.queue.write_buffer(&self.vertex_buffer, 0, data);
+    }
+
+    fn prepare_text(&mut self) {
+        let Self {
+            ref mut text_renderer,
+            ref device,
+            ref queue,
+            ref mut font_system,
+            ref mut text_atlas,
+            ref mut text_viewport,
+            ref mut swash_cache,
+            physical_width,
+            physical_height,
+            scale,
+            ref fuzzel,
+            ..
+        } = self;
+
+        let viewport = Resolution { width: *physical_width, height: *physical_height };
+        text_viewport.update(queue, viewport);
+
+        let scale_f32 = *scale as f32;
+
+        let mut areas: Vec<TextArea> = Vec::new();
+        let mut widget_buffers: Vec<Buffer> = Vec::new();
+        let mut widget_labels: Vec<TextLabel> = Vec::new();
+
+        for label in fuzzel.text_labels() {
+            widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
+            widget_labels.push(label);
+        }
+
+        for (buf, label) in widget_buffers.iter().zip(widget_labels.iter()) {
+            areas.push(TextArea {
+                buffer: buf,
+                left: label.x * scale_f32,
+                top: label.y * scale_f32,
+                scale: scale_f32,
+                bounds: TextBounds {
+                    left: 0,
+                    top: 0,
+                    right: *physical_width as i32,
+                    bottom: *physical_height as i32,
+                },
+                default_color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
+                custom_glyphs: &[],
+            });
+        }
+
+        text_renderer
+            .prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache)
+            .unwrap();
+    }
+
+    fn resize(&mut self, new_size: PhysicalSize<u32>) {
+        if new_size.width > 0 && new_size.height > 0 {
+            self.physical_width = new_size.width;
+            self.physical_height = new_size.height;
+            self.width = new_size.width as f32 / self.scale as f32;
+            self.height = new_size.height as f32 / self.scale as f32;
+            self.config.width = new_size.width;
+            self.config.height = new_size.height;
+            self.surface.configure(&self.device, &self.config);
+            self.apply_layout();
+            self.upload_vertices();
+        }
+    }
+
+    fn render(&mut self) {
+        self.prepare_text();
+
+        let output = match self.surface.get_current_texture() {
+            Ok(t) => t,
+            Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
+                self.surface.configure(&self.device, &self.config);
+                return;
+            }
+            Err(wgpu::SurfaceError::Timeout) => return,
+            Err(e) => {
+                eprintln!("Surface error: {e:?}");
+                return;
+            }
+        };
+
+        let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+            label: Some("Encoder"),
+        });
+
+        {
+            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("Render Pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: &view,
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(wgpu::Color {
+                            r: 0.05, g: 0.05, b: 0.08, a: 0.92,
+                        }),
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: None,
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+
+            if self.vertex_count > 0 {
+                pass.set_pipeline(&self.render_pipeline);
+                pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
+                pass.draw(0..self.vertex_count, 0..1);
+            }
+
+            self.text_renderer.render(&self.text_atlas, &self.text_viewport, &mut pass).unwrap();
+        }
+
+        self.queue.submit(std::iter::once(encoder.finish()));
+        self.window.pre_present_notify();
+        output.present();
+    }
+}
+
+struct App {
+    state: Option<State>,
+    prompt: String,
+}
+
+impl App {
+    fn new(prompt: String) -> Self {
+        Self { state: None, prompt }
+    }
+}
+
+impl ApplicationHandler for App {
+    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
+        if self.state.is_some() { return; }
+
+        let monitor = event_loop.primary_monitor().or_else(|| event_loop.available_monitors().next());
+        let (width, height) = (600, 400);
+        let mut window_attributes = WindowAttributes::default()
+            .with_title("clear-cloud")
+            .with_decorations(false)
+            .with_inner_size(LogicalSize::new(width, height))
+            .with_transparent(true);
+
+        if let Some(monitor) = monitor {
+            let monitor_size = monitor.size();
+            let scale_factor = monitor.scale_factor();
+            let monitor_w = monitor_size.width as f64 / scale_factor;
+            let monitor_h = monitor_size.height as f64 / scale_factor;
+            let x = (monitor_w - width as f64) / 2.0;
+            let y = (monitor_h - height as f64) / 2.0;
+            window_attributes = window_attributes.with_position(LogicalPosition::new(x, y));
+        }
+
+        let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
+        let state = pollster::block_on(State::new(window, self.prompt.clone()));
+        self.state = Some(state);
+        self.state.as_ref().unwrap().window.request_redraw();
+    }
+
+    fn window_event(
+        &mut self,
+        event_loop: &ActiveEventLoop,
+        _window_id: winit::window::WindowId,
+        event: WindowEvent,
+    ) {
+        if self.state.is_none() { return; }
+        let state = self.state.as_mut().unwrap();
+
+        let needs_redraw = match event {
+            WindowEvent::CloseRequested => {
+                event_loop.exit();
+                true
+            }
+            WindowEvent::Resized(size) => {
+                state.resize(size);
+                true
+            }
+            WindowEvent::RedrawRequested => {
+                if state.check_stdin_updates() {
+                    state.apply_layout();
+                    state.upload_vertices();
+                }
+                state.render();
+                false
+            }
+            WindowEvent::ScaleFactorChanged { scale_factor, mut inner_size_writer } => {
+                let new_physical = winit::dpi::PhysicalSize::new(
+                    (state.width as f64 * scale_factor) as u32,
+                    (state.height as f64 * scale_factor) as u32,
+                );
+                let _ = inner_size_writer.request_inner_size(new_physical);
+                state.scale = scale_factor;
+                state.resize(new_physical);
+                true
+            }
+            WindowEvent::CursorMoved { position, .. } => {
+                state.cursor_x = position.x as f32 / state.scale as f32;
+                state.cursor_y = position.y as f32 / state.scale as f32;
+                false
+            }
+            WindowEvent::MouseInput { state: btn_state, button, .. } => {
+                if button == winit::event::MouseButton::Left && btn_state == ElementState::Pressed {
+                    let prev_selected = state.fuzzel.selected;
+                    let changed = state.fuzzel.mouse_input(button, btn_state, state.cursor_x, state.cursor_y);
+                    if changed {
+                        if state.fuzzel.selected == prev_selected {
+                            if let Some(item) = state.fuzzel.filtered_items.get(state.fuzzel.selected) {
+                                println!("{}", item);
+                                std::process::exit(0);
+                            }
+                        }
+                        state.upload_vertices();
+                        true
+                    } else {
+                        false
+                    }
+                } else {
+                    false
+                }
+            }
+            WindowEvent::KeyboardInput { event, .. } => {
+                if event.state == ElementState::Pressed {
+                    let mut handled = true;
+                    match &event.logical_key {
+                        Key::Named(NamedKey::Escape) => {
+                            std::process::exit(0);
+                        }
+                        Key::Named(NamedKey::Enter) => {
+                            if let Some(item) = state.fuzzel.filtered_items.get(state.fuzzel.selected) {
+                                println!("{}", item);
+                                std::process::exit(0);
+                            }
+                        }
+                        Key::Named(NamedKey::ArrowDown) => {
+                            if !state.fuzzel.filtered_items.is_empty() {
+                                state.fuzzel.selected = (state.fuzzel.selected + 1).min(state.fuzzel.filtered_items.len() - 1);
+                                state.fuzzel.update_scroll();
+                                state.upload_vertices();
+                            }
+                        }
+                        Key::Named(NamedKey::ArrowUp) => {
+                            if state.fuzzel.selected > 0 {
+                                state.fuzzel.selected -= 1;
+                                state.fuzzel.update_scroll();
+                                state.upload_vertices();
+                            }
+                        }
+                        Key::Named(NamedKey::Backspace) => {
+                            state.fuzzel.query.pop();
+                            state.fuzzel.filter();
+                            state.upload_vertices();
+                        }
+                        _ => {
+                            if let Some(text) = &event.text {
+                                for ch in text.chars().filter(|c| !c.is_control()) {
+                                    state.fuzzel.query.push(ch);
+                                }
+                                state.fuzzel.filter();
+                                state.upload_vertices();
+                            } else {
+                                handled = false;
+                            }
+                        }
+                    }
+                    handled
+                } else {
+                    false
+                }
+            }
+            _ => false,
+        };
+
+        if needs_redraw {
+            state.window.request_redraw();
+        }
+    }
+}
+
+fn main() {
+    let mut prompt = "Search: ".to_string();
+    let mut args = std::env::args().skip(1);
+    while let Some(arg) = args.next() {
+        if arg == "-p" || arg == "--prompt" {
+            if let Some(p) = args.next() {
+                prompt = p;
+            }
+        }
+    }
+
+    let event_loop = EventLoop::new().unwrap();
+    event_loop.set_control_flow(ControlFlow::Wait);
+
+    let mut app = App::new(prompt);
+    event_loop.run_app(&mut app).unwrap();
+}