mirror of https://github.com/jetkvm/kvm.git
Compare commits
10 Commits
a9be36d253
...
2260613aee
Author | SHA1 | Date |
---|---|---|
|
2260613aee | |
|
f1953fddbc | |
|
9ba97ebe67 | |
|
5fb8d866ba | |
|
3359f8fca4 | |
|
ef95643a86 | |
|
1fc603b553 | |
|
aada3d95e0 | |
|
d704fcc6c7 | |
|
ab3dda6dee |
|
@ -23,6 +23,9 @@ linters:
|
|||
- linters:
|
||||
- errcheck
|
||||
path: _test.go
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: cmd/main.go
|
||||
- linters:
|
||||
- gochecknoinits
|
||||
path: internal/logging/sse.go
|
||||
|
|
18
cmd/main.go
18
cmd/main.go
|
@ -1,9 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jetkvm/kvm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
versionPtr := flag.Bool("version", false, "print version and exit")
|
||||
versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit")
|
||||
flag.Parse()
|
||||
|
||||
if *versionPtr || *versionJsonPtr {
|
||||
versionData, err := kvm.GetVersionData(*versionJsonPtr)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to get version data: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(string(versionData))
|
||||
return
|
||||
}
|
||||
|
||||
kvm.Main()
|
||||
}
|
||||
|
|
23
config.go
23
config.go
|
@ -9,6 +9,8 @@ import (
|
|||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/jetkvm/kvm/internal/network"
|
||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
type WakeOnLanDevice struct {
|
||||
|
@ -138,6 +140,21 @@ var (
|
|||
configLock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
var (
|
||||
configSuccess = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_config_last_reload_successful",
|
||||
Help: "The last configuration load succeeded",
|
||||
},
|
||||
)
|
||||
configSuccessTime = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_config_last_reload_success_timestamp_seconds",
|
||||
Help: "Timestamp of last successful config load",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
func LoadConfig() {
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
|
@ -153,6 +170,8 @@ func LoadConfig() {
|
|||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
logger.Debug().Msg("default config file doesn't exist, using default")
|
||||
configSuccess.Set(1.0)
|
||||
configSuccessTime.SetToCurrentTime()
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
@ -161,6 +180,7 @@ func LoadConfig() {
|
|||
loadedConfig := *defaultConfig
|
||||
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
|
||||
logger.Warn().Err(err).Msg("config file JSON parsing failed")
|
||||
configSuccess.Set(0.0)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -181,6 +201,9 @@ func LoadConfig() {
|
|||
|
||||
logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel)
|
||||
|
||||
configSuccess.Set(1.0)
|
||||
configSuccessTime.SetToCurrentTime()
|
||||
|
||||
logger.Info().Str("path", configPath).Msg("config loaded")
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
dcCurrentGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_current_amperes",
|
||||
Help: "Current DC power consumption in amperes",
|
||||
})
|
||||
|
||||
dcPowerGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_power_watts",
|
||||
Help: "DC power consumption in watts",
|
||||
})
|
||||
|
||||
dcVoltageGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_voltage_volts",
|
||||
Help: "DC voltage in volts",
|
||||
})
|
||||
|
||||
dcStateGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_power_state",
|
||||
Help: "DC power state (1 = on, 0 = off)",
|
||||
})
|
||||
|
||||
dcMetricsRegistered sync.Once
|
||||
)
|
||||
|
||||
// registerDCMetrics registers the DC power metrics with Prometheus (called once when DC control is mounted)
|
||||
func registerDCMetrics() {
|
||||
dcMetricsRegistered.Do(func() {
|
||||
prometheus.MustRegister(dcCurrentGauge)
|
||||
prometheus.MustRegister(dcPowerGauge)
|
||||
prometheus.MustRegister(dcVoltageGauge)
|
||||
prometheus.MustRegister(dcStateGauge)
|
||||
})
|
||||
}
|
||||
|
||||
// updateDCMetrics updates the Prometheus metrics with current DC power state values
|
||||
func updateDCMetrics(state DCPowerState) {
|
||||
dcCurrentGauge.Set(state.Current)
|
||||
dcPowerGauge.Set(state.Power)
|
||||
dcVoltageGauge.Set(state.Voltage)
|
||||
if state.IsOn {
|
||||
dcStateGauge.Set(1)
|
||||
} else {
|
||||
dcStateGauge.Set(0)
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ show_help() {
|
|||
echo " --run-go-tests Run go tests"
|
||||
echo " --run-go-tests-only Run go tests and exit"
|
||||
echo " --skip-ui-build Skip frontend/UI build"
|
||||
echo " -i, --install Build for release and install the app"
|
||||
echo " --help Display this help message"
|
||||
echo
|
||||
echo "Example:"
|
||||
|
@ -43,6 +44,7 @@ RESET_USB_HID_DEVICE=false
|
|||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
||||
RUN_GO_TESTS=false
|
||||
RUN_GO_TESTS_ONLY=false
|
||||
INSTALL_APP=false
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
|
@ -72,6 +74,10 @@ while [[ $# -gt 0 ]]; do
|
|||
RUN_GO_TESTS=true
|
||||
shift
|
||||
;;
|
||||
-i|--install)
|
||||
INSTALL_APP=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
|
@ -139,25 +145,36 @@ EOF
|
|||
fi
|
||||
fi
|
||||
|
||||
msg_info "▶ Building go binary"
|
||||
make build_dev
|
||||
|
||||
# Kill any existing instances of the application
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||
|
||||
# Copy the binary to the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
msg_info "▶ Resetting USB HID device"
|
||||
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
|
||||
# Remove the old USB gadget configuration
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
fi
|
||||
|
||||
# Deploy and run the application on the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
if [ "$INSTALL_APP" = true ]
|
||||
then
|
||||
msg_info "▶ Building release binary"
|
||||
make build_release
|
||||
|
||||
# Copy the binary to the remote host as if we were the OTA updater.
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
||||
|
||||
# Reboot the device, the new app will be deployed by the startup process.
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
|
||||
else
|
||||
msg_info "▶ Building development binary"
|
||||
make build_dev
|
||||
|
||||
# Kill any existing instances of the application
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||
|
||||
# Copy the binary to the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
msg_info "▶ Resetting USB HID device"
|
||||
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
|
||||
# Remove the old USB gadget configuration
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
fi
|
||||
|
||||
# Deploy and run the application on the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
set -e
|
||||
|
||||
# Set the library path to include the directory where librockit.so is located
|
||||
|
@ -174,7 +191,8 @@ cd "${REMOTE_PATH}"
|
|||
chmod +x jetkvm_app_debug
|
||||
|
||||
# Run the application in the background
|
||||
PION_LOG_TRACE=${LOG_TRACE_SCOPES} GODEBUG=netdns=1 ./jetkvm_app_debug
|
||||
PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug | tee -a /tmp/jetkvm_app_debug.log
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Deployment complete."
|
||||
echo "Deployment complete."
|
||||
|
|
|
@ -3,6 +3,7 @@ package confparser
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
@ -372,6 +373,10 @@ func (f *FieldConfig) validateField() error {
|
|||
if _, err := idna.Lookup.ToASCII(val); err != nil {
|
||||
return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
|
||||
}
|
||||
case "proxy":
|
||||
if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" {
|
||||
return fmt.Errorf("field `%s` is not a valid HTTP proxy URL: %s", f.Name, val)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package network
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/guregu/null/v6"
|
||||
|
@ -32,8 +34,9 @@ type IPv6StaticConfig struct {
|
|||
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
||||
}
|
||||
type NetworkConfig struct {
|
||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||
HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"`
|
||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||
|
||||
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
|
||||
IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
||||
|
@ -71,6 +74,18 @@ func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
|||
|
||||
return listenOptions
|
||||
}
|
||||
|
||||
func (s *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
|
||||
return func(*http.Request) (*url.URL, error) {
|
||||
if s.HTTPProxy.String == "" {
|
||||
return nil, nil
|
||||
} else {
|
||||
proxyUrl, _ := url.Parse(s.HTTPProxy.String)
|
||||
return proxyUrl, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) GetHostname() string {
|
||||
hostname := ToValidHostname(s.config.Hostname.String)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
@ -57,6 +58,7 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now
|
|||
ctx,
|
||||
url,
|
||||
timeout,
|
||||
t.networkConfig.GetTransportProxyFunc(),
|
||||
)
|
||||
duration := time.Since(startTime)
|
||||
|
||||
|
@ -122,10 +124,16 @@ func queryHttpTime(
|
|||
ctx context.Context,
|
||||
url string,
|
||||
timeout time.Duration,
|
||||
proxyFunc func(*http.Request) (*url.URL, error),
|
||||
) (now *time.Time, response *http.Response, err error) {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = proxyFunc
|
||||
|
||||
client := http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: transport,
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
@ -14,9 +14,10 @@ var keyboardConfig = gadgetConfigItem{
|
|||
path: []string{"functions", "hid.usb0"},
|
||||
configPath: []string{"hid.usb0"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "1",
|
||||
"subclass": "1",
|
||||
"report_length": "8",
|
||||
"protocol": "1",
|
||||
"subclass": "1",
|
||||
"report_length": "8",
|
||||
"no_out_endpoint": "0",
|
||||
},
|
||||
reportDesc: keyboardReportDesc,
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ var absoluteMouseConfig = gadgetConfigItem{
|
|||
path: []string{"functions", "hid.usb1"},
|
||||
configPath: []string{"hid.usb1"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "2",
|
||||
"subclass": "0",
|
||||
"report_length": "6",
|
||||
"protocol": "2",
|
||||
"subclass": "0",
|
||||
"report_length": "6",
|
||||
"no_out_endpoint": "1",
|
||||
},
|
||||
reportDesc: absoluteMouseCombinedReportDesc,
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ var relativeMouseConfig = gadgetConfigItem{
|
|||
path: []string{"functions", "hid.usb2"},
|
||||
configPath: []string{"hid.usb2"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "2",
|
||||
"subclass": "1",
|
||||
"report_length": "4",
|
||||
"protocol": "2",
|
||||
"subclass": "1",
|
||||
"report_length": "4",
|
||||
"no_out_endpoint": "1",
|
||||
},
|
||||
reportDesc: relativeMouseCombinedReportDesc,
|
||||
}
|
||||
|
|
19
native.go
19
native.go
|
@ -9,6 +9,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -366,6 +367,22 @@ func shouldOverwrite(destPath string, srcHash []byte) bool {
|
|||
return !bytes.Equal(srcHash, dstHash)
|
||||
}
|
||||
|
||||
func getNativeSha256() ([]byte, error) {
|
||||
version, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func GetNativeVersion() (string, error) {
|
||||
version, err := getNativeSha256()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(version)), nil
|
||||
}
|
||||
|
||||
func ensureBinaryUpdated(destPath string) error {
|
||||
srcFile, err := resource.ResourceFS.Open("jetkvm_native")
|
||||
if err != nil {
|
||||
|
@ -373,7 +390,7 @@ func ensureBinaryUpdated(destPath string) error {
|
|||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256")
|
||||
srcHash, err := getNativeSha256()
|
||||
if err != nil {
|
||||
nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, proceeding with update")
|
||||
srcHash = nil
|
||||
|
|
|
@ -24,7 +24,9 @@ func networkStateChanged() {
|
|||
timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString())
|
||||
}
|
||||
|
||||
timeSync.Sync()
|
||||
if err := timeSync.Sync(); err != nil {
|
||||
networkLogger.Error().Err(err).Msg("failed to sync time after network state change")
|
||||
}
|
||||
}
|
||||
|
||||
// always restart mDNS when the network state changes
|
||||
|
|
14
ota.go
14
ota.go
|
@ -50,6 +50,10 @@ const UpdateMetadataUrl = "https://api.jetkvm.com/releases"
|
|||
|
||||
var builtAppVersion = "0.1.0+dev"
|
||||
|
||||
func GetBuiltAppVersion() string {
|
||||
return builtAppVersion
|
||||
}
|
||||
|
||||
func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Version, err error) {
|
||||
appVersion, err = semver.NewVersion(builtAppVersion)
|
||||
if err != nil {
|
||||
|
@ -89,7 +93,14 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease
|
|||
return nil, fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = config.NetworkConfig.GetTransportProxyFunc()
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error sending request: %w", err)
|
||||
}
|
||||
|
@ -135,6 +146,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
|
|||
client := http.Client{
|
||||
Timeout: 10 * time.Minute,
|
||||
Transport: &http.Transport{
|
||||
Proxy: config.NetworkConfig.GetTransportProxyFunc(),
|
||||
TLSHandshakeTimeout: 30 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: rootcerts.ServerCertPool(),
|
||||
|
|
|
@ -128,6 +128,7 @@ func pressATXResetButton(duration time.Duration) error {
|
|||
|
||||
func mountDCControl() error {
|
||||
_ = port.SetMode(defaultMode)
|
||||
registerDCMetrics()
|
||||
go runDCControl()
|
||||
return nil
|
||||
}
|
||||
|
@ -206,6 +207,9 @@ func runDCControl() {
|
|||
dcState.Current = amps
|
||||
dcState.Power = watts
|
||||
|
||||
// Update Prometheus metrics
|
||||
updateDCMetrics(dcState)
|
||||
|
||||
if currentSession != nil {
|
||||
writeJSONRPCEvent("dcState", dcState, currentSession)
|
||||
}
|
||||
|
|
|
@ -67,19 +67,19 @@ function Terminal({
|
|||
}) {
|
||||
const enableTerminal = useUiStore(state => state.terminalType == type);
|
||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||
const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
|
||||
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setDisableKeyboardFocusTrap(enableTerminal);
|
||||
setDisableVideoFocusTrap(enableTerminal);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
setDisableKeyboardFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
};
|
||||
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]);
|
||||
}, [enableTerminal, setDisableVideoFocusTrap]);
|
||||
|
||||
const readyState = dataChannel.readyState;
|
||||
useEffect(() => {
|
||||
|
@ -116,7 +116,7 @@ function Terminal({
|
|||
const { domEvent } = e;
|
||||
if (domEvent.key === "Escape") {
|
||||
setTerminalType("none");
|
||||
setDisableKeyboardFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
domEvent.preventDefault();
|
||||
}
|
||||
});
|
||||
|
@ -131,7 +131,7 @@ function Terminal({
|
|||
onDataHandler.dispose();
|
||||
onKeyHandler.dispose();
|
||||
};
|
||||
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
|
||||
}, [dataChannel, instance, readyState, setDisableVideoFocusTrap, setTerminalType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instance) return;
|
||||
|
@ -158,7 +158,7 @@ function Terminal({
|
|||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [ref, instance]);
|
||||
}, [instance]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -10,11 +10,11 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import { layouts, chars } from "@/keyboardLayouts";
|
||||
import { KeyStroke, KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
const hidKeyboardPayload = (keys: number[], modifier: number) => {
|
||||
return { keys, modifier };
|
||||
const hidKeyboardPayload = (modifier: number, keys: number[]) => {
|
||||
return { modifier, keys };
|
||||
};
|
||||
|
||||
const modifierCode = (shift?: boolean, altRight?: boolean) => {
|
||||
|
@ -62,49 +62,56 @@ export default function PasteModal() {
|
|||
const onConfirmPaste = useCallback(async () => {
|
||||
setPasteMode(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
|
||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||
if (!safeKeyboardLayout) return;
|
||||
if (!chars[safeKeyboardLayout]) return;
|
||||
const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout);
|
||||
if (!keyboard) return;
|
||||
|
||||
const text = TextAreaRef.current.value;
|
||||
|
||||
try {
|
||||
for (const char of text) {
|
||||
const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char]
|
||||
const keyprops = keyboard.chars[char];
|
||||
if (!keyprops) continue;
|
||||
|
||||
const { key, shift, altRight, deadKey, accentKey } = keyprops;
|
||||
if (!key) continue;
|
||||
|
||||
const keyz = [ keys[key] ];
|
||||
const modz = [ modifierCode(shift, altRight) ];
|
||||
|
||||
if (deadKey) {
|
||||
keyz.push(keys["Space"]);
|
||||
modz.push(noModifier);
|
||||
}
|
||||
// if this is an accented character, we need to send that accent FIRST
|
||||
if (accentKey) {
|
||||
keyz.unshift(keys[accentKey.key])
|
||||
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
|
||||
await sendKeystroke({modifier: modifierCode(accentKey.shift, accentKey.altRight), keys: [ keys[accentKey.key] ] })
|
||||
}
|
||||
|
||||
for (const [index, kei] of keyz.entries()) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload([kei], modz[index]),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
send("keyboardReport", hidKeyboardPayload([], 0), params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
// now send the actual key
|
||||
await sendKeystroke({ modifier: modifierCode(shift, altRight), keys: [ keys[key] ]});
|
||||
|
||||
// if what was requested was a dead key, we need to send an unmodified space to emit
|
||||
// just the accent character
|
||||
if (deadKey) {
|
||||
await sendKeystroke({ modifier: noModifier, keys: [ keys["Space"] ] });
|
||||
}
|
||||
|
||||
// now send a message with no keys down to "release" the keys
|
||||
await sendKeystroke({ modifier: 0, keys: [] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error("Failed to paste text:", error);
|
||||
notifications.error("Failed to paste text");
|
||||
}
|
||||
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]);
|
||||
|
||||
async function sendKeystroke(stroke: KeyStroke) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload(stroke.modifier, stroke.keys),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (TextAreaRef.current) {
|
||||
|
@ -154,7 +161,7 @@ export default function PasteModal() {
|
|||
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
||||
[...new Intl.Segmenter().segment(value)]
|
||||
.map(x => x.segment)
|
||||
.filter(char => !chars[safeKeyboardLayout][char]),
|
||||
.filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]),
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -175,7 +182,7 @@ export default function PasteModal() {
|
|||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Sending text using keyboard layout: {layouts[safeKeyboardLayout]}
|
||||
Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ import AddDeviceForm from "./AddDeviceForm";
|
|||
export default function WakeOnLanModal() {
|
||||
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
|
||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
||||
|
||||
|
@ -24,9 +24,9 @@ export default function WakeOnLanModal() {
|
|||
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const onCancelWakeOnLanModal = useCallback(() => {
|
||||
setDisableVideoFocusTrap(false);
|
||||
close();
|
||||
setDisableFocusTrap(false);
|
||||
}, [close, setDisableFocusTrap]);
|
||||
}, [close, setDisableVideoFocusTrap]);
|
||||
|
||||
const onSendMagicPacket = useCallback(
|
||||
(macAddress: string) => {
|
||||
|
@ -43,12 +43,12 @@ export default function WakeOnLanModal() {
|
|||
}
|
||||
} else {
|
||||
notifications.success("Magic Packet sent successfully");
|
||||
setDisableFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
close();
|
||||
}
|
||||
});
|
||||
},
|
||||
[close, rpcDataChannel?.readyState, send, setDisableFocusTrap],
|
||||
[close, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap],
|
||||
);
|
||||
|
||||
const syncStoredDevices = useCallback(() => {
|
||||
|
@ -78,7 +78,7 @@ export default function WakeOnLanModal() {
|
|||
}
|
||||
});
|
||||
},
|
||||
[storedDevices, send, syncStoredDevices],
|
||||
[send, storedDevices, syncStoredDevices],
|
||||
);
|
||||
|
||||
const onAddDevice = useCallback(
|
||||
|
|
|
@ -747,6 +747,7 @@ export type TimeSyncMode =
|
|||
export interface NetworkSettings {
|
||||
hostname: string;
|
||||
domain: string;
|
||||
http_proxy: string;
|
||||
ipv4_mode: IPv4Mode;
|
||||
ipv6_mode: IPv6Mode;
|
||||
lldp_mode: LLDPMode;
|
||||
|
@ -935,5 +936,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -1,45 +1,32 @@
|
|||
import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE"
|
||||
import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ"
|
||||
import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK"
|
||||
import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US"
|
||||
import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR"
|
||||
import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE"
|
||||
import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT"
|
||||
import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO"
|
||||
import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES"
|
||||
import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||
import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH"
|
||||
import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH"
|
||||
export interface KeyStroke { modifier: number; keys: number[]; }
|
||||
export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
|
||||
export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo }
|
||||
export interface KeyboardLayout { isoCode: string, name: string, chars: Record<string, KeyCombo> }
|
||||
|
||||
interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
|
||||
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
|
||||
// to add a new layout, create a file like the above and add it to the list
|
||||
import { cs_CZ } from "@/keyboardLayouts/cs_CZ"
|
||||
import { de_CH } from "@/keyboardLayouts/de_CH"
|
||||
import { de_DE } from "@/keyboardLayouts/de_DE"
|
||||
import { en_US } from "@/keyboardLayouts/en_US"
|
||||
import { en_UK } from "@/keyboardLayouts/en_UK"
|
||||
import { es_ES } from "@/keyboardLayouts/es_ES"
|
||||
import { fr_BE } from "@/keyboardLayouts/fr_BE"
|
||||
import { fr_CH } from "@/keyboardLayouts/fr_CH"
|
||||
import { fr_FR } from "@/keyboardLayouts/fr_FR"
|
||||
import { it_IT } from "@/keyboardLayouts/it_IT"
|
||||
import { nb_NO } from "@/keyboardLayouts/nb_NO"
|
||||
import { sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||
|
||||
export const layouts: Record<string, string> = {
|
||||
be_FR: name_fr_BE,
|
||||
cs_CZ: name_cs_CZ,
|
||||
en_UK: name_en_UK,
|
||||
en_US: name_en_US,
|
||||
fr_FR: name_fr_FR,
|
||||
de_DE: name_de_DE,
|
||||
it_IT: name_it_IT,
|
||||
nb_NO: name_nb_NO,
|
||||
es_ES: name_es_ES,
|
||||
sv_SE: name_sv_SE,
|
||||
fr_CH: name_fr_CH,
|
||||
de_CH: name_de_CH,
|
||||
}
|
||||
export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ];
|
||||
|
||||
export const chars: Record<string, Record<string, KeyCombo>> = {
|
||||
be_FR: chars_fr_BE,
|
||||
cs_CZ: chars_cs_CZ,
|
||||
en_UK: chars_en_UK,
|
||||
en_US: chars_en_US,
|
||||
fr_FR: chars_fr_FR,
|
||||
de_DE: chars_de_DE,
|
||||
it_IT: chars_it_IT,
|
||||
nb_NO: chars_nb_NO,
|
||||
es_ES: chars_es_ES,
|
||||
sv_SE: chars_sv_SE,
|
||||
fr_CH: chars_fr_CH,
|
||||
de_CH: chars_de_CH,
|
||||
export const selectedKeyboard = (isoCode: string): KeyboardLayout => {
|
||||
// fallback to original behaviour of en-US if no isoCode given
|
||||
return keyboards.find(keyboard => keyboard.isoCode == isoCode)
|
||||
?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!;
|
||||
};
|
||||
|
||||
export const keyboardOptions = () => {
|
||||
return keyboards.map((keyboard) => {
|
||||
return { label: keyboard.name, value: keyboard.isoCode }
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Čeština";
|
||||
const name = "Čeština";
|
||||
|
||||
const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -13,7 +13,7 @@ const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (do
|
|||
const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
|
||||
const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -242,3 +242,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const cs_CZ: KeyboardLayout = {
|
||||
isoCode: "cs-CZ",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Schwiizerdütsch";
|
||||
const name = "Schwiizerdütsch";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ place
|
|||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -163,3 +163,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const de_CH: KeyboardLayout = {
|
||||
isoCode: "de-CH",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,12 +1,12 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Deutsch";
|
||||
const name = "Deutsch";
|
||||
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
|
@ -150,3 +150,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const de_DE: KeyboardLayout = {
|
||||
isoCode: "de-DE",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (UK)";
|
||||
const name = "English (UK)";
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
|
@ -105,3 +105,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>
|
||||
|
||||
export const en_UK: KeyboardLayout = {
|
||||
isoCode: "en-UK",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (US)";
|
||||
const name = "English (US)";
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
|
@ -111,3 +111,9 @@ export const chars = {
|
|||
Insert: { key: "Insert", shift: false },
|
||||
Delete: { key: "Delete", shift: false },
|
||||
} as Record<string, KeyCombo>
|
||||
|
||||
export const en_US: KeyboardLayout = {
|
||||
isoCode: "en-US",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Español";
|
||||
const name = "Español";
|
||||
|
||||
const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
|
|||
const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -166,3 +166,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const es_ES: KeyboardLayout = {
|
||||
isoCode: "es-ES",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Belgisch Nederlands";
|
||||
const name = "Belgisch Nederlands";
|
||||
|
||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute acce
|
|||
const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyQ", shift: true },
|
||||
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
|
||||
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
|
||||
|
@ -165,3 +165,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const fr_BE: KeyboardLayout = {
|
||||
isoCode: "fr-BE",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,11 +1,11 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
import { chars as chars_de_CH } from "./de_CH"
|
||||
import { de_CH } from "./de_CH"
|
||||
|
||||
export const name = "Français de Suisse";
|
||||
const name = "Français de Suisse";
|
||||
|
||||
export const chars = {
|
||||
...chars_de_CH,
|
||||
const chars = {
|
||||
...de_CH.chars,
|
||||
"è": { key: "BracketLeft" },
|
||||
"ü": { key: "BracketLeft", shift: true },
|
||||
"é": { key: "Semicolon" },
|
||||
|
@ -13,3 +13,9 @@ export const chars = {
|
|||
"à": { key: "Quote" },
|
||||
"ä": { key: "Quote", shift: true },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const fr_CH: KeyboardLayout = {
|
||||
isoCode: "fr-CH",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Français";
|
||||
const name = "Français";
|
||||
|
||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyQ", shift: true },
|
||||
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
|
||||
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
|
||||
|
@ -137,3 +137,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const fr_FR: KeyboardLayout = {
|
||||
isoCode: "fr-FR",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Italiano";
|
||||
const name = "Italiano";
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
|
@ -111,3 +111,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const it_IT: KeyboardLayout = {
|
||||
isoCode: "it-IT",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Norsk bokmål";
|
||||
const name = "Norsk bokmål";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
|
|||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -165,3 +165,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const nb_NO: KeyboardLayout = {
|
||||
isoCode: "nb-NO",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Svenska";
|
||||
const name = "Svenska";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
|
|||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
|
@ -162,3 +162,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const sv_SE: KeyboardLayout = {
|
||||
isoCode: "sv-SE",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,17 +1,19 @@
|
|||
// Key codes and modifiers correspond to definitions in the
|
||||
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
|
||||
// [Section 10. Keyboard/Keypad Page 0x07](https://usb.org/sites/default/files/hut1_21.pdf)
|
||||
export const keys = {
|
||||
ArrowDown: 0x51,
|
||||
ArrowLeft: 0x50,
|
||||
ArrowRight: 0x4f,
|
||||
ArrowUp: 0x52,
|
||||
Backquote: 0x35,
|
||||
Backquote: 0x35, // aka Grave
|
||||
Backslash: 0x31,
|
||||
Backspace: 0x2a,
|
||||
BracketLeft: 0x2f,
|
||||
BracketRight: 0x30,
|
||||
BracketLeft: 0x2f, // aka LeftBrace
|
||||
BracketRight: 0x30, // aka RightBrace
|
||||
CapsLock: 0x39,
|
||||
Comma: 0x36,
|
||||
Compose: 0x65,
|
||||
ContextMenu: 0,
|
||||
Delete: 0x4c,
|
||||
Digit0: 0x27,
|
||||
|
@ -40,10 +42,21 @@ export const keys = {
|
|||
F10: 0x43,
|
||||
F11: 0x44,
|
||||
F12: 0x45,
|
||||
F13: 0x68,
|
||||
F14: 0x69,
|
||||
F15: 0x6a,
|
||||
F16: 0x6b,
|
||||
F17: 0x6c,
|
||||
F18: 0x6d,
|
||||
F19: 0x6e,
|
||||
F20: 0x6f,
|
||||
F21: 0x70,
|
||||
F22: 0x71,
|
||||
F23: 0x72,
|
||||
F24: 0x73,
|
||||
Home: 0x4a,
|
||||
HashTilde: 0x32, // non-US # and ~
|
||||
Insert: 0x49,
|
||||
IntlBackslash: 0x64,
|
||||
IntlBackslash: 0x64, // non-US \ and |
|
||||
KeyA: 0x04,
|
||||
KeyB: 0x05,
|
||||
KeyC: 0x06,
|
||||
|
@ -72,30 +85,35 @@ export const keys = {
|
|||
KeyZ: 0x1d,
|
||||
KeypadExclamation: 0xcf,
|
||||
Minus: 0x2d,
|
||||
NumLock: 0x53,
|
||||
Numpad0: 0x62,
|
||||
Numpad1: 0x59,
|
||||
Numpad2: 0x5a,
|
||||
Numpad3: 0x5b,
|
||||
Numpad4: 0x5c,
|
||||
None: 0x00,
|
||||
NumLock: 0x53, // and Clear
|
||||
Numpad0: 0x62, // and Insert
|
||||
Numpad1: 0x59, // and End
|
||||
Numpad2: 0x5a, // and Down Arrow
|
||||
Numpad3: 0x5b, // and Page Down
|
||||
Numpad4: 0x5c, // and Left Arrow
|
||||
Numpad5: 0x5d,
|
||||
Numpad6: 0x5e,
|
||||
Numpad7: 0x5f,
|
||||
Numpad8: 0x60,
|
||||
Numpad9: 0x61,
|
||||
Numpad6: 0x5e, // and Right Arrow
|
||||
Numpad7: 0x5f, // and Home
|
||||
Numpad8: 0x60, // and Up Arrow
|
||||
Numpad9: 0x61, // and Page Up
|
||||
NumpadAdd: 0x57,
|
||||
NumpadComma: 0x85,
|
||||
NumpadDecimal: 0x63,
|
||||
NumpadDivide: 0x54,
|
||||
NumpadEnter: 0x58,
|
||||
NumpadEqual: 0x67,
|
||||
NumpadLeftParen: 0xb6,
|
||||
NumpadMultiply: 0x55,
|
||||
NumpadRightParen: 0xb7,
|
||||
NumpadSubtract: 0x56,
|
||||
NumpadDecimal: 0x63,
|
||||
PageDown: 0x4e,
|
||||
PageUp: 0x4b,
|
||||
Period: 0x37,
|
||||
PrintScreen: 0x46,
|
||||
Pause: 0x48,
|
||||
Quote: 0x34,
|
||||
Power: 0x66,
|
||||
Quote: 0x34, // aka Single Quote or Apostrophe
|
||||
ScrollLock: 0x47,
|
||||
Semicolon: 0x33,
|
||||
Slash: 0x38,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { layouts } from "@/keyboardLayouts";
|
||||
import { keyboardOptions } from "@/keyboardLayouts";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
@ -32,7 +32,7 @@ export default function SettingsKeyboardRoute() {
|
|||
return "en_US";
|
||||
}, [keyboardLayout]);
|
||||
|
||||
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
||||
const layoutOptions = keyboardOptions();
|
||||
const ledSyncOptions = [
|
||||
{ value: "auto", label: "Automatic" },
|
||||
{ value: "browser", label: "Browser Only" },
|
||||
|
@ -46,7 +46,7 @@ export default function SettingsKeyboardRoute() {
|
|||
if ("error" in resp) return;
|
||||
setKeyboardLayout(resp.result as string);
|
||||
});
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [send, setKeyboardLayout]);
|
||||
|
||||
const onKeyboardLayoutChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
|
|
|
@ -34,6 +34,7 @@ dayjs.extend(relativeTime);
|
|||
|
||||
const defaultNetworkSettings: NetworkSettings = {
|
||||
hostname: "",
|
||||
http_proxy: "",
|
||||
domain: "",
|
||||
ipv4_mode: "unknown",
|
||||
ipv6_mode: "unknown",
|
||||
|
@ -185,6 +186,10 @@ export default function SettingsNetworkRoute() {
|
|||
setNetworkSettings({ ...networkSettings, hostname: value });
|
||||
};
|
||||
|
||||
const handleProxyChange = (value: string) => {
|
||||
setNetworkSettings({ ...networkSettings, http_proxy: value });
|
||||
};
|
||||
|
||||
const handleDomainChange = (value: string) => {
|
||||
setNetworkSettings({ ...networkSettings, domain: value });
|
||||
};
|
||||
|
@ -253,6 +258,26 @@ export default function SettingsNetworkRoute() {
|
|||
</div>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="HTTP Proxy"
|
||||
description="Proxy server for outgoing HTTP(S) requests from the device. Blank for none."
|
||||
>
|
||||
<div className="relative">
|
||||
<div>
|
||||
<InputField
|
||||
size="SM"
|
||||
type="text"
|
||||
placeholder="http://proxy.example.com:8080/"
|
||||
defaultValue={networkSettings.http_proxy}
|
||||
onChange={e => {
|
||||
handleProxyChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
|
|
|
@ -79,7 +79,7 @@ export default function SettingsRoute() {
|
|||
return () => {
|
||||
setDisableVideoFocusTrap(false);
|
||||
};
|
||||
}, [setDisableVideoFocusTrap, sendKeyboardEvent]);
|
||||
}, [sendKeyboardEvent, setDisableVideoFocusTrap]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white">
|
||||
|
@ -151,7 +151,6 @@ export default function SettingsRoute() {
|
|||
className={({ isActive }) => (isActive ? "active" : "")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
|
||||
<LuMouse className="h-4 w-4 shrink-0" />
|
||||
<h1>Mouse</h1>
|
||||
</div>
|
||||
|
@ -163,7 +162,7 @@ export default function SettingsRoute() {
|
|||
to="keyboard"
|
||||
className={({ isActive }) => (isActive ? "active" : "")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuKeyboard className="h-4 w-4 shrink-0" />
|
||||
<h1>Keyboard</h1>
|
||||
</div>
|
||||
|
|
|
@ -707,7 +707,7 @@ export default function KvmIdRoute() {
|
|||
}, [diskChannel, file]);
|
||||
|
||||
// System update
|
||||
const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||
|
||||
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||
|
@ -805,7 +805,7 @@ export default function KvmIdRoute() {
|
|||
)}
|
||||
<div className="relative h-full">
|
||||
<FocusTrap
|
||||
paused={disableKeyboardFocusTrap}
|
||||
paused={disableVideoFocusTrap}
|
||||
focusTrapOptions={{
|
||||
allowOutsideClick: true,
|
||||
escapeDeactivates: false,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"runtime"
|
||||
|
||||
"github.com/prometheus/common/version"
|
||||
)
|
||||
|
||||
var versionInfoTmpl = `
|
||||
JetKVM Application, version {{.version}} (branch: {{.branch}}, revision: {{.revision}})
|
||||
build date: {{.buildDate}}
|
||||
go version: {{.goVersion}}
|
||||
platform: {{.platform}}
|
||||
|
||||
{{if .nativeVersion}}
|
||||
JetKVM Native, version {{.nativeVersion}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
func GetVersionData(isJson bool) ([]byte, error) {
|
||||
version.Version = GetBuiltAppVersion()
|
||||
|
||||
m := map[string]string{
|
||||
"version": version.Version,
|
||||
"revision": version.GetRevision(),
|
||||
"branch": version.Branch,
|
||||
"buildDate": version.BuildDate,
|
||||
"goVersion": version.GoVersion,
|
||||
"platform": runtime.GOOS + "/" + runtime.GOARCH,
|
||||
}
|
||||
|
||||
nativeVersion, err := GetNativeVersion()
|
||||
if err == nil {
|
||||
m["nativeVersion"] = nativeVersion
|
||||
}
|
||||
|
||||
if isJson {
|
||||
jsonData, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
t := template.Must(template.New("version").Parse(versionInfoTmpl))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := t.ExecuteTemplate(&buf, "version", m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
22
wol.go
22
wol.go
|
@ -4,6 +4,24 @@ import (
|
|||
"bytes"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
wolPackets = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_wol_sent_packets_total",
|
||||
Help: "Total number of Wake-on-LAN magic packets sent.",
|
||||
},
|
||||
)
|
||||
wolErrors = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_wol_sent_packet_errors_total",
|
||||
Help: "Total number of Wake-on-LAN magic packets errors.",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
// SendWOLMagicPacket sends a Wake-on-LAN magic packet to the specified MAC address
|
||||
|
@ -11,6 +29,7 @@ func rpcSendWOLMagicPacket(macAddress string) error {
|
|||
// Parse the MAC address
|
||||
mac, err := net.ParseMAC(macAddress)
|
||||
if err != nil {
|
||||
wolErrors.Inc()
|
||||
return ErrorfL(wolLogger, "invalid MAC address", err)
|
||||
}
|
||||
|
||||
|
@ -20,6 +39,7 @@ func rpcSendWOLMagicPacket(macAddress string) error {
|
|||
// Set up UDP connection
|
||||
conn, err := net.Dial("udp", "255.255.255.255:9")
|
||||
if err != nil {
|
||||
wolErrors.Inc()
|
||||
return ErrorfL(wolLogger, "failed to establish UDP connection", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
@ -27,10 +47,12 @@ func rpcSendWOLMagicPacket(macAddress string) error {
|
|||
// Send the packet
|
||||
_, err = conn.Write(packet)
|
||||
if err != nil {
|
||||
wolErrors.Inc()
|
||||
return ErrorfL(wolLogger, "failed to send WOL packet", err)
|
||||
}
|
||||
|
||||
wolLogger.Info().Str("mac", macAddress).Msg("WOL packet sent")
|
||||
wolPackets.Inc()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue