#![cfg_attr(target_family = "wasm", no_main)] #[cfg(not(target_family = "wasm"))] use std::{ io::{self, BufRead as _}, sync::mpsc, time::Duration, }; #[cfg(not(target_family = "wasm"))] use gpui::{ AnyWindowHandle, App, AppContext as _, AsyncApp, Context, Entity, GraphicalEnvironment, QuitMode, Render, Subscription, TitlebarOptions, Window, WindowOptions, WindowingRequest, div, prelude::*, }; #[cfg(not(target_family = "wasm"))] struct Todo { message: String, } #[cfg(not(target_family = "wasm"))] struct Todos { items: Vec>, window: Option, } #[cfg(not(target_family = "wasm"))] struct TodoWindow { todos: Entity, _todos_subscription: Subscription, } #[cfg(not(target_family = "wasm"))] impl Render for TodoWindow { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let rows = self .todos .read(cx) .items .iter() .enumerate() .map(|(index, todo)| div().child(describe_todo(index, todo, cx))) .collect::>(); div() .size_full() .bg(gpui::rgb(0xffffff)) .text_color(gpui::rgb(0x202020)) .flex() .flex_col() .gap_2() .p_4() .child("Switchable display todos") .child("Use the terminal: create [message], ls, close, open, quit") .when(rows.is_empty(), |view| { view.child("No todos yet. Type `create` in the terminal.") }) .children(rows) } } /// English, Chinese, Japanese and Korean for a word, then an emoji for it. #[cfg(not(target_family = "wasm"))] const SAMPLE_TODOS: [&str; 5] = [ "milk / 牛奶 / 牛乳 / 우유 🥛", "apple / 苹果 / りんご / 사과 🍎", "cat / 猫 / ねこ / 고양이 🐱", "book / 书 / 本 / 책 📚", "rain / 雨 / あめ / 비 🌧️", ]; #[cfg(not(target_family = "wasm"))] fn describe_todo(index: usize, todo: &Entity, cx: &App) -> String { let todo_reference: &Todo = todo.read(cx); format!( "{index} - {} - {:?} @{:p}", todo_reference.message, todo.entity_id(), todo_reference ) } #[cfg(not(target_family = "wasm"))] fn print_todos(todos: &Entity, cx: &App) { if todos.read(cx).items.is_empty() { println!("no todos"); } for (index, todo) in todos.read(cx).items.iter().enumerate() { println!("{}", describe_todo(index, todo, cx)); } } #[cfg(not(target_family = "wasm"))] fn open_window(todos: &Entity, cx: &mut App) -> anyhow::Result<()> { if todos.read(cx).window.is_some() { return Ok(()); } let options = WindowOptions { titlebar: Some(TitlebarOptions { title: Some("Switchable display todos".into()), ..Default::default() }), ..Default::default() }; let handle = cx.open_window(options, |_, cx| { cx.new(|cx| TodoWindow { todos: todos.clone(), _todos_subscription: cx.observe(todos, |_, _, cx| cx.notify()), }) })?; // On macOS, making the window key does not activate the app over the terminal. cx.activate(true); handle.update(cx, |_, window, _| window.activate_window())?; todos.update(cx, |todos, _| todos.window = Some(handle.into())); Ok(()) } /// Builds the environment for `open`: the given `NAME=value` pairs, or this process's /// environment when none are given. #[cfg(target_os = "linux")] fn display_environment(arguments: &str) -> anyhow::Result { if arguments.trim().is_empty() { return Ok(GraphicalEnvironment::detect()); } let mut environment = GraphicalEnvironment::default(); for assignment in arguments.split_whitespace() { let (name, value) = assignment .split_once('=') .ok_or_else(|| anyhow::anyhow!("expected NAME=value, got {assignment:?}"))?; match name { "WAYLAND_DISPLAY" => environment.wayland_display = Some(value.into()), "DISPLAY" => environment.x11_display = Some(value.into()), "XDG_RUNTIME_DIR" => environment.xdg_runtime_dir = Some(value.into()), "XDG_ACTIVATION_TOKEN" => environment.activation_token = Some(value.into()), _ => anyhow::bail!("unknown display variable {name}"), } } Ok(environment) } /// Builds the environment for `open`: `SESSION=id`, or this process's session when not given. #[cfg(target_os = "windows")] fn display_environment(arguments: &str) -> anyhow::Result { let mut environment = GraphicalEnvironment::detect(); for assignment in arguments.split_whitespace() { let session = assignment .strip_prefix("SESSION=") .ok_or_else(|| anyhow::anyhow!("expected SESSION=id, got {assignment:?}"))?; environment.session_id = Some(session.parse()?); } Ok(environment) } /// Builds the environment for `open`, which carries nothing on this platform. #[cfg(not(any(target_os = "linux", target_os = "windows", target_family = "wasm")))] fn display_environment(_arguments: &str) -> anyhow::Result { Ok(GraphicalEnvironment::detect()) } #[cfg(not(target_family = "wasm"))] fn handle_command( command: &str, todos: &Entity, cx: &mut App, ) -> anyhow::Result> { let (name, arguments) = command.split_once(' ').unwrap_or((command, "")); match name { "ls" => print_todos(todos, cx), "create" => { // A sample with CJK text and an emoji, so that drawing todos exercises color glyph // rasterization, which uses the GPU devices a switch to windowed mode creates. let count = todos.read(cx).items.len(); let sample = SAMPLE_TODOS[count % SAMPLE_TODOS.len()]; let message = if arguments.trim().is_empty() { sample.to_owned() } else { format!("{arguments} · {sample}") }; let todo = cx.new(|_| Todo { message }); todos.update(cx, |todos, cx| { todos.items.push(todo); println!("todo added, total {}", todos.items.len()); cx.notify(); }); } "open" => { return Ok(Some(WindowingRequest::Windowed(display_environment( arguments, )?))); } "close" => { let window = todos.update(cx, |todos, _| todos.window.take()); if let Some(window) = window { window.update(cx, |_, window, _| window.remove_window())?; } return Ok(Some(WindowingRequest::Headless)); } "quit" => cx.quit(), "" => {} _ => println!("{USAGE}"), } Ok(None) } #[cfg(target_os = "linux")] const USAGE: &str = "commands: ls | create [message] | open [DISPLAY=… WAYLAND_DISPLAY=… XDG_RUNTIME_DIR=… XDG_ACTIVATION_TOKEN=…] | close | quit"; #[cfg(target_os = "windows")] const USAGE: &str = "commands: ls | create [message] | open [SESSION=…] | close | quit"; #[cfg(not(any(target_os = "linux", target_os = "windows", target_family = "wasm")))] const USAGE: &str = "commands: ls | create [message] | open | close | quit"; #[cfg(not(target_family = "wasm"))] async fn run_command( command: String, todos: &Entity, cx: &mut AsyncApp, ) -> anyhow::Result<()> { match cx.update(|cx| handle_command(&command, todos, cx))? { None => return Ok(()), Some(request @ WindowingRequest::Headless) => { if cx.update(|cx| cx.graphical_environment()).is_some() { cx.update(|cx| cx.request_windowing(request)).await?; } } Some(request @ WindowingRequest::Windowed(_)) => { // Switching fails while windowed, so `open` then just opens the window. if cx.update(|cx| cx.graphical_environment()).is_none() { cx.update(|cx| cx.request_windowing(request)).await?; } cx.update(|cx| open_window(todos, cx))?; } } let mode = cx.update(|cx| match cx.graphical_environment() { None => "headless".to_owned(), Some(_) => match cx.compositor_name() { "" => "windowed".to_owned(), compositor => compositor.to_owned(), }, }); println!("mode: {mode}"); Ok(()) } #[cfg(not(target_family = "wasm"))] fn main() { #[cfg(target_os = "linux")] let application = gpui_platform::linux(gpui::WindowingModes::all()); #[cfg(not(target_os = "linux"))] let application = gpui_platform::application(); application .with_windowing(WindowingRequest::Headless) .with_quit_mode(QuitMode::Explicit) .run(|cx| { let todos = cx.new(|_| Todos { items: Vec::new(), window: None, }); let window_closed_subscription = cx.on_window_closed({ let todos = todos.clone(); move |cx, window_id| { todos.update(cx, |todos, _| { if todos .window .as_ref() .is_some_and(|window| window.window_id() == window_id) { todos.window = None; } }); } }); let (command_sender, command_receiver) = mpsc::channel(); std::thread::spawn(move || { for line in io::stdin().lock().lines() { match line { Ok(line) => { if command_sender.send(line).is_err() { break; } } Err(error) => { eprintln!("stdin error: {error}"); break; } } } }); println!("{USAGE}"); cx.spawn(async move |cx| { let _window_closed_subscription = window_closed_subscription; loop { cx.background_executor() .timer(Duration::from_millis(25)) .await; while let Ok(command) = command_receiver.try_recv() { if let Err(error) = run_command(command, &todos, cx).await { println!("error: {error:#}"); } } } }) .detach(); }); }