rust 使用 GPUI 在 Windows 系统创建一个修改环境变量的 GUI 应用

136 阅读3分钟

问题

很早之前遇到一个问题,命令行怎样通过代理去访问被墙的网站。

比如,在没有科学上网的情况下:

curl google.com
curl: (28) Failed to connect to google.com port 80 after 21074 ms: Could not connect to server

网上的大多数教程,都是说在环境变量增加 http_proxy 和 https_proxy

image.png

配置好之后,再重新打开“命令行提示符”执行:

curl google.com
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

这样就可以解决问题了。 但是当不需要通过代理访问时,又需要手动删除掉这两个环境变量。

需要(手动添加)<-> 不需要(手动删除),在这两者之间切换时,每次手动去添加和删除就很痛苦,有时嫌麻烦,索性就不访问被墙的网站了。

我没有找到合适的客户端(GUI),如果你知道有好用的客户端能解决我这个痛点,麻烦评论区告诉我。

既然没有好用的 GUI 应用,那就撸起袖子自己写一个。

最近 Zed 发布了 Windows 版本,它的 gpui 库也发布到 creates.io 上了。

那这次就用 gpui 来练练手。

开发

开发前准备

  • 操作系统: Windows 11 专业版(24H2)
  • 编辑器: Zed
  • rustc: rustc 1.88.0 (6b00bc388 2025-06-23)
  • 编译工具链: stable-x86_64-pc-windows-msvc (active, default)
  • 配置好 crates.io 镜像代理,参考 RsProxy

创建项目并添加依赖

cargo new env_gui

zed env_gui

cargo add gpui tokio

写代码

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use gpui::{
    App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
    prelude::*, px, rgb, size,
};
use std::process::Command;
use tokio::task;

async fn set_proxy_vars(port: SharedString) {
    task::spawn_blocking(move || {
        let proxy_url = format!("http://127.0.0.1:{port}");
        set_env_item("https_proxy", &proxy_url);
        set_env_item("http_proxy", &proxy_url);
        println!("set proxy vars end");
    })
    .await
    .ok();
}

// 设置用户环境变量
fn set_env_item(key: &str, value: &String) {
    let mut binding = Command::new("setx");
    let command = binding.args([key, value.as_str()]);
    #[cfg(target_os = "windows")]
    {
        use std::os::windows::process::CommandExt;
        command.creation_flags(0x08000000);
    }
    command.status().expect("Failed to set {key}");
}

async fn unset_proxy_vars() {
    task::spawn_blocking(move || {
        unset_env_item("http_proxy");
        unset_env_item("https_proxy");
        println!("unset proxy vars end");
    })
    .await
    .ok();
}

// 删除用户环境变量
fn unset_env_item(key: &str) {
    let mut binding = Command::new("reg");
    let command = binding.args(["delete", "HKCU\\Environment", "/v", key, "/f"]);
    #[cfg(target_os = "windows")]
    {
        use std::os::windows::process::CommandExt;
        command.creation_flags(0x08000000);
    }
    command.status().expect("Failed to remove {key}");
}

struct ProxyClient {
    port: SharedString,
    processing: bool,
}

impl Render for ProxyClient {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let text = if self.processing { "处理中..." } else { "" };
        div()
            .flex()
            .flex_col()
            .gap_3()
            .bg(rgb(0x505050))
            .size_full()
            .justify_center()
            .items_center()
            .text_xl()
            .text_color(rgb(0xffffff))
            .child(div().child(text).absolute().top(px(10.0)))
            .child(
                div()
                    .flex()
                    .gap_2()
                    .child(
                        div()
                            .bg(gpui::green())
                            .px_2()
                            .child("添加")
                            .cursor(gpui::CursorStyle::PointingHand)
                            .id("start")
                            .on_click(cx.listener(move |this, _, _window, cx| {
                                this.processing = true;
                                cx.notify();

                                let port = this.port.clone();
                                cx.spawn(async move |this, cx| {
                                    set_proxy_vars(port).await;
                                    let _ = this.update(cx, |this, cx| {
                                        this.processing = false;
                                        cx.notify();
                                    });
                                })
                                .detach();
                            })),
                    )
                    .child(
                        div()
                            .bg(gpui::red())
                            .px_2()
                            .child("移除")
                            .cursor(gpui::CursorStyle::PointingHand)
                            .id("stop")
                            .on_click(cx.listener(move |this, _, _window, cx| {
                                this.processing = true;
                                cx.notify();

                                cx.spawn(async move |this, cx| {
                                    unset_proxy_vars().await;
                                    let _ = this.update(cx, |this, cx| {
                                        this.processing = false;
                                        cx.notify();
                                    });
                                })
                                .detach();
                            })),
                    ),
            )
    }
}

#[tokio::main]
async fn main() {
    Application::new().run(|cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(500.), px(250.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |_, cx| {
                cx.new(|_| ProxyClient {
                    port: "9999".into(),
                    processing: false,
                })
            },
        )
        .unwrap();
    });
}

Cargo.toml

[package]
name = "env_gui"
version = "0.0.1"
edition = "2024"

[dependencies]
gpui = "0.2.2"
tokio = {version = "1.48.0", features = ["full"]}

[profile.release]
opt-level = 's'
lto = true
strip = true

效果

  • 效果图

image.png

image.png

image.png

  • exe 大小:4928 KB(4.18 MB) image.png

  • 内存占用 image.png

参考

gpui