Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions apps/light/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ foreach(target monky_light_platform monky-light-platform-test monky_light_loop m
endif()
endforeach()
if(MONKY_LIGHT_WITH_RTC)
# Opening without a command line: the flow is platform-free, the window is not.
add_library(monky_light_standalone STATIC src/standalone.cpp)
target_include_directories(monky_light_standalone PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(monky_light_standalone PUBLIC monky_light_profile monky_light_invite monky_light_contracts)
add_executable(monky-light-standalone-test test/standalone_test.cpp)
target_link_libraries(monky-light-standalone-test PRIVATE monky_light_standalone)
set_target_properties(monky-light-standalone-test PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/bin")
add_library(monky_light_application STATIC src/headless_application.cpp)
target_include_directories(monky_light_application PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(monky_light_application PUBLIC
Expand All @@ -224,22 +233,24 @@ if(MONKY_LIGHT_WITH_RTC)
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/bin")
if(WIN32)
add_library(monky_light_ui STATIC src/ui/tray_windows.cpp)
target_link_libraries(monky_light_ui PUBLIC Shell32 User32 Gdi32)
add_library(monky_light_ui STATIC src/ui/tray_windows.cpp src/ui/connect_windows.cpp
src/ui/startup_windows.cpp)
target_link_libraries(monky_light_ui PUBLIC Shell32 User32 Gdi32 Ole32 Advapi32)
target_compile_definitions(monky_light_ui PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
else()
find_library(MONKY_APPKIT_FRAMEWORK AppKit REQUIRED)
# AXIsProcessTrusted: push-to-talk needs Accessibility to see keys.
find_library(MONKY_APPLICATION_SERVICES_FRAMEWORK ApplicationServices REQUIRED)
add_library(monky_light_ui STATIC src/ui/tray_macos.mm)
set_source_files_properties(src/ui/tray_macos.mm PROPERTIES COMPILE_OPTIONS "-fobjc-arc")
add_library(monky_light_ui STATIC src/ui/tray_macos.mm src/ui/connect_macos.mm src/ui/startup_macos.mm)
set_source_files_properties(src/ui/tray_macos.mm src/ui/connect_macos.mm src/ui/startup_macos.mm
PROPERTIES COMPILE_OPTIONS "-fobjc-arc")
target_link_libraries(monky_light_ui PUBLIC "${MONKY_APPKIT_FRAMEWORK}"
"${MONKY_APPLICATION_SERVICES_FRAMEWORK}")
endif()
target_include_directories(monky_light_ui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(monky_light_ui PUBLIC monky_light_json)
add_executable(monky-light src/main.cpp)
target_link_libraries(monky-light PRIVATE monky_light_application monky_light_ui)
target_link_libraries(monky-light PRIVATE monky_light_application monky_light_ui monky_light_standalone)
if(APPLE)
set_target_properties(monky-light PROPERTIES
MACOSX_BUNDLE TRUE
Expand All @@ -257,7 +268,8 @@ if(MONKY_LIGHT_WITH_RTC)
monky_light_profile monky-light-profile-test monky_light_fixture_audio monky-light-audio-fixture-test
monky_light_session monky-light-session-test monky_light_media
monky-light-media-lifecycle-test monky-light-media-loopback-test
monky_light_application monky_light_ui monky-light-tray-test monky-light monky-light-fixture)
monky_light_application monky_light_ui monky-light-tray-test monky-light monky-light-fixture
monky_light_standalone monky-light-standalone-test)
if(MSVC)
target_compile_options(${target} PRIVATE /W4 /WX /EHsc /utf-8 /permissive-)
else()
Expand Down
10 changes: 10 additions & 0 deletions apps/light/QA.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,13 @@ available, so the terminal shows what each click did.
| 6.28 | Open `In the call` while someone speaks | They appear with `(speaking)`; you are not listed | | |
| 6.29 | Choose 0%, 200%, and 100% for a participant | Silent, louder, back to normal; nobody else notices a difference | | |
| 6.30 | Restart the Light and reopen `In the call` | The chosen volume is still checked and applied | | |
| 6.31 | With `MONKY_LIGHT_PROFILE` on a disposable folder, double-click the executable | The console disappears at once, the `Connect to a server` window appears, and no terminal stays open | | |
| 6.32 | Paste an invitation link, pick a nickname, and connect | Joins the server; `monky-light-settings.json` holds only `server` (`ws(s)://`) and `nickname`, no password | | |
| 6.33 | Quit and open again | Connects straight to the same server, with no window | | |
| 6.34 | Invalid address, empty nickname, or one longer than 32 characters | The window stays open, explains why, and focuses the field to fix | | |
| 6.35 | A server with a password, with none or a wrong one | The window reopens on the password field with the explanation, keeping server and nickname | | |
| 6.36 | A nickname someone else already uses on the server | The window reopens on the nickname field with the explanation | | |
| 6.37 | `Change server…` in the tray | The Light leaves the current server, the icon disappears, and the window reopens filled with the current server, without a password | | |
| 6.38 | Open a second copy with the same profile | Profile unavailable notice; the first copy stays connected | | |
| 6.39 | Cancel the window | The Light quits without connecting and leaves no process | | |
| 6.40 | Turn `Start with the system` on, sign out of the system and back in | The Light opens by itself, tray only, and connects to the last server; turning it off removes the registration and it no longer opens | | |
10 changes: 10 additions & 0 deletions apps/light/QA.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,13 @@ valendo, então o terminal serve para conferir o que cada clique fez.
| 6.28 | Abrir `Na chamada` enquanto alguém fala | A pessoa aparece com `(falando)`; você não aparece na lista | | |
| 6.29 | Escolher 0%, 200% e 100% para um participante | Some, fica mais alto, volta ao normal; os outros não percebem diferença | | |
| 6.30 | Reiniciar o Light e reabrir `Na chamada` | O volume escolhido continua marcado e aplicado | | |
| 6.31 | Com `MONKY_LIGHT_PROFILE` numa pasta descartável, abrir o executável com duplo clique | O console some na hora, a janela `Conectar a um servidor` aparece e nenhum terminal fica aberto | | |
| 6.32 | Colar um link de convite, escolher o apelido e conectar | Entra no servidor; `monky-light-settings.json` tem só `server` (`ws(s)://`) e `nickname`, sem senha | | |
| 6.33 | Fechar e abrir de novo | Conecta direto ao mesmo servidor, sem janela | | |
| 6.34 | Endereço inválido, apelido vazio ou com mais de 32 caracteres | A janela continua aberta, explica o motivo e foca o campo a corrigir | | |
| 6.35 | Servidor com senha, sem senha ou com a errada | A janela reabre no campo da senha com a explicação, mantendo servidor e apelido | | |
| 6.36 | Apelido já usado por outra pessoa no servidor | A janela reabre no campo do apelido com a explicação | | |
| 6.37 | `Trocar de servidor…` na bandeja | O Light sai do servidor atual, o ícone some e a janela reabre preenchida com o servidor atual, sem senha | | |
| 6.38 | Abrir uma segunda cópia com o mesmo perfil | Aviso de perfil indisponível; a primeira cópia continua conectada | | |
| 6.39 | Cancelar a janela | O Light encerra sem conectar e sem deixar processo | | |
| 6.40 | Ligar `Iniciar com o sistema`, sair da sessão do sistema e entrar de novo | O Light abre sozinho, só com a bandeja, e conecta ao último servidor; desligar remove o registro e ele não abre mais | | |
50 changes: 49 additions & 1 deletion apps/light/README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,53 @@ processing, and 100% costs nothing. Setting 100 again removes the entry. The
profile keeps up to 256 sessions with a changed volume; beyond that, `set-volume`
is rejected until one is set back to 100.

## Opening as an application

With no arguments at all (double click, Finder, or a shortcut), the Light opens
with only the tray and no console. The first time, a small window asks for the
invitation link or server address, the nickname, and the password if the server
needs one. Later starts connect straight to the last server. `Change server…`,
in the tray, ends the current connection and reopens the window in the same
process.

- **Profile:** `%LOCALAPPDATA%\Monky Light\Profile` on Windows and
`~/Library/Application Support/Monky Light/Profile` on macOS, apart from any
profile given with `--profile`. For development and QA, point
`MONKY_LIGHT_PROFILE` at a disposable absolute folder; never use the profile
of a real installation.
- **What is saved:** only the `ws(s)://` address and the nickname, in
`monky-light-settings.json`. An invitation link is reduced to its address:
the server key is pinned on first contact, and the password **never** reaches
the disk. On a server with a password, each start connects without it; the
server's refusal reopens the window on the password field, keeping the rest.
- **Refusals the window can fix** reopen it with the reason, instead of only a
tray alert: a wrong password (`AUTH_INVALID_PASSWORD`), a nickname in use
(`NICKNAME_ALREADY_EXISTS`) or refused (`NICKNAME_INVALID`).
- **The window checks** what the core would refuse: empty fields, an address
that is neither an invitation nor `ws://`/`wss://`, and a nickname above the
shared limit (32 units, counted as the server counts them). The reason shows
in the window itself, and the field to fix gets the focus.
- **Without a console reader**, events reach only the tray and nothing is read
from stdin; a missing console does not end the core as it does in
command-line mode.
- **Another copy open:** the profile keeps its exclusive lock. A second start
shows a notice and quits without touching the first one.
- On Windows the executable is still a console program: a double click opens a
console for an instant, which the Light lets go right away. A console shared
with the terminal that started it is kept.

- **Start with the system**, off by default, registers this executable with no
arguments to open at sign-in: a value under
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run` on Windows and a
LaunchAgent `~/Library/LaunchAgents/org.monky.light.plist` on macOS
(effective from the next login; works from macOS 12). The row is checked only
when the registration points at this very executable, and it is not offered
with `MONKY_LIGHT_PROFILE`, so a development build never registers itself
with the real profile.

Any argument keeps the command-line behaviour documented above, even without
`--tray`.

## Tray interface

`--tray` adds the icon to the Windows notification area or the macOS status bar.
Expand Down Expand Up @@ -201,7 +248,8 @@ rendering between activations. It carries:
- `In the call`, with each participant, who is speaking, and the volume, in
presets from 0 to 200%;
- `Audio devices`, with input, output, and `System default`;
- `Reconnect now` and `Quit Monky Light`.
- `Reconnect now`, `Change server…` and `Start with the system` (both only when
opened as an application), and `Quit Monky Light`.

In a native menu an exclusive choice (channel, device) uses a radio mark and a
toggle uses a check mark: these are the native equivalents of the selectable card
Expand Down
50 changes: 49 additions & 1 deletion apps/light/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,53 @@ não há processamento extra, e 100% não custa nada. Voltar a 100 apaga a entra
O perfil guarda até 256 sessões com volume alterado; acima disso, `set-volume`
é recusado até que alguma volte a 100.

## Abrir como aplicativo

Sem nenhum argumento (duplo clique, Finder ou atalho), o Light abre só com a
bandeja, sem console. Na primeira vez, uma janela pequena pede o link de
convite ou o endereço do servidor, o apelido e, se o servidor pedir, a senha.
Nas próximas aberturas ele conecta direto ao último servidor.
`Trocar de servidor…`, na bandeja, encerra a conexão atual e reabre a janela
no mesmo processo.

- **Perfil:** `%LOCALAPPDATA%\Monky Light\Profile` no Windows e
`~/Library/Application Support/Monky Light/Profile` no macOS, separado de
qualquer perfil passado por `--profile`. Para desenvolvimento e QA, aponte
`MONKY_LIGHT_PROFILE` para uma pasta descartável absoluta; nunca use o perfil
de uma instalação real.
- **O que é salvo:** só o endereço `ws(s)://` e o apelido, em
`monky-light-settings.json`. Um link de convite é reduzido ao endereço: a
chave do servidor fica fixada no primeiro contato, e a senha **nunca** vai
para o disco. Num servidor com senha, cada abertura conecta sem ela; ao
recusar, o servidor faz a janela reabrir no campo da senha, mantendo o resto.
- **Recusas que a janela resolve** reabrem a janela com o motivo, em vez de só
um alerta na bandeja: senha errada (`AUTH_INVALID_PASSWORD`), apelido em uso
(`NICKNAME_ALREADY_EXISTS`) ou recusado (`NICKNAME_INVALID`).
- **A janela confere** o que o núcleo recusaria: campos vazios, endereço que
não é convite nem `ws://`/`wss://` e apelido acima do limite compartilhado
(32 unidades, contadas como o servidor conta). O motivo aparece na própria
janela, e o campo a corrigir recebe o foco.
- **Sem leitor de console**, os eventos vão só para a bandeja e nada é lido do
stdin; a ausência do console não encerra o núcleo como acontece no modo de
linha de comando.
- **Outra cópia aberta:** o perfil continua com lock exclusivo. Uma segunda
abertura mostra um aviso e encerra, sem tocar na primeira.
- No Windows, o executável ainda é de console: o duplo clique abre um console
por um instante, que o Light solta logo em seguida. Um console compartilhado
com o terminal que o iniciou é mantido.

- **Iniciar com o sistema**, desligado por padrão, registra este executável
sem argumentos para abrir no login: um valor em
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run` no Windows e um
LaunchAgent `~/Library/LaunchAgents/org.monky.light.plist` no macOS (vale a
partir do próximo login; funciona desde o macOS 12). O item só aparece
marcado quando o registro aponta para este mesmo executável, e não é
oferecido com `MONKY_LIGHT_PROFILE`, para um build de desenvolvimento nunca
se registrar com o perfil real.

Qualquer argumento mantém o comportamento de linha de comando documentado
acima, inclusive sem `--tray`.

## Interface de bandeja

`--tray` adiciona o ícone na área de notificação do Windows ou na barra de status
Expand Down Expand Up @@ -202,7 +249,8 @@ renderizando entre as ativações. Ele traz:
- `Na chamada`, com cada participante, quem está falando e o volume, em
predefinições de 0 a 200%;
- `Dispositivos de áudio`, com entrada, saída e `Padrão do sistema`;
- `Reconectar agora` e `Sair do Monky Light`.
- `Reconectar agora`, `Trocar de servidor…` e `Iniciar com o sistema` (os dois
só quando aberto como aplicativo) e `Sair do Monky Light`.

Em menu nativo, escolha exclusiva (canal, dispositivo) usa marca de rádio e
liga/desliga usa marca de seleção: são os equivalentes nativos do card
Expand Down
3 changes: 3 additions & 0 deletions apps/light/src/core_commands.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ namespace monky::light {
struct CoreCommands {
std::function<void(nlohmann::json)> command;
std::function<void()> quit;
// Present only when the Light was opened without a command line: stops the
// core so the connection window can ask for another server.
std::function<void()> switchServer{};
};

// A native interface owns the process main thread, because the platform event
Expand Down
21 changes: 18 additions & 3 deletions apps/light/src/headless_application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class Application final {
Application(Options options, HeadlessConfiguration audio)
: options_(std::move(options)), audio_(std::move(audio)), identity_(options_.profile),
session_({identity_.publicKeyHex(), identity_.deviceId(), options_.nickname,
sessionPassword(options_.invitedPassword),
sessionPassword(audio_.password ? audio_.password : options_.invitedPassword),
identity_.serverKey(options_.server), identity_.serverKeysExcept(options_.server),
options_.invitedServerKey},
{[this](const std::string& frame) {
Expand Down Expand Up @@ -186,6 +186,11 @@ class Application final {
loop_.post([this, line = std::move(line)] { command(line); }, bytes);
},
[this] { loop_.post([this] { quit(); }); },
audio_.askAgain ? std::function<void()>([this] {
audio_.askAgain(std::nullopt);
loop_.post([this] { quit(); });
})
: std::function<void()>(),
};
}

Expand All @@ -200,7 +205,9 @@ class Application final {
}

int run() {
ConsoleControl control({
// An application without a console has no control channel at all.
std::optional<ConsoleControl> control;
if (audio_.console) control.emplace(ConsoleControl::Callbacks{
[this](std::string line) {
const auto bytes = line.size();
loop_.post([this, line = std::move(line)] { command(line); }, bytes);
Expand Down Expand Up @@ -269,7 +276,7 @@ class Application final {

void output(Json event) {
if (observer_) observer_(event);
if (outputLost_) return;
if (outputLost_ || !audio_.console) return;
std::cout << event.dump() << '\n' << std::flush;
if (std::cout) return;
// Nobody reads the events anymore. Leave the call and the server properly,
Expand Down Expand Up @@ -752,6 +759,14 @@ class Application final {
if (event.failure) {
output({{"event", "session-error"}, {"code", event.failure->code},
{"kind", static_cast<int>(event.failure->kind)}, {"detail", event.failure->detail}});
// Retrying cannot fix these; the connection window can.
const auto& code = event.failure->code;
if (audio_.askAgain && (code == protocol::error::AUTH_INVALID_PASSWORD ||
code == protocol::error::NICKNAME_ALREADY_EXISTS ||
code == protocol::error::NICKNAME_INVALID)) {
audio_.askAgain(code);
quit();
}
}
break;
}
Expand Down
12 changes: 12 additions & 0 deletions apps/light/src/headless_application.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ struct HeadlessConfiguration {
// Optional native interface for --tray. Without it the core keeps the main
// thread and behaves exactly as the qualified headless client.
std::function<std::unique_ptr<CoreInterface>(CoreCommands)> createInterface;
// False when opened as an application (no command line): nobody reads stdout
// or writes stdin, so events reach only the interface and its absence is
// never taken as a reason to stop.
bool console = true;
// Typed into the connection window. Like MONKY_LIGHT_PASSWORD, which wins
// when set, it is only held in memory.
std::optional<std::string> password{};
// Application start only, so the connection window can ask again. Called on
// the core thread, just before the core stops: with the server's error code
// when it refused what the window can fix (password, nickname), or with
// nothing for the interface's CoreCommands::switchServer.
std::function<void(std::optional<std::string> refusal)> askAgain{};
};

int runHeadless(std::vector<std::string> arguments, HeadlessConfiguration configuration);
Expand Down
Loading
Loading