From 22de35029b31b96c5b0886296c076b2832f802c7 Mon Sep 17 00:00:00 2001 From: Danilo Lemes Date: Sat, 28 Dec 2024 11:54:54 +0000 Subject: [PATCH] Select specific app to monitor via packets --- .idea/runConfigurations/Run.xml | 2 +- .idea/runConfigurations/Run_Standalone.xml | 13 ++ .../HardwareMonitor.sln.DotSettings.user | 3 + .../Monitor/MonitorPacketCommand.cs | 9 + .../HardwareMonitor/Monitor/MonitorPoller.cs | 61 ++++++- .../PresentMon/PresentMonPoller.cs | 71 +++++++- HardwareMonitor/HardwareMonitor/Program.cs | 2 +- .../HardwareMonitor/Sockets/SocketHost.cs | 37 +++- .../hardwaremonitor/HardwareMonitorData.kt | 3 +- .../core/designsystem/Typography.kt | 14 +- .../HardwareMonitorProcessManager.kt | 6 + .../hardwaremonitor/HardwareMonitorReader.kt | 91 +++++----- .../core/os/hardwaremonitor/SocketClient.kt | 103 +++++++++++ .../cleanmeter/core/os/util/MemoryUtils.kt | 6 + .../ui/components/CheckboxWithLabel.kt | 3 +- .../ui/components/dropdown/DropdownMenu.kt | 90 +++++++--- .../target/desktop/ui/settings/Settings.kt | 4 + .../desktop/ui/settings/SettingsViewModel.kt | 8 + .../desktop/ui/settings/tabs/AppSettingsUi.kt | 52 +++--- .../ui/settings/tabs/OverlaySettingsUi.kt | 161 ++++++++++-------- 20 files changed, 558 insertions(+), 181 deletions(-) create mode 100644 .idea/runConfigurations/Run_Standalone.xml create mode 100644 HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs create mode 100644 core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/SocketClient.kt diff --git a/.idea/runConfigurations/Run.xml b/.idea/runConfigurations/Run.xml index 2e415db..beddb34 100644 --- a/.idea/runConfigurations/Run.xml +++ b/.idea/runConfigurations/Run.xml @@ -8,7 +8,7 @@ \ No newline at end of file diff --git a/.idea/runConfigurations/Run_Standalone.xml b/.idea/runConfigurations/Run_Standalone.xml new file mode 100644 index 0000000..71c7d3c --- /dev/null +++ b/.idea/runConfigurations/Run_Standalone.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/HardwareMonitor/HardwareMonitor.sln.DotSettings.user b/HardwareMonitor/HardwareMonitor.sln.DotSettings.user index d8ae40b..43446f3 100644 --- a/HardwareMonitor/HardwareMonitor.sln.DotSettings.user +++ b/HardwareMonitor/HardwareMonitor.sln.DotSettings.user @@ -1,8 +1,10 @@  + ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -10,6 +12,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded \ No newline at end of file diff --git a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs new file mode 100644 index 0000000..b3f16c9 --- /dev/null +++ b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs @@ -0,0 +1,9 @@ +namespace HardwareMonitor.Monitor; + +public enum MonitorPacketCommand : short +{ + Data = 0, + RefreshPresentMonApps = 1, + SelectPresentMonApp = 2, + PresentMonApps = 3, +} \ No newline at end of file diff --git a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs index d060890..5459f9e 100644 --- a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs +++ b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs @@ -35,8 +35,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _computer.Open(); _computer.Accept(new UpdateVisitor()); - _presentMonPoller.Start(); + _presentMonPoller.Start(stoppingToken); _socketHost.StartServer(); + _socketHost.onClientData += OnClientData; + _socketHost.onClientConnected += OnClientConnected; var sharedMemoryData = QueryHardwareData(); @@ -46,6 +48,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) using var writer = new BinaryWriter(memoryStream); var accumulator = 0; + writer.Write((short)MonitorPacketCommand.Data); writer.Write(sharedMemoryData.Hardwares.Count); writer.Write(sharedMemoryData.Sensors.Count); @@ -97,6 +100,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { GC.Collect(); accumulator = 0; + SendPresentMonAppsToClients(); } accumulator += 500; @@ -107,6 +111,60 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) hostApplicationLifetime.StopApplication(); } + private void OnClientConnected() + { + SendPresentMonAppsToClients(); + } + + private void OnClientData(byte[] data) + { + var cmd = (MonitorPacketCommand) BitConverter.ToInt16(data, 0); + logger.LogInformation("Received command from client: {Command}", cmd); + switch (cmd) + { + case MonitorPacketCommand.RefreshPresentMonApps: + SendPresentMonAppsToClients(); + break; + case MonitorPacketCommand.SelectPresentMonApp: + SelectPresentMonApp(data); + break; + + // server -> client cases + case MonitorPacketCommand.Data: + case MonitorPacketCommand.PresentMonApps: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void SelectPresentMonApp(byte[] data) + { + // start at 2 because the first 2 were the command + var size = BitConverter.ToInt16(data, 2); + var appName = Encoding.UTF8.GetString(data, 4, size); + _presentMonPoller.SetSelectedApp(appName); + } + + private void SendPresentMonAppsToClients() + { + using var memoryStream = new MemoryStream(); + using var writer = new BinaryWriter(memoryStream); + + writer.Write((short)MonitorPacketCommand.PresentMonApps); + //logger.LogInformation("Sending presentmon apps to clients {Count}", _presentMonPoller.CurrentApps.Count); + writer.Write((short)_presentMonPoller.CurrentApps.Count); + foreach (var app in _presentMonPoller.CurrentApps) + { + writer.Write(GetBytes(app, SharedMemoryConsts.NameSize)); + } + + if (_socketHost.HasConnections()) + { + _socketHost.SendToAll(memoryStream.ToArray()); + } + } + private SharedMemoryData QueryHardwareData() { var hardwareList = new List(); @@ -155,6 +213,7 @@ private void Stop() _computer.Close(); _presentMonPoller.Stop(); _socketHost.Close(); + _socketHost.onClientData -= OnClientData; } private static SharedMemoryHardware MapHardware(IHardware hardware) => new() diff --git a/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs b/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs index 842dffe..f2ba92e 100644 --- a/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs +++ b/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs @@ -7,45 +7,58 @@ namespace HardwareMonitor.PresentMon; public class PresentMonPoller(ILogger logger) { + private const string NO_SELECTED_APP = "NONE"; + private IHardware _hardware = new PresentMonHardware(); public PresentMonSensor Displayed { get; private set; } public PresentMonSensor Presented { get; private set; } public PresentMonSensor Frametime { get; private set; } + public HashSet CurrentApps { get; private set; } private Process _process; private CultureInfo _cultureInfo = (CultureInfo)CultureInfo.CurrentCulture.Clone(); - public async void Start() + private string _currentSelectedApp = NO_SELECTED_APP; + + + public async void Start(CancellationToken stoppingToken) { _cultureInfo.NumberFormat.NumberDecimalSeparator = "."; Displayed = new PresentMonSensor(_hardware, "displayed", 0, "Displayed Frames"); Presented = new PresentMonSensor(_hardware, "presented", 1, "Presented Frames"); Frametime = new PresentMonSensor(_hardware, "frametime", 2, "Frametime"); + CurrentApps = []; - using var reader = new StreamReader(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "presentmon", "ignored-processes.txt")); - var text = await reader.ReadToEndAsync(); - var processes = text + using var reader = new StreamReader(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "presentmon", + "ignored-processes.txt")); + var text = (await reader.ReadToEndAsync()) .Split("\n", StringSplitOptions.RemoveEmptyEntries) .Select(x => $"--exclude {x.Trim()}"); + var filteredApps = string.Join(" ", text); + await TerminateCurrentPresentMon(); var processStartInfo = new ProcessStartInfo { CreateNoWindow = true, RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false, FileName = "presentmon\\presentmon.exe", - Arguments = $"--stop_existing_session --no_console_stats --output_stdout {string.Join(' ', processes)}" + Arguments = $"--stop_existing_session --no_console_stats --output_stdout --session_name HardwareMonitor {filteredApps}", }; + logger.LogInformation("Starting PresentMon process with {Arguments}", processStartInfo.Arguments); + _process = new Process(); _process.StartInfo = processStartInfo; - _process.OutputDataReceived += (sender, args) => ParseData(args.Data); - _process.Exited += (sender, args) => Start(); - + _process.ErrorDataReceived += (sender, args) => logger.LogError(args.Data); + _process.Start(); _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + ClearCurrentAppsAsync(stoppingToken); await _process.WaitForExitAsync(); } @@ -60,6 +73,13 @@ private void ParseData(string? argsData) if (argsData != null) { parts = argsData.Split(","); + CurrentApps.Add(parts[0]); + + if (_currentSelectedApp != NO_SELECTED_APP && _currentSelectedApp != parts[0]) + { + return; + } + if (float.TryParse(parts[9], NumberStyles.Any, _cultureInfo, out var frametime)) { Frametime.Value = frametime; @@ -76,4 +96,39 @@ private void ParseData(string? argsData) } } } + + public void SetSelectedApp(string appName) + { + if (appName == "Auto") { + _currentSelectedApp = NO_SELECTED_APP; + return; + } + _currentSelectedApp = appName; + } + private async Task TerminateCurrentPresentMon() + { + var processStartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + FileName = "presentmon\\presentmon.exe", + Arguments = $"--terminate_existing_session --no_console_stats --output_stdout --session_name HardwareMonitor", + }; + logger.LogInformation("Starting PresentMon process with {Arguments}", processStartInfo.Arguments); + + var process = new Process(); + process.StartInfo = processStartInfo; + process.Start(); + await process.WaitForExitAsync(); + } + + private async Task ClearCurrentAppsAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) return; + await Task.Delay(10_000, cancellationToken); + CurrentApps.Clear(); + ClearCurrentAppsAsync(cancellationToken); + } } \ No newline at end of file diff --git a/HardwareMonitor/HardwareMonitor/Program.cs b/HardwareMonitor/HardwareMonitor/Program.cs index 76481e4..3906db8 100644 --- a/HardwareMonitor/HardwareMonitor/Program.cs +++ b/HardwareMonitor/HardwareMonitor/Program.cs @@ -16,7 +16,7 @@ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LogFiles", $"{DateTime.Now.Year}-{DateTime.Now.Month}-{DateTime.Now.Day}", "Log.txt"), rollingInterval: RollingInterval.Infinite, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}") + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {Message}{NewLine}{Exception}") .WriteTo.Console() ); diff --git a/HardwareMonitor/HardwareMonitor/Sockets/SocketHost.cs b/HardwareMonitor/HardwareMonitor/Sockets/SocketHost.cs index 49b6d25..a62019d 100644 --- a/HardwareMonitor/HardwareMonitor/Sockets/SocketHost.cs +++ b/HardwareMonitor/HardwareMonitor/Sockets/SocketHost.cs @@ -8,12 +8,16 @@ public class SocketHost(ILogger logger) { private Socket _listener; private List _clients = new(); - + private byte[] _receiveBuffer = new byte[2048]; + + public Action onClientData; + public Action onClientConnected; + public async void StartServer() { IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 31337); logger.LogInformation("Listening for connections on {LocalEndPoint}", localEndPoint); - + _listener = new Socket(localEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); _listener.Bind(localEndPoint); _listener.Listen(); @@ -25,9 +29,24 @@ private void OnConnection(IAsyncResult asyncResult) { var server = (Socket)asyncResult.AsyncState!; var client = server.EndAccept(asyncResult); - + client.BeginReceive(_receiveBuffer, 0, _receiveBuffer.Length, SocketFlags.None, OnDataReceived, client); + _clients.Add(client); server.BeginAccept(OnConnection, server); + onClientConnected?.Invoke(); + } + + private void OnDataReceived(IAsyncResult asyncResult) + { + var client = (Socket)asyncResult.AsyncState!; + int received = client.EndReceive(asyncResult); + + if (received > 0) + { + onClientData?.Invoke(_receiveBuffer); + } + + client.BeginReceive(_receiveBuffer, 0, _receiveBuffer.Length, SocketFlags.None, OnDataReceived, client); } public void Close() @@ -35,18 +54,21 @@ public void Close() _clients.ForEach(it => it.Close()); _listener.Close(); } - + public bool HasConnections() => _clients.Count > 0; public void SendToAll(byte[] memoryStream) { + var listWithSize = memoryStream.ToList(); + listWithSize.InsertRange(2, BitConverter.GetBytes(listWithSize.Count - 2)); for (var i = 0; i < _clients.Count; i++) { if (_clients[i].IsConnected()) { - _clients[i].SendAsync(memoryStream, SocketFlags.None); + _clients[i].SendAsync(listWithSize.ToArray(), SocketFlags.None); } } + listWithSize.Clear(); } } @@ -58,6 +80,9 @@ public static bool IsConnected(this Socket socket) { return !(socket.Poll(1, SelectMode.SelectRead) && socket.Available == 0); } - catch (SocketException) { return false; } + catch (SocketException) + { + return false; + } } } \ No newline at end of file diff --git a/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt b/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt index 6d8d555..589b62a 100644 --- a/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt +++ b/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt @@ -6,7 +6,8 @@ import kotlinx.serialization.Serializable data class HardwareMonitorData( val LastPollTime: Long, val Hardwares: List, - val Sensors: List + val Sensors: List, + val PresentMonApps: List, ) { @Serializable data class Hardware( diff --git a/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt b/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt index 80ebc16..f26e28c 100644 --- a/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt +++ b/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt @@ -119,6 +119,18 @@ class Typography { * lineHeight = 0.sp, * fontWeight = W400, */ + val labelSMedium: TextStyle + @Composable get() = defaultTextStyle.copy( + fontSize = 12.sp, + lineHeight = 0.sp, + fontFamily = fontFamilyMedium, + ) + + /** + * fontSize = 12.sp, + * lineHeight = 0.sp, + * fontWeight = W600, + */ val labelSSemiBold: TextStyle @Composable get() = defaultTextStyle.copy( fontSize = 12.sp, @@ -148,7 +160,7 @@ class Typography { // Font(resource = "font/inter_black.ttf", weight = FontWeight.Black), private val fontFamilyThin = FontFamily( - Font(resource = "font/inter_thin.ttf", weight = FontWeight.Normal), + Font(resource = "font/inter_thin.ttf", weight = FontWeight.SemiBold), ) private val fontFamilyNormal = FontFamily( diff --git a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt index 2772235..3d5bb6e 100644 --- a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt +++ b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt @@ -10,6 +10,12 @@ import java.util.* object HardwareMonitorProcessManager { private var process: Process? = null + fun checkRuntime() { + ProcessBuilder().apply { + command("dotnet --list-runtimes") + }.start() + } + fun start() { val currentDir = Path.of("").toAbsolutePath().toString() val file = if (isDev()) { diff --git a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorReader.kt b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorReader.kt index 24c3299..46295b8 100644 --- a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorReader.kt +++ b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorReader.kt @@ -4,13 +4,15 @@ import app.cleanmeter.core.common.hardwaremonitor.HardwareMonitorData import app.cleanmeter.core.os.util.getByteBuffer import app.cleanmeter.core.os.util.readString import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import java.io.InputStream -import java.net.InetSocketAddress +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.mapNotNull import java.net.Socket -import java.net.SocketException import java.nio.ByteBuffer private const val HARDWARE_SIZE = 260 @@ -18,57 +20,52 @@ private const val SENSOR_SIZE = 392 private const val NAME_SIZE = 128 private const val IDENTIFIER_SIZE = 128 private const val HEADER_SIZE = 8 +private const val LENGTH_SIZE = 2 -object HardwareMonitorReader { - - val currentData = flow { - var socket = Socket() +enum class Command(val value: Short) { + Data(0), + RefreshPresentMonApps(1), + SelectPresentMonApp(2), + PresentMonApps(3); - while (true) { + companion object { + fun fromValue(value: Short) = entries.find { it.value == value } ?: Data + } +} - // try open a connection with HardwareMonitor - if (!socket.isConnected) { - try { - println("Trying to connect") - socket = Socket() - socket.connect(InetSocketAddress("0.0.0.0", 31337)) - println("Connected ${socket.isConnected}") - } catch (ex: Exception) { - println("Couldn't connect ${ex.message}") - if (ex !is SocketException) { - ex.printStackTrace() - } - } finally { - delay(500) - continue - } - } +object HardwareMonitorReader { - val inputStream = socket.inputStream - while (socket.isConnected) { - try { + private var _currentData: HardwareMonitorData = HardwareMonitorData(0L, emptyList(), emptyList(), emptyList()) + val currentData: Flow = SocketClient + .packetFlow + .mapNotNull { packet -> + when (packet) { + is Packet.Data -> { // read first 8 bytes to get the amount of hardware and sensors - val (hardware, sensor) = readHardwareAndSensorCount(inputStream) + val (hardware, sensor) = readHardwareAndSensorCount(packet.data) + if (hardware + sensor <= 0) return@mapNotNull null - // if both are 0, bail - if (hardware + sensor == 0) continue - - // we know the length in bytes of hardware and sensor, so we know the length of the packet - val buffer = getByteBuffer(inputStream, hardware * HARDWARE_SIZE + sensor * SENSOR_SIZE) + val buffer = getByteBuffer(packet.data, hardware * HARDWARE_SIZE + sensor * SENSOR_SIZE, HEADER_SIZE) val hardwares = readHardware(buffer, hardware) val sensors = readSensor(buffer, sensor) - emit(HardwareMonitorData(0L, hardwares, sensors)) - } catch (e: SocketException) { - socket.close() - socket = Socket() - e.printStackTrace() + _currentData = _currentData.copy(Hardwares = hardwares, Sensors = sensors) + _currentData + } + + is Packet.PresentMonApps -> { + val appsCount = getByteBuffer(packet.data, LENGTH_SIZE, 0).short + val buffer = getByteBuffer(packet.data, appsCount.toInt() * NAME_SIZE, LENGTH_SIZE) + val apps = listOf("Auto") + readPresentMonApps(buffer, appsCount) + _currentData = _currentData.copy(PresentMonApps = apps) + _currentData } + + is Packet.SelectPresentMonApp -> null } } - }.flowOn(Dispatchers.IO) - private fun readHardwareAndSensorCount(input: InputStream): Pair { - val buffer = getByteBuffer(input, HEADER_SIZE) + private fun readHardwareAndSensorCount(input: ByteArray): Pair { + val buffer = getByteBuffer(input, HEADER_SIZE, 0) return buffer.int to buffer.int } @@ -99,4 +96,12 @@ object HardwareMonitorReader { } } } + + private fun readPresentMonApps(buffer: ByteBuffer, count: Short): List { + return buildList { + for (i in 0 until count) { + add(buffer.readString(NAME_SIZE)) + } + } + } } \ No newline at end of file diff --git a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/SocketClient.kt b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/SocketClient.kt new file mode 100644 index 0000000..975c90b --- /dev/null +++ b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/SocketClient.kt @@ -0,0 +1,103 @@ +package app.cleanmeter.core.os.hardwaremonitor + +import app.cleanmeter.core.os.util.getByteBuffer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import java.io.InputStream +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketException +import java.nio.ByteBuffer +import java.nio.ByteOrder + +private const val COMMAND_SIZE = 2 +private const val LENGTH_SIZE = 4 + +sealed class Packet { + data class Data(val data: ByteArray) : Packet() + data class PresentMonApps(val data: ByteArray) : Packet() + data class SelectPresentMonApp(val name: String) : Packet() +} + +object SocketClient { + + private var socket = Socket() + + private val packetChannel = Channel(Channel.CONFLATED) + val packetFlow: Flow = packetChannel.receiveAsFlow() + + init { + connect() + } + + private fun connect() = CoroutineScope(Dispatchers.IO).launch { + while (true) { + // try open a connection with HardwareMonitor + if (!socket.isConnected) { + try { + println("Trying to connect") + socket = Socket() + socket.connect(InetSocketAddress("0.0.0.0", 31337)) + println("Connected ${socket.isConnected}") + } catch (ex: Exception) { + println("Couldn't connect ${ex.message}") +// if (ex !is SocketException) { + ex.printStackTrace() +// } + } finally { + delay(500) + continue + } + } + + val inputStream = socket.inputStream + while(socket.isConnected) { + try { + val command = getCommand(inputStream) + val size = getSize(inputStream) + when (command) { + Command.Data -> packetChannel.trySend(Packet.Data(inputStream.readNBytes(size))) + Command.PresentMonApps -> packetChannel.trySend(Packet.PresentMonApps(inputStream.readNBytes(size))) + Command.RefreshPresentMonApps -> Unit + Command.SelectPresentMonApp -> Unit + } + } catch (e: SocketException) { + socket.close() + socket = Socket() + e.printStackTrace() + } + } + } + } + + private fun getCommand(inputStream: InputStream): Command { + val buffer = getByteBuffer(inputStream, COMMAND_SIZE) + return Command.fromValue(buffer.short) + } + + private fun getSize(inputStream: InputStream): Int { + val buffer = getByteBuffer(inputStream, LENGTH_SIZE) + return buffer.int + } + + fun sendPacket(selectPresentMonApp: Packet.SelectPresentMonApp) { + if (socket.isConnected) { + val nameBytes = selectPresentMonApp.name.toByteArray() + val buffer = ByteBuffer.allocate(2 + 2 + nameBytes.count()).order(ByteOrder.LITTLE_ENDIAN).apply { + putShort(Command.SelectPresentMonApp.value) + putShort(nameBytes.size.toShort()) + put(nameBytes) + }.array() + println("Sending ${buffer.count()} bytes") + socket.outputStream.apply { + write(buffer) + flush() + } + } + } +} diff --git a/core/native/src/main/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt b/core/native/src/main/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt index 9b50402..ddf5aa7 100644 --- a/core/native/src/main/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt +++ b/core/native/src/main/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt @@ -8,9 +8,15 @@ import java.nio.charset.Charset import java.util.Locale internal fun getByteBuffer(input: InputStream, length: Int): ByteBuffer { + if (length <= 0) return ByteBuffer.allocate(0).order(ByteOrder.LITTLE_ENDIAN) return ByteBuffer.wrap(input.readNBytes(length)).order(ByteOrder.LITTLE_ENDIAN) } +internal fun getByteBuffer(input: ByteArray, length: Int, offset: Int): ByteBuffer { + if (length <= 0) return ByteBuffer.allocate(0) + return ByteBuffer.wrap(input).slice(offset, length).order(ByteOrder.LITTLE_ENDIAN) +} + internal fun getByteBuffer(pointer: Pointer, size: Int, offset: Int = 0): ByteBuffer { val buffer = ByteBuffer.allocateDirect(size) buffer.put(pointer.getByteArray(0, size)) diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/CheckboxWithLabel.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/CheckboxWithLabel.kt index 613445b..5c0cddc 100644 --- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/CheckboxWithLabel.kt +++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/CheckboxWithLabel.kt @@ -33,7 +33,8 @@ fun CheckboxWithLabel( colors = CheckboxDefaults.colors( checkedColor = LocalColorScheme.current.background.brand, uncheckedColor = LocalColorScheme.current.background.surfaceSunken, - checkmarkColor = LocalColorScheme.current.background.surfaceRaised + checkmarkColor = LocalColorScheme.current.background.surfaceRaised, + disabledColor = LocalColorScheme.current.background.surfaceSunkenSubtle, ), modifier = Modifier.size(24.dp) ) diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/dropdown/DropdownMenu.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/dropdown/DropdownMenu.kt index 2d4fe3e..0348243 100644 --- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/dropdown/DropdownMenu.kt +++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/dropdown/DropdownMenu.kt @@ -3,10 +3,12 @@ package app.cleanmeter.target.desktop.ui.components.dropdown import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.DropdownMenuItem import androidx.compose.material.ExperimentalMaterialApi @@ -15,6 +17,7 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.rounded.ChevronRight import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -32,9 +35,12 @@ import app.cleanmeter.core.designsystem.LocalTypography @OptIn(ExperimentalMaterialApi::class) @Composable fun DropdownMenu( + label: String? = null, + disclaimer: String? = null, options: List, selectedIndex: Int, - onValueChanged: (Int) -> Unit + onValueChanged: (Int) -> Unit, + modifier: Modifier = Modifier, ) { var expanded by remember { mutableStateOf(false) } var selectedOption by remember { mutableStateOf(options[selectedIndex]) } @@ -42,33 +48,69 @@ fun DropdownMenu( ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = !expanded }, - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .border(1.dp, LocalColorScheme.current.border.bolder, RoundedCornerShape(8.dp)) - .background(LocalColorScheme.current.background.surfaceRaised).padding(12.dp) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Text( - text = selectedOption, - color = LocalColorScheme.current.text.heading, - style = LocalTypography.current.labelL, - modifier = Modifier.align(Alignment.CenterVertically) - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .border(1.dp, LocalColorScheme.current.border.bolder, RoundedCornerShape(8.dp)) + .background(LocalColorScheme.current.background.surfaceRaised).padding(12.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (label != null) { + Text( + text = label, + color = LocalColorScheme.current.text.paragraph1, + style = LocalTypography.current.labelL, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } - IconButton(onClick = { }, modifier = Modifier.size(20.dp).clearAndSetSemantics { }) { - Icon( - imageVector = Icons.Rounded.ChevronRight, - contentDescription = "Trailing icon for exposed dropdown menu", - tint = LocalColorScheme.current.icon.bolderActive, - modifier = Modifier.rotate( - if (expanded) 270f - else 90f + Text( + text = selectedOption, + color = LocalColorScheme.current.text.heading, + style = LocalTypography.current.labelLMedium, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + + IconButton(onClick = { }, modifier = Modifier.size(20.dp).clearAndSetSemantics { }) { + Icon( + imageVector = Icons.Rounded.ChevronRight, + contentDescription = "Trailing icon for exposed dropdown menu", + tint = LocalColorScheme.current.icon.bolderActive, + modifier = Modifier.rotate( + if (expanded) 270f + else 90f + ) + ) + } + } + + if (disclaimer != null) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = "Trailing icon for exposed dropdown menu", + tint = LocalColorScheme.current.icon.bolderActive, + modifier = Modifier.size(16.dp) ) - ) + Text( + text = disclaimer, + color = LocalColorScheme.current.text.disabled, + style = LocalTypography.current.labelSMedium, + modifier = Modifier.wrapContentHeight(align = Alignment.CenterVertically) + ) + } } } diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/Settings.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/Settings.kt index 22fbe43..042d3c2 100644 --- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/Settings.kt +++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/Settings.kt @@ -113,10 +113,14 @@ private fun TabContent( onDisplaySelect = { viewModel.onEvent(SettingsEvent.DisplaySelect(it)) }, + onFpsApplicationSelect = { + viewModel.onEvent(SettingsEvent.FpsApplicationSelect(it)) + }, getCpuSensorReadings = { settingsState.hardwareData?.cpuReadings() ?: emptyList() }, getGpuSensorReadings = { settingsState.hardwareData?.gpuReadings() ?: emptyList() }, getNetworkSensorReadings = { settingsState.hardwareData?.networkReadings() ?: emptyList() }, getHardwareSensors = { settingsState.hardwareData?.Hardwares ?: emptyList() }, + getPresentMonApps = { settingsState.hardwareData?.PresentMonApps ?: emptyList() }, ) 1 -> StyleUi( diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/SettingsViewModel.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/SettingsViewModel.kt index 0bc2f35..a733bd4 100644 --- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/SettingsViewModel.kt +++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/SettingsViewModel.kt @@ -3,6 +3,8 @@ package app.cleanmeter.target.desktop.ui.settings import androidx.compose.ui.unit.IntOffset import androidx.lifecycle.ViewModel import app.cleanmeter.core.common.hardwaremonitor.HardwareMonitorData +import app.cleanmeter.core.os.hardwaremonitor.Packet +import app.cleanmeter.core.os.hardwaremonitor.SocketClient import app.cleanmeter.target.desktop.data.ObserveHardwareReadings import app.cleanmeter.target.desktop.data.OverlaySettingsRepository import app.cleanmeter.target.desktop.model.OverlaySettings @@ -31,6 +33,7 @@ sealed class SettingsEvent { data class OverlayOpacityChange(val opacity: Float) : SettingsEvent() data class OverlayGraphChange(val progressType: OverlaySettings.ProgressType) : SettingsEvent() data class DarkThemeToggle(val isEnabled: Boolean) : SettingsEvent() + data class FpsApplicationSelect(val applicationName: String) : SettingsEvent() } class SettingsViewModel : ViewModel() { @@ -75,9 +78,14 @@ class SettingsViewModel : ViewModel() { is SettingsEvent.OverlayOpacityChange -> onOverlayOpacityChange(event.opacity, this) is SettingsEvent.OverlayGraphChange -> onOverlayGraphChange(event.progressType, this) is SettingsEvent.DarkThemeToggle -> onDarkModeToggle(event.isEnabled, this) + is SettingsEvent.FpsApplicationSelect -> onFpsApplicationSelect(event.applicationName, this) } } + private fun onFpsApplicationSelect(applicationName: String, settingsState: SettingsState) { + SocketClient.sendPacket(Packet.SelectPresentMonApp(applicationName)) + } + private fun onDarkModeToggle(enabled: Boolean, settingsState: SettingsState) { with(settingsState) { val newSettings = overlaySettings?.copy(isDarkTheme = enabled) diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/AppSettingsUi.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/AppSettingsUi.kt index a729064..efdfcee 100644 --- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/AppSettingsUi.kt +++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/AppSettingsUi.kt @@ -18,6 +18,7 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AdminPanelSettings import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -110,33 +111,34 @@ fun AppSettingsUi( private fun startWithWindowsCheckbox() { var state by remember { mutableStateOf(WinRegistry.isAppRegisteredToStartWithWindows()) } - CheckboxWithLabel( - label = "Start with Windows", - checked = state, - onCheckedChange = { value -> - state = value - if (value) { - WinRegistry.registerAppToStartWithWindows() - } else { - WinRegistry.removeAppFromStartWithWindows() - } + LaunchedEffect(Unit) { + if (state) { + WinRegistry.removeAppFromStartWithWindows() } - ) { - TooltipArea( - delayMillis = 0, - tooltip = { - Text( - text = "Admin rights needed", - style = LocalTypography.current.labelM, - color = LocalColorScheme.current.text.heading, - ) - }) { - Icon( - imageVector = Icons.Filled.AdminPanelSettings, - contentDescription = null, - tint = LocalColorScheme.current.icon.bolderActive + } + + TooltipArea( + delayMillis = 0, + tooltip = { + Text( + text = "Temporarily disabled.", + style = LocalTypography.current.labelM, + color = LocalColorScheme.current.text.heading, ) - } + }) { + CheckboxWithLabel( + label = "Start with Windows", + checked = state, + enabled = false, + onCheckedChange = { value -> + state = value + if (value) { + WinRegistry.registerAppToStartWithWindows() + } else { + WinRegistry.removeAppFromStartWithWindows() + } + } + ) } } diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/OverlaySettingsUi.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/OverlaySettingsUi.kt index cf163b1..30d74d0 100644 --- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/OverlaySettingsUi.kt +++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/OverlaySettingsUi.kt @@ -17,12 +17,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.cleanmeter.core.common.hardwaremonitor.HardwareMonitorData import app.cleanmeter.core.designsystem.LocalColorScheme +import app.cleanmeter.core.designsystem.LocalTypography import app.cleanmeter.target.desktop.model.OverlaySettings import app.cleanmeter.target.desktop.ui.components.section.CheckboxSection import app.cleanmeter.target.desktop.ui.components.CheckboxWithLabel import app.cleanmeter.target.desktop.ui.components.section.CustomBodyCheckboxSection import app.cleanmeter.target.desktop.ui.components.section.DropdownSection import app.cleanmeter.target.desktop.ui.components.KeyboardShortcutInfoLabel +import app.cleanmeter.target.desktop.ui.components.dropdown.DropdownMenu import app.cleanmeter.target.desktop.ui.components.dropdown.SensorReadingDropdownMenu import app.cleanmeter.target.desktop.ui.settings.CheckboxSectionOption import app.cleanmeter.target.desktop.ui.settings.SectionType @@ -39,10 +41,12 @@ fun OverlaySettingsUi( onSectionSwitchToggle: (SectionType, Boolean) -> Unit, onCustomSensorSelect: (SensorType, String) -> Unit, onDisplaySelect: (Int) -> Unit, + onFpsApplicationSelect: (String) -> Unit, getCpuSensorReadings: () -> List, getGpuSensorReadings: () -> List, getNetworkSensorReadings: () -> List, getHardwareSensors: () -> List, + getPresentMonApps: () -> List, ) = Column( modifier = Modifier.padding(bottom = 8.dp, top = 20.dp).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) @@ -52,11 +56,33 @@ fun OverlaySettingsUi( KeyboardShortcutInfoLabel() - CheckboxSection( + CustomBodyCheckboxSection( title = "FPS", options = availableOptions.filterOptions(SensorType.Framerate, SensorType.Frametime), - onOptionToggle = onOptionsToggle, - onSwitchToggle = { onSectionSwitchToggle(SectionType.Fps, it) } + onSwitchToggle = { onSectionSwitchToggle(SectionType.Fps, it) }, + body = { options -> + Column(modifier = Modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) { + options.forEach { option -> + CheckboxWithLabel( + label = option.name, + enabled = option.useCheckbox, + onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) }, + checked = option.isSelected, + ) + } + val presentMonApps = getPresentMonApps() + if (presentMonApps.isNotEmpty()) { + DropdownMenu( + label = "Monitored app:", + disclaimer = "Apps are auto updated every 10 seconds.", + options = presentMonApps, + selectedIndex = 0, + onValueChanged = { onFpsApplicationSelect(presentMonApps[it]) }, + modifier = Modifier.padding(top = 8.dp) + ) + } + } + } ) CustomBodyCheckboxSection( @@ -73,29 +99,29 @@ fun OverlaySettingsUi( options.forEach { option -> val readings = getGpuSensorReadings().filter { it.SensorType == option.dataType } - Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) { - CheckboxWithLabel( - label = option.name, - enabled = option.useCheckbox, - onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) }, - checked = option.isSelected, - ) + Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) { + CheckboxWithLabel( + label = option.name, + enabled = option.useCheckbox, + onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) }, + checked = option.isSelected, + ) - if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) { - SensorReadingDropdownMenu( - options = readings, - onValueChanged = { - onCustomSensorSelect(option.type, it.Identifier) - }, - selectedIndex = readings - .indexOfFirst { it.Identifier == option.optionReadingId } - .coerceAtLeast(0), - label = "Sensor:", - sensorName = option.name, - ) - } + if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) { + SensorReadingDropdownMenu( + options = readings, + onValueChanged = { + onCustomSensorSelect(option.type, it.Identifier) + }, + selectedIndex = readings + .indexOfFirst { it.Identifier == option.optionReadingId } + .coerceAtLeast(0), + label = "Sensor:", + sensorName = option.name, + ) } } + } } } ) @@ -109,27 +135,27 @@ fun OverlaySettingsUi( options.forEach { option -> val readings = getCpuSensorReadings().filter { it.SensorType == option.dataType } - Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) { - CheckboxWithLabel( - label = option.name, - onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) }, - checked = option.isSelected, + Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) { + CheckboxWithLabel( + label = option.name, + onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) }, + checked = option.isSelected, + ) + if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) { + SensorReadingDropdownMenu( + options = readings, + onValueChanged = { + onCustomSensorSelect(option.type, it.Identifier) + }, + selectedIndex = readings + .indexOfFirst { it.Identifier == option.optionReadingId } + .coerceAtLeast(0), + label = "Sensor:", + sensorName = option.name, ) - if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) { - SensorReadingDropdownMenu( - options = readings, - onValueChanged = { - onCustomSensorSelect(option.type, it.Identifier) - }, - selectedIndex = readings - .indexOfFirst { it.Identifier == option.optionReadingId } - .coerceAtLeast(0), - label = "Sensor:", - sensorName = option.name, - ) - } } } + } } } ) @@ -152,34 +178,34 @@ fun OverlaySettingsUi( body = { options -> Column(modifier = Modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) { options.forEach { option -> - val readings = getNetworkSensorReadings().sortedBy { it.HardwareIdentifier }.filter { it.SensorType == option.dataType } + val readings = getNetworkSensorReadings().sortedBy { it.HardwareIdentifier }.filter { it.SensorType == option.dataType } - Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) { - CheckboxWithLabel( - label = option.name, - enabled = option.useCheckbox, - onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) }, - checked = option.isSelected, - ) + Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) { + CheckboxWithLabel( + label = option.name, + enabled = option.useCheckbox, + onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) }, + checked = option.isSelected, + ) - if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) { - SensorReadingDropdownMenu( - dropdownLabel = { - "${getHardwareSensors().firstOrNull { hardware -> hardware.Identifier == it.HardwareIdentifier }?.Name}: ${it.Name} (${it.Value} - ${it.SensorType})" - }, - options = readings, - onValueChanged = { - onCustomSensorSelect(option.type, it.Identifier) - }, - selectedIndex = readings - .indexOfFirst { it.Identifier == option.optionReadingId } - .coerceAtLeast(0), - label = "Sensor:", - sensorName = option.name, - ) - } + if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) { + SensorReadingDropdownMenu( + dropdownLabel = { + "${getHardwareSensors().firstOrNull { hardware -> hardware.Identifier == it.HardwareIdentifier }?.Name}: ${it.Name} (${it.Value} - ${it.SensorType})" + }, + options = readings, + onValueChanged = { + onCustomSensorSelect(option.type, it.Identifier) + }, + selectedIndex = readings + .indexOfFirst { it.Identifier == option.optionReadingId } + .coerceAtLeast(0), + label = "Sensor:", + sensorName = option.name, + ) } } + } } } ) @@ -193,11 +219,8 @@ fun OverlaySettingsUi( Text( text = "May your frames be high, and temps be low.", - fontSize = 12.sp, color = LocalColorScheme.current.text.disabled, - lineHeight = 0.sp, - fontWeight = FontWeight(550), - letterSpacing = 0.14.sp, + style = LocalTypography.current.labelSMedium, textAlign = TextAlign.Right, modifier = Modifier.fillMaxWidth() )