Skip to content
Merged
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
10 changes: 6 additions & 4 deletions apps/light/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,17 @@ 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 src/ui/connect_windows.cpp)
target_link_libraries(monky_light_ui PUBLIC Shell32 User32 Gdi32 Ole32)
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 src/ui/connect_macos.mm)
set_source_files_properties(src/ui/tray_macos.mm src/ui/connect_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()
Expand Down
1 change: 1 addition & 0 deletions apps/light/QA.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,4 @@ available, so the terminal shows what each click did.
| 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 | | |
1 change: 1 addition & 0 deletions apps/light/QA.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,4 @@ valendo, então o terminal serve para conferir o que cada clique fez.
| 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 | | |
13 changes: 11 additions & 2 deletions apps/light/README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,15 @@ process.
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`.

Expand Down Expand Up @@ -239,8 +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`, `Change server…` (only when opened as an application), 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
13 changes: 11 additions & 2 deletions apps/light/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ no mesmo processo.
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`.

Expand Down Expand Up @@ -240,8 +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`, `Trocar de servidor…` (só quando aberto como aplicativo)
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
17 changes: 14 additions & 3 deletions apps/light/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "profile_identity.hpp"
#include "standalone.hpp"
#include "ui/connect.hpp"
#include "ui/startup.hpp"
#include "ui/tray.hpp"

#include <api/audio/create_audio_device_module.h>
Expand Down Expand Up @@ -97,9 +98,14 @@ int runApplication() {
if (GetConsoleProcessList(processes, 2) == 1) FreeConsole();
#endif
monky::light::StandaloneHooks hooks;
// A build started with a development or QA profile must not register itself
// to open at sign-in, where it would use the real profile instead.
bool developmentProfile = false;
try {
// Development and QA point this elsewhere, never at the installed profile.
hooks.profile = profileOverride().value_or(ui::defaultProfileDirectory());
const auto override = profileOverride();
developmentProfile = override.has_value();
hooks.profile = override ? *override : ui::defaultProfileDirectory();
// The profile creates only its own folder; the one above is the Light's.
std::filesystem::create_directories(hooks.profile.parent_path());
} catch (const std::exception& error) {
Expand All @@ -123,9 +129,14 @@ int runApplication() {
return ui::askConnection(current, issue, language);
};
hooks.profileUnavailable = [language](const std::string& detail) { ui::showProfileUnavailable(detail, language); };
hooks.run = [](std::vector<std::string> arguments, std::optional<std::string> password,
std::function<void(std::optional<std::string>)> askAgain) {
hooks.run = [developmentProfile, language](std::vector<std::string> arguments, std::optional<std::string> password,
std::function<void(std::optional<std::string>)> askAgain) {
auto audio = configuration();
if (!developmentProfile) {
audio.createInterface = [language](monky::light::CoreCommands core) {
return ui::createTray(std::move(core), language, ui::systemStartup());
};
}
audio.console = false;
audio.password = std::move(password);
audio.askAgain = std::move(askAgain);
Expand Down
22 changes: 22 additions & 0 deletions apps/light/src/ui/startup.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <functional>
#include <optional>

namespace monky::light::ui {

// Starting with the system, for an application start only. Both functions run
// on the interface thread; `set` throws when the system refuses the change.
struct Startup {
std::function<bool()> enabled;
std::function<void(bool)> set;
};

// Registers this executable, with no arguments, to open at sign-in: a value
// under HKCU\...\CurrentVersion\Run on Windows, a LaunchAgent in
// ~/Library/LaunchAgents on macOS. Reported as enabled only when the
// registration points at this very executable, so another copy's entry is
// never shown as ours.
Startup systemStartup();

} // namespace monky::light::ui
71 changes: 71 additions & 0 deletions apps/light/src/ui/startup_macos.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include "ui/startup.hpp"

#import <Foundation/Foundation.h>

#include <stdexcept>

namespace monky::light::ui {
namespace {

// A per-user LaunchAgent works from macOS 12; SMAppService needs macOS 13.
NSString* const kLabel = @"org.monky.light";

NSURL* agentFile() {
NSURL* library = [[NSFileManager defaultManager] URLsForDirectory:NSLibraryDirectory
inDomains:NSUserDomainMask].firstObject;
if (!library) throw std::runtime_error("Could not find the Library folder");
return [[library URLByAppendingPathComponent:@"LaunchAgents" isDirectory:YES]
URLByAppendingPathComponent:[kLabel stringByAppendingString:@".plist"]];
}

NSString* executable() {
NSString* path = [NSBundle mainBundle].executablePath;
if (!path) throw std::runtime_error("Could not find the Monky Light executable");
return path;
}

bool enabled() {
@autoreleasepool {
NSDictionary* agent = [NSDictionary dictionaryWithContentsOfURL:agentFile() error:nil];
NSArray* arguments = agent[@"ProgramArguments"];
return [arguments isKindOfClass:[NSArray class]] && arguments.count == 1 &&
[arguments.firstObject isEqual:executable()];
}
}

void set(bool wanted) {
@autoreleasepool {
NSURL* file = agentFile();
NSError* error = nil;
if (!wanted) {
if (![[NSFileManager defaultManager] removeItemAtURL:file error:&error] &&
!(error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError)) {
throw std::runtime_error("macOS refused to stop starting Monky Light at login");
}
return;
}
if (![[NSFileManager defaultManager] createDirectoryAtURL:file.URLByDeletingLastPathComponent
withIntermediateDirectories:YES
attributes:nil
error:&error]) {
throw std::runtime_error("Could not create the LaunchAgents folder");
}
// Takes effect at the next login; nothing is started now.
NSDictionary* agent = @{
@"Label" : kLabel,
@"ProgramArguments" : @[ executable() ],
@"RunAtLoad" : @YES,
@"ProcessType" : @"Interactive",
@"LimitLoadToSessionType" : @"Aqua",
};
if (![agent writeToURL:file error:&error]) {
throw std::runtime_error("macOS refused to start Monky Light at login");
}
}
}

} // namespace

Startup systemStartup() { return {&enabled, &set}; }

} // namespace monky::light::ui
61 changes: 61 additions & 0 deletions apps/light/src/ui/startup_windows.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#include "platform/utf8.hpp" // Defines NOMINMAX and WIN32_LEAN_AND_MEAN before <windows.h>.
#include "ui/startup.hpp"

#include <stdexcept>
#include <string>
#include <vector>

namespace monky::light::ui {
namespace {

constexpr wchar_t kRunKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
constexpr wchar_t kValue[] = L"Monky Light";

// The quoted path of this executable and nothing else: no arguments is an
// application start, which reuses the saved server.
std::wstring command() {
std::vector<wchar_t> path(MAX_PATH);
for (;;) {
const auto length = GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
if (length == 0) throw std::runtime_error("Could not find the Monky Light executable");
if (length < path.size()) return L"\"" + std::wstring(path.data(), length) + L"\"";
path.resize(path.size() * 2);
}
}

bool enabled() {
DWORD size = 0;
if (RegGetValueW(HKEY_CURRENT_USER, kRunKey, kValue, RRF_RT_REG_SZ, nullptr, nullptr, &size) != ERROR_SUCCESS ||
size == 0) {
return false;
}
std::wstring value(size / sizeof(wchar_t), L'\0');
if (RegGetValueW(HKEY_CURRENT_USER, kRunKey, kValue, RRF_RT_REG_SZ, nullptr, value.data(), &size) !=
ERROR_SUCCESS) {
return false;
}
value.resize(size / sizeof(wchar_t));
while (!value.empty() && value.back() == L'\0') value.pop_back();
return CompareStringOrdinal(value.c_str(), -1, command().c_str(), -1, TRUE) == CSTR_EQUAL;
}

void set(bool wanted) {
if (wanted) {
const auto value = command();
const auto bytes = static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t));
if (RegSetKeyValueW(HKEY_CURRENT_USER, kRunKey, kValue, REG_SZ, value.c_str(), bytes) != ERROR_SUCCESS) {
throw std::runtime_error("Windows refused to start Monky Light with the system");
}
return;
}
const auto result = RegDeleteKeyValueW(HKEY_CURRENT_USER, kRunKey, kValue);
if (result != ERROR_SUCCESS && result != ERROR_FILE_NOT_FOUND) {
throw std::runtime_error("Windows refused to stop starting Monky Light with the system");
}
}

} // namespace

Startup systemStartup() { return {&enabled, &set}; }

} // namespace monky::light::ui
5 changes: 4 additions & 1 deletion apps/light/src/ui/tray.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "core_commands.hpp"
#include "ui/startup.hpp"
#include "ui/tray_strings.hpp"

#include <memory>
Expand All @@ -10,7 +11,9 @@ namespace monky::light::ui {
// Creates the platform tray. It must be called on the process main thread,
// before the core thread starts, because the returned interface is observed from
// the core thread. Throws when the platform interface cannot be created.
std::unique_ptr<CoreInterface> createTray(CoreCommands core, Language language);
// `startup` is offered only to an application start (see systemStartup()).
std::unique_ptr<CoreInterface> createTray(CoreCommands core, Language language,
std::optional<Startup> startup = std::nullopt);

// The operating system interface language, falling back to English.
Language systemLanguage();
Expand Down
27 changes: 25 additions & 2 deletions apps/light/src/ui/tray_controller.hpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#pragma once

#include "core_commands.hpp"
#include "ui/startup.hpp"
#include "ui/tray_model.hpp"

#include <exception>
#include <mutex>
#include <optional>
#include <string>
Expand All @@ -16,8 +18,8 @@ namespace monky::light::ui {
// the same command-accepted/command-error answers.
class TrayController final {
public:
TrayController(CoreCommands core, Language language)
: core_(std::move(core)), model_(language) {}
TrayController(CoreCommands core, Language language, std::optional<Startup> startup = std::nullopt)
: core_(std::move(core)), model_(language), startup_(std::move(startup)) {}

// Core thread. Returns true when the interface must redraw or notify.
bool observe(const Json& event) {
Expand All @@ -39,14 +41,33 @@ class TrayController final {
std::lock_guard lock(mutex_);
auto result = model_.presentation();
result.canSwitchServer = static_cast<bool>(core_.switchServer);
result.startupAvailable = startup_.has_value();
// Read when the menu is built, so a change made elsewhere is never hidden.
try {
result.startupChecked = startup_ && startup_->enabled();
} catch (const std::exception&) {
result.startupChecked = false;
}
return result;
}

std::optional<std::string> takeAlert() {
std::lock_guard lock(mutex_);
if (startupFailure_) return std::exchange(startupFailure_, std::nullopt);
return model_.takeAlert();
}

// Interface thread. A refusal is shown like any other failure.
void setStartup(bool enabled) {
if (!startup_) return;
try {
startup_->set(enabled);
} catch (const std::exception& error) {
std::lock_guard lock(mutex_);
startupFailure_ = error.what();
}
}

void join(const std::string& channelId) {
core_.command(Json{{"command", "join"}, {"channelId", channelId}});
}
Expand Down Expand Up @@ -117,6 +138,8 @@ class TrayController final {
mutable std::mutex mutex_;
TrayModel model_;
bool pressed_ = false;
std::optional<Startup> startup_;
std::optional<std::string> startupFailure_;
};

} // namespace monky::light::ui
Loading
Loading