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
9 changes: 8 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@
"Bash(echo \"BUILD_EXIT:$?\")",
"Bash(python -c \"import yaml; yaml.safe_load\\(open\\('.github/workflows/ci.yml'\\)\\)\")",
"Bash(Get-ChildItem -Recurse \"C:\\\\Users\\\\pasichdev\\\\AndroidStudioProjects\\\\linqora\\\\design_handoff_linqora\")",
"Bash(Select-Object FullName, Length)"
"Bash(Select-Object FullName, Length)",
"Bash(go env *)",
"PowerShell(go build *)",
"Bash(flutter analyze *)",
"Bash(flutter create *)",
"Bash(flutter build *)",
"PowerShell(ls \"C:\\\\Users\\\\pasichdev\\\\AndroidStudioProjects\\\\linqora\\\\LinqoraHost\\\\linqorahost.exe\"; \\(Get-Item \"C:\\\\Users\\\\pasichdev\\\\AndroidStudioProjects\\\\linqora\\\\LinqoraHost\\\\linqorahost.exe\"\\).LastWriteTime)",
"Bash(Select-Object Name, Length, LastWriteTime)"
]
}
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@


.claude
39 changes: 38 additions & 1 deletion LinqoraHost/cmd/linqora_gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"image/color"
"log/slog"
"os"
"sync"

"fyne.io/fyne/v2"
Expand All @@ -18,6 +19,7 @@ import (

"LinqoraHost/internal/config"
"LinqoraHost/internal/deviceinfo"
"LinqoraHost/internal/startup"
)

// ─────────────────────── log writer ───────────────────────
Expand Down Expand Up @@ -256,6 +258,26 @@ func buildSettingsTab() fyne.CanvasObject {
widget.NewFormItem("", e2eeCheck),
)

// Auto-start on Windows login (registry HKCU Run key).
if startup.IsSupported() {
enabled, _ := startup.IsEnabled()
autoStartCheck := widget.NewCheck("Run at Windows startup (starts minimised to tray)", nil)
autoStartCheck.SetChecked(enabled)
autoStartCheck.OnChanged = func(v bool) {
var err error
if v {
err = startup.Enable()
} else {
err = startup.Disable()
}
if err != nil {
statusLbl.SetText("⚠ Startup: " + err.Error())
autoStartCheck.SetChecked(!v) // revert
}
}
form.Append("", autoStartCheck)
}

