mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
979275af1f | ||
|
|
ede99b309c | ||
|
|
c307d63d3b | ||
|
|
4c53d424a7 | ||
|
|
39f2e847d8 | ||
|
|
7d96b3dcf6 |
No files matched your search
@@ -108,7 +108,7 @@ The first run asks three questions (data folder, port, password; Enter takes the
|
||||
|
||||
**[Download Codeman.dmg](https://github.com/Ark0N/Codeman/releases/latest/download/Codeman.dmg)** · one universal app for Apple Silicon and Intel · macOS 11.3 or later
|
||||
|
||||
A small native app that opens Codeman in its own window. On a Mac without Codeman it installs it for you, with no Terminal and no administrator password, shows the install as a checklist, and offers to install a coding agent if the Mac has none yet. On a Mac that already runs Codeman it finds the server and opens it, or starts it when it is stopped. Under **Another machine** it connects to Codeman on your Mac mini, a home server or your tailnet, with the password kept in the Keychain, and can start an install over there through SSH. On a MacBook Pro with a PROLED XDR display, whose lid logo lights up, **View > Blink Apple Logo** blinks the logo by what your agents are doing, with a style of your own for working, for waiting for you and for needing permission; all three stay off until you choose. The app is Rust around macOS's own WebKit, not a bundled browser, so the download is under 5 MB.
|
||||
A small native app that opens Codeman in its own window. On a Mac without Codeman it installs it for you, with no Terminal and no administrator password, shows the install as a checklist, and offers to install a coding agent if the Mac has none yet. On a Mac that already runs Codeman it finds the server and opens it, or starts it when it is stopped. Under **Another machine** it connects to Codeman on your Mac mini, a home server or your tailnet, with the password kept in the Keychain, and can start an install over there through SSH. On a MacBook Pro with a PROLED XDR display, whose lid logo lights up, **View > Blink Apple Logo** makes the logo show what your agents are doing: it breathes while they work, blinks when one waits for you and flashes when one needs permission, and each state can take a style of your own. It stays off until you turn it on. The app is Rust around macOS's own WebKit, not a bundled browser, so the download is under 5 MB.
|
||||
|
||||
Open `Codeman.dmg`, drag Codeman into Applications and open it from there. macOS asks once whether to open an app downloaded from the internet: click **Open**.
|
||||
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ bash docker/Start-Codeman.sh
|
||||
|
||||
**[下载 Codeman.dmg](https://github.com/Ark0N/Codeman/releases/latest/download/Codeman.dmg)** · 同一个通用应用,适用于 Apple 芯片和 Intel · macOS 11.3 或更高版本
|
||||
|
||||
一个小巧的原生应用,在它自己的窗口里打开 Codeman。在还没有 Codeman 的 Mac 上,它会替你安装,不用终端,也不需要管理员密码,用一张清单显示安装进度;如果这台 Mac 上还没有编程智能体,它还会提出帮你装一个。在已经运行 Codeman 的 Mac 上,它会找到服务器并打开;服务器停着时则替你启动。在 **Another machine** 下,它可以连接你的 Mac mini、家用服务器或 tailnet 上的 Codeman,密码保存在钥匙串里,还能通过 SSH 在那台机器上开始安装。在配备 PROLED XDR 显示屏、机盖 Apple 标志会发光的 MacBook Pro 上,**View > Blink Apple Logo** 会让标志按智能体在做的事闪烁:工作中、等你回复、需要权限各用一种你自己选的样式;在你选定之前,三种都保持关闭。这个应用用 Rust 写成,套在 macOS 自带的 WebKit 外面,不捆绑浏览器,所以下载不到 5 MB。
|
||||
一个小巧的原生应用,在它自己的窗口里打开 Codeman。在还没有 Codeman 的 Mac 上,它会替你安装,不用终端,也不需要管理员密码,用一张清单显示安装进度;如果这台 Mac 上还没有编程智能体,它还会提出帮你装一个。在已经运行 Codeman 的 Mac 上,它会找到服务器并打开;服务器停着时则替你启动。在 **Another machine** 下,它可以连接你的 Mac mini、家用服务器或 tailnet 上的 Codeman,密码保存在钥匙串里,还能通过 SSH 在那台机器上开始安装。在配备 PROLED XDR 显示屏、机盖 Apple 标志会发光的 MacBook Pro 上,**View > Blink Apple Logo** 会让标志显示智能体在做的事:工作时缓慢呼吸,有智能体等你回复时闪烁,需要权限时快闪三下;每种状态也可以换成你自己选的样式。在你打开之前,它保持关闭。这个应用用 Rust 写成,套在 macOS 自带的 WebKit 外面,不捆绑浏览器,所以下载不到 5 MB。
|
||||
|
||||
打开 `Codeman.dmg`,把 Codeman 拖进「应用程序」文件夹,再从那里打开。macOS 会问一次是否打开这个从互联网下载的应用:点**打开**。
|
||||
|
||||
|
||||
@@ -248,9 +248,10 @@ A small native app that opens Codeman in its own window. Its first screen has tw
|
||||
runs the installer on that machine over SSH.
|
||||
|
||||
On a MacBook Pro with a PROLED XDR display, whose lid logo lights up,
|
||||
**View > Blink Apple Logo** blinks the logo by what your agents are doing, with a style of
|
||||
your own for working, for waiting for you and for needing permission. All three are off
|
||||
until you choose, and the menu only exists on such a Mac.
|
||||
**View > Blink Apple Logo** makes the logo show what your agents are doing: it breathes while
|
||||
they work, blinks when one waits for you and flashes three times when one needs permission.
|
||||
Each state can take another style under **View > Apple Logo Blink Styles**. It is off until
|
||||
you turn it on, and the menu only exists on such a Mac.
|
||||
|
||||
### First launch
|
||||
|
||||
|
||||
+90
-35
@@ -40,7 +40,7 @@ cannot do.
|
||||
| **Links** | On Codeman's own pages (the dashboard and session windows), a link to another site opens in your default browser, also on `⌘`-click or a middle click. Pages can send at most 3 links to the browser at once, then 1 per second. Inside a rendered HTML preview or a web tab, a plain link to another site does nothing, while one that asks for a new window (`target="_blank"`, `window.open`) opens in your default browser within the same budget (see Known limits). |
|
||||
| **Downloads** | Codeman's Download links, and downloads Codeman opens in a new window, save the file to `~/Downloads` whatever its type, and a toast in Codeman names the file. Files the web view cannot display are saved the same way. |
|
||||
| **A page that stops** | When the web content process of a window ends (a WebKit crash, or macOS taking the process back under memory pressure), the window loads its page again instead of staying blank. A second time within 30 seconds, the main window goes back to the connect screen and a pop-out closes, so a page that cannot stay up never becomes a reload loop. |
|
||||
| **Apple logo** | On a MacBook Pro with a PROLED XDR display, the lit Apple logo on the lid says what the agents are doing, with a blink style of your own for each state: **View > Blink Apple Logo > While Working**, **Waiting for You (Yellow)** and **Needs Permission (Red)**. Off, Calm, Slow, Wink, Heartbeat, Fast, or Alarm, three sharp flashes for a state that needs you. All three are off until you choose. When several states hold, the most urgent one with a style shows: red, then yellow, then work. A style chosen in an earlier version stays as the one for work, and yellow and red start as Wink and Alarm. The menu only exists on a Mac with that logo. No password is needed. Shell tabs do not count. The app asks the server every two seconds, and a change has to hold for three seconds before the logo follows it, so an agent between two turns does not flicker it. It keeps working while the window is hidden, and with any Codeman server. A logo you switched off yourself stays off, and if the setting changes while a blink runs, the blink ends and leaves it as it found it. While the app is closed nothing blinks, and macOS keeps the logo dark while the display sleeps or the lid is closed, so the blink pauses then and writes nothing. |
|
||||
| **Apple logo** | On a MacBook Pro with a PROLED XDR display, the lit Apple logo on the lid says what the agents are doing. **View > Blink Apple Logo** is the one switch, off until you turn it on. The more the logo moves, the more it needs you: it breathes (a slow fade out and back in) while agents work, blinks every two seconds for a yellow tab and flashes three times, then rests, for a red one. Each state can take another style under **View > Apple Logo Blink Styles**: Off, Breathe, Calm, Slow, Wink, Heartbeat, Fast, or Alarm. Picking a style turns the switch on, and turning the switch on with every state off brings back the recommended three (Breathe, Calm, Alarm). When several states hold, the most urgent one with a style shows: red, then yellow, then work. Styles chosen in 1.50.0 are kept, with the switch on if any of them was. The menu only exists on a Mac with that logo. No password is needed. Shell tabs do not count. The app asks the server every two seconds, and a change has to hold for three seconds before the logo follows it, so an agent between two turns does not flicker it. It keeps working while the window is hidden, and with any Codeman server. The logo moves on its own hardware fades, the ones [LogoLight](https://github.com/Ark0N/LogoLight) uses, so a blink writes no setting, and a logo you switched off yourself stays off. While LogoLight runs, Codeman leaves the logo to it. While the app is closed nothing blinks. With the lid closed it just goes on while the Mac stays awake on its power adapter (**Keep This Mac Awake > With the Lid Closed…**, or an external display): macOS keeps the logo dark under a closed lid, so the app tells the logo the lid is open and the keyboard that it is closed, which stays dark, as LogoLight's *Lit with Lid Closed* does. The light sensor is covered then, so the logo glows as it would in a dark room. The line under **Apple Logo Blink Styles** says what the closed lid did last (blinking, or why not). On battery, and while the display only sleeps with the lid open, the blink pauses and writes nothing. |
|
||||
| **Window** | Size, position and full-screen state are remembered. A saved window that hangs off the screen it opens on (it was last on a larger display that is gone, say) is pulled back on and shrunk to fit; one whose title bar is on no screen at all opens at the default size instead. Clicking the Dock icon brings back a minimized window. |
|
||||
|
||||
## Build (on a Mac)
|
||||
@@ -225,31 +225,79 @@ If that probe is refused or rate limited, the connect screen comes back; the
|
||||
app does not send the refused login again. Keeping no copy of the login in
|
||||
WebKit has a cost here: after a server restart, each request that arrives
|
||||
without a cookie gets a 401 before the
|
||||
login goes out, and those count until one succeeds. Many windows reconnecting
|
||||
at once can push a request past 10, and that request then fails with a 429
|
||||
instead of being answered. A refusal that lands while you are in another app
|
||||
does not bring Codeman forward: the connect screen loads where the window is,
|
||||
and the Dock icon bounces.
|
||||
login goes out, and those count until on**The logo moves on its own hardware fades, from a child process, through CoreBrightness.**
|
||||
Apple documents one control for the logo ([support.apple.com/en-us/149248](https://support.apple.com/en-us/149248)):
|
||||
`sudo defaults write com.apple.CoreBrightness LEBacklightMode -int 0`, an on/off setting that
|
||||
`corebrightnessd` keeps in root's preferences. A process of the logged-in user can reach the
|
||||
daemon without an administrator through `BrightnessSystemClient` in the private CoreBrightness
|
||||
framework, and 1.50.0 blinked by switching that setting there. The blink now uses two other
|
||||
properties of the same client, which [LogoLight](https://github.com/Ark0N/LogoLight) (MIT, its
|
||||
`docs/corebrightnessd.md`) measured on the real logo and `src/logo.rs` reimplements:
|
||||
`LEPWMFactorFadeDuration` sets how long the next fade takes, and `DisplayLogicalState` 1
|
||||
(Blank) fades the logo to dark and 2 (Unblank) back to lit, on the LED's own spring curve.
|
||||
Nothing of that is saved, so a blink writes no setting and leaves nothing behind a restart;
|
||||
`LEBacklightMode` is only read.
|
||||
|
||||
**The logo blinks from a child process, through CoreBrightness.** Apple documents
|
||||
one control for the logo ([support.apple.com/en-us/149248](https://support.apple.com/en-us/149248)):
|
||||
`sudo defaults write com.apple.CoreBrightness LEBacklightMode -int 0`. Doing that
|
||||
for every blink would need a root helper and an administrator password. The same
|
||||
setting is also a property of `BrightnessSystemClient` in the private
|
||||
CoreBrightness framework, and a process of the logged-in user can set it there,
|
||||
which is what `src/logo.rs` does. Measured on a Mac17,13 with macOS 27.2 from an
|
||||
ad-hoc signed binary: `corebrightnessd` logs the logo dark about a millisecond
|
||||
after the write, with no fade, and the NVRAM variable `bklb-backlight-enabled`
|
||||
keeps its value while the logo is dark.
|
||||
The daemon decides the rest. Writing the direction the logo already heads in is ignored, and
|
||||
the state cannot be read back, so the child keeps track of it and, when it does not know,
|
||||
first turns the logo round with a fade too slow to see. The keyboard backlight follows
|
||||
`DisplayLogicalState` too, so after every Blank the keyboards alone are told Unblank
|
||||
(`setProperty:withKey:keyboardID:`), only while the built-in display is awake
|
||||
(`CGDisplayIsAsleep`); when it sleeps they are told Blank, and Unblank when it wakes. A rise
|
||||
from exactly dark waits for the display to show a frame, up to half a second with nothing
|
||||
changing on screen. So the short dark beats (Alarm's, Heartbeat's, Wink's, Fast's) turn round
|
||||
while their fall is still under way and never quite reach dark, and a long one (Calm's,
|
||||
Slow's, the breath's rest) times its light from when the rise starts.
|
||||
|
||||
The setting is system-wide and survives a reboot, so the app must not end with the
|
||||
logo dark, and the release profile has `panic = "abort"`, so no cleanup code of the
|
||||
app can be relied on. So all of it runs as this same binary with
|
||||
`--logo-blink`, for as long as any style is chosen and a server is open, tied to the app
|
||||
by nothing but a pipe: when the pipe closes, however the app ended, it puts the logo
|
||||
back and exits. Switching it off closes the same pipe, so the path a crash
|
||||
takes is the one used every day. A marker file
|
||||
(`logo-blink`, next to `desktop.json`) covers what the child cannot survive itself:
|
||||
With the lid closed the daemon tells every module so (`ClamshellUpdate` 1), and the logo's
|
||||
target is then 0 whatever `DisplayLogicalState` says. A client's `ClamshellUpdate` reaches the
|
||||
same modules, and LogoLight measured on the real MacBook what follows from that, first with an
|
||||
external display and, for its 1.3.1, on a Mac kept awake with none:
|
||||
- Kept awake with the lid closed and no other display, the built-in display stays online and
|
||||
`CGDisplayIsAsleep` says 0. So the lid sensor (`AppleClamshellState` on `IOPMrootDomain`)
|
||||
decides that the logo is out of sight, not the display list.
|
||||
- Once the daemon has handled the lid (`CBClamshellState` reads 1, a moment after the sensor),
|
||||
`ClamshellUpdate` 0 relights the logo and fades work as with the lid open. The child then
|
||||
writes `ClamshellUpdate` 1 to each keyboard alone, which stays dark, and to each display in
|
||||
`CBDisplayList` alone (`setProperty:withKey:andDisplay:`): told the lid is open, True Tone
|
||||
adapts an external display to the covered light sensor and tints it warm and dim.
|
||||
- The daemon sends the real lid state again when a display or a keyboard comes up. Every beat
|
||||
ends lit, so a logo found dark at the start of the next one is told again.
|
||||
- `DisplayPowerStateEvent` is never written. A client's DidSleep keeps the logo dark, with the
|
||||
lid open too, until someone writes DidWake.
|
||||
|
||||
The child does this only on the power adapter (on battery a closed MacBook sleeps, and a dark
|
||||
wake must not light the logo in a bag) and only with every keyboard reachable. The first rise
|
||||
has to move the logo within 1.5 s. If it does not, the logo is told again, three times in a
|
||||
row and then every half minute, and the menu line says it stayed dark. Ending a blink with the
|
||||
lid closed hands the logo back to normal (the automatic duration, then Unblank, which a held
|
||||
level needs) and then writes `ClamshellUpdate` 1, since normal for a closed lid is dark; the
|
||||
marker covers a child killed in between. When the lid opens, the daemon tells every module
|
||||
itself and nothing is written. What the closed lid did last (blinking, stayed dark, nothing to
|
||||
show with no agent at work, not tried and why, or that the Mac went to sleep under it, which
|
||||
the child sees as wall-clock time that passed while its monotonic clock stood still) goes to a
|
||||
file next to `desktop.json` (`logo-lid`, with the readings behind it on its second line), and
|
||||
the app shows its first line as **Lid Closed: …** under View > Apple Logo Blink Styles, or
|
||||
says nothing watches the lid while no child runs.
|
||||
|
||||
Whether the Mac stays awake under the lid is not the logo's business: Keep This Mac Awake's
|
||||
lid helper does that (`pmset disablesleep`, which holds the Mac at full wake), or LogoLight's
|
||||
own Lit with Lid Closed, or an external display. An assertion against system sleep alone would
|
||||
not do: LogoLight saw the Mac enter a dark wake with it, where the logo stays dark.
|
||||
|
||||
The app must not end with the logo dark, and the release profile has `panic = "abort"`, so no
|
||||
cleanup code of the app can be relied on. So all of it runs as this same binary with
|
||||
`--logo-blink`, for as long as the switch is on and a server is open, tied to the app by
|
||||
nothing but a pipe: when the pipe closes, however the app ended, it puts the logo back (lit,
|
||||
with the daemon's automatic fade duration) and exits. Switching it off closes the same pipe,
|
||||
so the path a crash takes is the one used every day. A marker file (`logo-fade`, next to
|
||||
`desktop.json`) covers what the child cannot survive itself: at the next launch the app puts
|
||||
back a logo a blink left dark, and with 1.50.0's marker (`logo-blink`) it switches back on a
|
||||
logo that version left off. The framework is loaded at run time and never linked, and
|
||||
`LEBacklightMode` reading nil (a Mac mini answers nil) is what hides the option, so a macOS
|
||||
that drops it loses this feature and nothing else.
|
||||
|
||||
e child cannot survive itself:
|
||||
at the next launch the app puts back a logo a blink left dark. The framework is
|
||||
loaded at run time and never linked, and `LEBacklightMode` reading nil (a Mac mini
|
||||
answers nil) is what hides the option, so a macOS that drops it loses this feature
|
||||
@@ -308,17 +356,24 @@ crosses, so the login is never on a command line.
|
||||
look as a failed login, like any look at a server the app has no login for.
|
||||
*Check again* asks again.
|
||||
- **The logo blink rests on a private macOS interface.** Apple documents the
|
||||
setting, not the way the app reaches it, so a macOS update can take it away;
|
||||
the menu then disappears. Every change of the logo writes `LEBacklightMode`,
|
||||
from about once every two seconds (*Slow*, *Wink*) to about three times a
|
||||
second (*Fast*), which `corebrightnessd` keeps in root's preferences. The
|
||||
setting is on or off, so these styles are rhythms of hard changes. After a
|
||||
blink the key stays there rather than being absent as on a fresh Mac. That
|
||||
reads the same to macOS; `sudo defaults delete com.apple.CoreBrightness
|
||||
LEBacklightMode` removes it. After a power cut in a
|
||||
dark phase the logo stays dark until the app next starts. `api/sessions` does
|
||||
not say which turns are Codeman's own, so with prompt-cache keep-warm on, a
|
||||
keep-alive turn blinks too.
|
||||
on/off setting, not the fades the app uses, so a macOS update can take them
|
||||
away. The menu disappears if `LEBacklightMode` goes; if only the fade keys
|
||||
change, the logo stops moving. A long dark beat (*Calm*, *Slow*, the rest in
|
||||
*Breathe*) can be up to half a second longer than its style says, while the
|
||||
display waits for a frame before the logo rises. Every Blank dims the
|
||||
keyboard backlight a little before it is told to stay lit. A child killed
|
||||
outright (SIGKILL) leaves the logo where it was, perhaps dark, until the
|
||||
display next sleeps and wakes or the app next starts. Another app that drives
|
||||
the logo the same way and is not LogoLight is not recognised, and the two
|
||||
fight. `api/sessions` does not say which turns are Codeman's own, so with
|
||||
prompt-cache keep-warm on, a keep-alive turn blinks too.
|
||||
- **Blinking with the lid closed has not been seen from this app on a real
|
||||
MacBook yet.** The method is LogoLight's, which its author saw working on the
|
||||
real lid, on power with no display attached (LogoLight 1.3.1). This app's own
|
||||
run of it, with the Mac kept awake by the lid helper, has not been watched. If
|
||||
the logo does not move, the menu line says "stayed dark". Not checked either,
|
||||
by LogoLight or here: the built-in display's brightness when the lid opens
|
||||
again, and an Apple display with its own light sensor.
|
||||
- **Self-signed HTTPS is refused.** A server started with `codeman web --https`
|
||||
uses a self-signed certificate, and WKWebView rejects it (wry has no trust
|
||||
override). Use `http://127.0.0.1:3000` for a server on this Mac. For another
|
||||
|
||||
@@ -44,9 +44,10 @@ pub struct Config {
|
||||
pub auto_connect: bool,
|
||||
/// Always open in full screen (otherwise the last state is restored).
|
||||
pub start_fullscreen: bool,
|
||||
/// What the lit Apple logo does for each state of the agents (logo.rs). The
|
||||
/// menu only shows on a Mac that has one. A value this version does not know
|
||||
/// reads as off: a bad style must not make the whole file unreadable.
|
||||
/// Whether the lit Apple logo shows what the agents are doing, and its style
|
||||
/// for each state (logo.rs). The menu only shows on a Mac that has one. A
|
||||
/// value this version does not know reads as off: a bad style must not make
|
||||
/// the whole file unreadable.
|
||||
#[serde(deserialize_with = "lenient_blink")]
|
||||
pub logo_blink: BlinkStyles,
|
||||
/// Anything that is not a whole frame reads as None: a bad frame must not
|
||||
@@ -415,9 +416,12 @@ mod tests {
|
||||
use crate::logo::BlinkStyle;
|
||||
let mut c = Config::default();
|
||||
assert_eq!(c.logo_blink, BlinkStyles::default());
|
||||
c.logo_blink.needs_you = BlinkStyle::Alarm;
|
||||
assert!(!c.logo_blink.enabled, "off until the user turns it on");
|
||||
c.logo_blink.enabled = true;
|
||||
c.logo_blink.needs_you = BlinkStyle::Wink;
|
||||
let text = serde_json::to_string(&c).unwrap();
|
||||
assert!(text.contains(r#""needsYou":"alarm""#), "{text}");
|
||||
assert!(text.contains(r#""needsYou":"wink""#), "{text}");
|
||||
assert!(text.contains(r#""enabled":true"#), "{text}");
|
||||
assert_eq!(serde_json::from_str::<Config>(&text).unwrap().logo_blink, c.logo_blink);
|
||||
for bad in [
|
||||
r#"{"logoBlink":3}"#,
|
||||
@@ -434,6 +438,7 @@ mod tests {
|
||||
use crate::logo::BlinkStyle;
|
||||
let c: Config = serde_json::from_str(r#"{"logoBlink":"heartbeat"}"#).unwrap();
|
||||
assert_eq!(c.logo_blink.working, BlinkStyle::Heartbeat);
|
||||
assert!(c.logo_blink.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+2348
-357
File diff suppressed because it is too large.
Load diff
@@ -110,6 +110,8 @@ enter it again (a single-user Codeman accepts the right password at once), or wa
|
||||
const KEYCHAIN_UNREADABLE: &str = "The saved password could not be read from the Keychain. Enter it again.";
|
||||
const PAGE_KEEPS_STOPPING: &str = "Codeman's page stopped twice in a row, so it was closed. Connect again.";
|
||||
const LOGO_SWITCHED_OFF: &str = "The Apple logo is switched off on this Mac, so it cannot blink.";
|
||||
const LOGO_DRIVEN_ELSEWHERE: &str =
|
||||
"LogoLight is driving the Apple logo, so Codeman leaves it alone. Quit LogoLight to see your agents on it.";
|
||||
/// What a download's toast calls a file whose name is not known.
|
||||
const UNNAMED_FILE: &str = "the file";
|
||||
const START_REFUSED: &str = "Codeman can only be started here for an http://127.0.0.1 or http://localhost address.";
|
||||
@@ -182,6 +184,8 @@ enum UserEvent {
|
||||
/// install opened in Terminal, the install command copied): the message
|
||||
/// goes to the page if it is still the one choosing a server.
|
||||
ToConnectPage(serde_json::Value),
|
||||
/// What the closed lid did with the logo last (logo::watch_lid_status).
|
||||
LogoLidStatus(String),
|
||||
}
|
||||
|
||||
struct ProbeDone {
|
||||
@@ -399,6 +403,13 @@ fn main() {
|
||||
logo_watch: logo::Watcher::default(),
|
||||
};
|
||||
|
||||
if app.menu.logo_lid.is_some() {
|
||||
let lid_proxy = app.proxy.clone();
|
||||
logo::watch_lid_status(move |line| {
|
||||
let _ = lid_proxy.send_event(UserEvent::LogoLidStatus(line));
|
||||
});
|
||||
}
|
||||
|
||||
event_loop.run(move |event, target, control_flow| app.handle(event, target, control_flow));
|
||||
}
|
||||
|
||||
@@ -821,6 +832,7 @@ impl App {
|
||||
self.to_connect_page(msg);
|
||||
}
|
||||
}
|
||||
UserEvent::LogoLidStatus(line) => self.show_lid_status(line),
|
||||
UserEvent::ServerStartDone {
|
||||
generation,
|
||||
url,
|
||||
@@ -1030,34 +1042,77 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// The logo is watched while any style is chosen and a server is open.
|
||||
/// The logo is watched while the switch is on, a state has a style and a
|
||||
/// server is open.
|
||||
fn sync_logo_watch(&mut self) {
|
||||
let styles = self.config.logo_blink;
|
||||
let phase = self.phase.borrow();
|
||||
let server = phase.loaded().filter(|_| styles.any() && logo::available());
|
||||
let server = phase.loaded().filter(|_| styles.active() && logo::available());
|
||||
self.logo_watch
|
||||
.sync(server.map(|url| (url.as_str(), self.auth.as_deref(), styles)));
|
||||
drop(phase);
|
||||
self.show_lid_status(logo::lid_status_line());
|
||||
}
|
||||
|
||||
/// The line under the logo's styles: what the closed lid did last, or that
|
||||
/// nothing watches it now.
|
||||
fn show_lid_status(&self, line: String) {
|
||||
if let Some(item) = &self.menu.logo_lid {
|
||||
if self.logo_watch.watching() {
|
||||
item.set_text(line);
|
||||
} else {
|
||||
item.set_text(logo::LID_NOT_WATCHED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_logo_blink(&mut self, signal: Signal, style: BlinkStyle) {
|
||||
// A click flips the item it landed on, the chosen one included, so all
|
||||
// of that state's are set.
|
||||
for (item_signal, item_style, item) in &self.menu.logo_styles {
|
||||
if *item_signal == signal {
|
||||
item.set_checked(*item_style == style);
|
||||
}
|
||||
}
|
||||
let was_off = !self.config.logo_blink.any();
|
||||
let was_active = self.config.logo_blink.active();
|
||||
self.config.logo_blink.set(signal, style);
|
||||
// Picking a style is asking for the logo to show it.
|
||||
if style != BlinkStyle::Off {
|
||||
self.config.logo_blink.enabled = true;
|
||||
}
|
||||
self.logo_blink_changed(was_active);
|
||||
}
|
||||
|
||||
/// The one switch. Turned on with every state off, it brings the recommended
|
||||
/// styles, so that on always shows something.
|
||||
fn toggle_logo_blink(&mut self) {
|
||||
let was_active = self.config.logo_blink.active();
|
||||
let on = !self.config.logo_blink.enabled;
|
||||
self.config.logo_blink.enabled = on;
|
||||
if on && !self.config.logo_blink.any_style() {
|
||||
self.config.logo_blink = logo::BlinkStyles::RECOMMENDED;
|
||||
}
|
||||
self.logo_blink_changed(was_active);
|
||||
}
|
||||
|
||||
fn logo_blink_changed(&mut self, was_active: bool) {
|
||||
// A click flips the item it landed on, so every item is set from the
|
||||
// settings, the clicked one included.
|
||||
let styles = self.config.logo_blink;
|
||||
if let Some(switch) = &self.menu.logo_switch {
|
||||
switch.set_checked(styles.enabled);
|
||||
}
|
||||
for (signal, style, item) in &self.menu.logo_styles {
|
||||
item.set_checked(styles.get(*signal) == *style);
|
||||
}
|
||||
self.config_dirty = true;
|
||||
self.sync_logo_watch();
|
||||
// Only from off: with a style running, a dark logo may be our own.
|
||||
if was_off && style != BlinkStyle::Off && logo::switched_off() {
|
||||
if let Some(main) = self.windows.get(&self.main_id) {
|
||||
let _ = main
|
||||
.webview
|
||||
.evaluate_script(&assets::toast_script(LOGO_SWITCHED_OFF, "warning"));
|
||||
}
|
||||
// Only from off: with a blink running, a dark logo may be our own.
|
||||
if was_active || !styles.active() {
|
||||
return;
|
||||
}
|
||||
let warning = if logo::switched_off() {
|
||||
LOGO_SWITCHED_OFF
|
||||
} else if logo::driven_elsewhere() {
|
||||
LOGO_DRIVEN_ELSEWHERE
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
if let Some(main) = self.windows.get(&self.main_id) {
|
||||
let _ = main.webview.evaluate_script(&assets::toast_script(warning, "warning"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1070,6 +1125,7 @@ impl App {
|
||||
menu_id::SWITCH => self.show_connect_page(None, true),
|
||||
menu_id::ADD_AGENT => self.add_agent(),
|
||||
menu_id::RELOAD => self.reload_focused(),
|
||||
menu_id::LOGO_BLINK => self.toggle_logo_blink(),
|
||||
menu_id::START_FULLSCREEN => {
|
||||
let value = self.menu.start_fullscreen.is_checked();
|
||||
self.config.start_fullscreen = value;
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod menu_id {
|
||||
pub const ADD_AGENT: &str = "add-agent";
|
||||
pub const RELOAD: &str = "reload";
|
||||
pub const START_FULLSCREEN: &str = "start-fullscreen";
|
||||
pub const LOGO_BLINK: &str = "logo-blink";
|
||||
pub const DEVTOOLS: &str = "devtools";
|
||||
pub const HELP_DOCS: &str = "help-docs";
|
||||
pub const HELP_GITHUB: &str = "help-github";
|
||||
@@ -26,9 +27,14 @@ pub struct AppMenu {
|
||||
pub window_menu: Submenu,
|
||||
pub help_menu: Submenu,
|
||||
pub start_fullscreen: CheckMenuItem,
|
||||
/// One item per state and style. Empty on a Mac without a lit logo, which has no
|
||||
/// The one switch for the logo. None on a Mac without a lit logo, which has no
|
||||
/// such menu.
|
||||
pub logo_switch: Option<CheckMenuItem>,
|
||||
/// One item per state and style, empty without the switch.
|
||||
pub logo_styles: Vec<(Signal, BlinkStyle, CheckMenuItem)>,
|
||||
/// What the closed lid did with the logo last, a line that cannot be
|
||||
/// clicked. None without the switch.
|
||||
pub logo_lid: Option<MenuItem>,
|
||||
}
|
||||
|
||||
pub fn build_menu(config: &Config) -> muda::Result<AppMenu> {
|
||||
@@ -110,10 +116,20 @@ pub fn build_menu(config: &Config) -> muda::Result<AppMenu> {
|
||||
),
|
||||
],
|
||||
)?;
|
||||
// Only a Mac with a lit Apple logo has this menu at all.
|
||||
// Only a Mac with a lit Apple logo has these items at all: the switch, and the
|
||||
// style for each state behind it.
|
||||
let mut logo_styles = Vec::new();
|
||||
let mut logo_switch = None;
|
||||
let mut logo_lid = None;
|
||||
if logo::available() {
|
||||
let submenu = Submenu::new("Blink Apple Logo", true);
|
||||
let switch = CheckMenuItem::with_id(
|
||||
menu_id::LOGO_BLINK,
|
||||
"Blink Apple Logo",
|
||||
true,
|
||||
config.logo_blink.enabled,
|
||||
None,
|
||||
);
|
||||
let submenu = Submenu::new("Apple Logo Blink Styles", true);
|
||||
for signal in Signal::ALL {
|
||||
let group = Submenu::new(signal.label(), true);
|
||||
for style in BlinkStyle::ALL {
|
||||
@@ -128,7 +144,12 @@ pub fn build_menu(config: &Config) -> muda::Result<AppMenu> {
|
||||
submenu.append(&group)?;
|
||||
}
|
||||
// Right under "Always Open in Full Screen", before the separator.
|
||||
view.insert(&submenu, 4)?;
|
||||
let lid = MenuItem::new(logo::lid_status_line(), false, None);
|
||||
view.insert(&switch, 4)?;
|
||||
view.insert(&submenu, 5)?;
|
||||
view.insert(&lid, 6)?;
|
||||
logo_switch = Some(switch);
|
||||
logo_lid = Some(lid);
|
||||
}
|
||||
let window_menu = Submenu::with_items(
|
||||
"Window",
|
||||
@@ -156,6 +177,8 @@ pub fn build_menu(config: &Config) -> muda::Result<AppMenu> {
|
||||
window_menu,
|
||||
help_menu,
|
||||
start_fullscreen,
|
||||
logo_switch,
|
||||
logo_styles,
|
||||
logo_lid,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user