saveBtn := widget.NewButtonWithIcon("Save Settings", theme.DocumentSaveIcon(), func() {
var p int
if _, err := fmt.Sscanf(portEntry.Text, "%d", &p); err != nil || p < 1 || p > 65535 {
Expand Down Expand Up @@ -305,6 +327,15 @@ func buildLogTab() (fyne.CanvasObject, *guiLogWriter) {
func RunGUI() {
hideConsole() // detach from terminal — no black console window

// --minimized: launched at Windows startup → hide to tray immediately.
minimized := false
for _, arg := range os.Args[1:] {
if arg == "--minimized" {
minimized = true
break
}
}

var err error
cfg, err = config.LoadConfig()
if err != nil {
Expand Down Expand Up @@ -348,5 +379,11 @@ func RunGUI() {
}

w.SetCloseIntercept(func() { w.Hide() })
w.ShowAndRun()

if minimized {
// Started at Windows login: stay hidden in tray, don't show the window.
a.Run()
} else {
w.ShowAndRun()
}
}
6 changes: 4 additions & 2 deletions LinqoraHost/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module LinqoraHost

go 1.24
go 1.25.0

require (
fyne.io/fyne/v2 v2.7.3
Expand Down Expand Up @@ -32,10 +32,12 @@ require (
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/rymdport/portal v0.4.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
Expand All @@ -58,6 +60,6 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/shirou/gopsutil/v4 v4.26.4
github.com/spf13/cobra v1.10.2
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4
golang.org/x/sys v0.43.0
)
7 changes: 7 additions & 0 deletions LinqoraHost/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCsc
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
Expand All @@ -63,6 +64,8 @@ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe9
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
Expand All @@ -78,6 +81,8 @@ github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
Expand Down Expand Up @@ -125,6 +130,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
Expand All @@ -135,6 +141,7 @@ golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
3 changes: 3 additions & 0 deletions LinqoraHost/internal/collectors/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type SystemMetrics struct {
CPUUMetrics metrics.CPUMetrics `json:"cpuMetrics"`
RamMetrics metrics.RamMetrics `json:"ramMetrics"`
GpuLoadPercent int `json:"gpuLoadPercent"`
GpuTemperature int `json:"gpuTemperature"`
Timestamp int64 `json:"timestamp"`
}

Expand Down Expand Up @@ -127,11 +128,13 @@ func (mc *MetricsCollector) collectMetrics() (*SystemMetrics, error) {
}

gpuLoad := metrics.GetGPULoadPercent()
gpuTemp := metrics.GetGPUTemperature()

result := &SystemMetrics{
CPUUMetrics: cpuMetrics,
RamMetrics: ramMetrics,
GpuLoadPercent: gpuLoad,
GpuTemperature: gpuTemp,
Timestamp: time.Now().Unix(),
}

Expand Down
87 changes: 14 additions & 73 deletions LinqoraHost/internal/metrics/cpu.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package metrics

import (
"fmt"
"math"
"strings"
"time"

"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/process"
"github.com/shirou/gopsutil/v4/sensors"
)

type CPUMetrics struct {
Expand All @@ -26,67 +23,54 @@ type CPUInfo struct {
}

func GetCPUInfo() (CPUInfo, error) {
info := CPUInfo{
Model: "Unknown",
LogicalCores: 0,
PhysicalCores: 0,
Frequency: 0.0,
}
info := CPUInfo{Model: "Unknown"}

logicalCores, logErr := cpu.Counts(true)
physicalCores, psyhErr := cpu.Counts(false)

if logErr == nil && psyhErr == nil {
info.LogicalCores = logicalCores
info.PhysicalCores = physicalCores
}

cpuArray, cpuError := cpu.Info()
if cpuError == nil {
if len(cpuArray) > 0 {
c := cpuArray[0]
info.Model = c.ModelName
info.Frequency = math.Round(c.Mhz*100) / 100
}
if cpuError == nil && len(cpuArray) > 0 {
c := cpuArray[0]
info.Model = c.ModelName
info.Frequency = math.Round(c.Mhz*100) / 100
}

return info, nil
}

func GetCPUMetrics() (CPUMetrics, error) {
cpu := CPUMetrics{}
m := CPUMetrics{}

load, loadErr := GetCPULoad()
temp, tempErr := GetCPUTemperature()
temp, tempErr := GetCPUTemperature() // cpu_temp_windows.go / cpu_temp_others.go
proc, thrd, prcThErr := GetProcessesAndThreads()

if loadErr == nil {
cpu.LoadPercent = load
m.LoadPercent = load
}
if tempErr == nil {
cpu.Temperature = temp
m.Temperature = temp
}
if prcThErr == nil {
cpu.Processes = float64(proc)
cpu.Threads = float64(thrd)
m.Processes = float64(proc)
m.Threads = float64(thrd)
}

return cpu, nil
return m, nil
}

// InitCPUBaseline seeds the gopsutil CPU counter so that the first call to
// InitCPUBaseline seeds the gopsutil CPU counter so the first call to
// GetCPULoad() returns a meaningful value instead of 0.
// Call once when the metrics collector starts.
func InitCPUBaseline() {
cpu.Percent(0, false) //nolint:errcheck — baseline seed, result discarded
// Small sleep so the OS has time to record a non-zero interval
cpu.Percent(0, false) //nolint:errcheck
time.Sleep(200 * time.Millisecond)
cpu.Percent(0, false) //nolint:errcheck
}

// GetCPULoad returns overall CPU usage percent.
// Uses interval=0 which measures since the previous call — non-blocking.
// InitCPUBaseline must be called once before the first call to this function.
func GetCPULoad() (float64, error) {
percentages, err := cpu.Percent(0, false)
if err != nil {
Expand All @@ -98,62 +82,19 @@ func GetCPULoad() (float64, error) {
return 0, nil
}

// GetCPUTemperature finds the most relevant CPU temperature sensor.
func GetCPUTemperature() (float64, error) {
temps, err := sensors.SensorsTemperatures()
if err != nil {
return 0, err
}

var cpuTemp float64
var found bool

for _, t := range temps {
key := strings.ToLower(t.SensorKey)

if strings.Contains(key, "tctl") ||
strings.Contains(key, "package") ||
strings.Contains(key, "core") ||
strings.Contains(key, "cpu") {

if strings.Contains(key, "acpitz") {
continue
}

cpuTemp = math.Round(t.Temperature)
found = true
break
}
}

if !found {
return 0, fmt.Errorf("CPU temperature not found")
}

return cpuTemp, nil
}

// GetProcessesAndThreads returns the total number of processes and threads.
// A process that exits between the snapshot and the NumThreads call is skipped
// rather than aborting the whole collection.
func GetProcessesAndThreads() (int, int, error) {
procs, err := process.Processes()
if err != nil {
return 0, 0, err
}

numProcesses := len(procs)
totalThreads := 0

for _, p := range procs {
threads, err := p.NumThreads()
if err != nil {
// The process likely exited between Processes() and NumThreads().
// Skip it instead of failing the entire collection.
continue
}
totalThreads += int(threads)
}

return numProcesses, totalThreads, nil
}
45 changes: 45 additions & 0 deletions LinqoraHost/internal/metrics/cpu_temp_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//go:build !windows

package metrics

import (
"fmt"
"math"
"strings"

"github.com/shirou/gopsutil/v4/sensors"
)

// GetCPUTemperature finds the most relevant CPU temperature sensor via gopsutil.
func GetCPUTemperature() (float64, error) {
temps, err := sensors.SensorsTemperatures()
if err != nil {
return 0, err
}

var cpuTemp float64
var found bool

for _, t := range temps {
key := strings.ToLower(t.SensorKey)

if strings.Contains(key, "tctl") ||
strings.Contains(key, "package") ||
strings.Contains(key, "core") ||
strings.Contains(key, "cpu") {

if strings.Contains(key, "acpitz") {
continue
}

cpuTemp = math.Round(t.Temperature)
found = true
break
}
}

if !found {
return 0, fmt.Errorf("CPU temperature sensor not found")
}
return cpuTemp, nil
}
Loading
Loading