From de45fa938197fd1af5d66d71a9ce9b0bc9c206b8 Mon Sep 17 00:00:00 2001
From: Peder Toftegaard Olsen <peder@morgendagen.dk>
Date: Thu, 1 May 2025 16:31:30 +0200
Subject: [PATCH 1/2] Added UI for changing display orientation.

---
 config.go                                     |  2 +
 display.go                                    |  5 +++
 jsonrpc.go                                    | 24 ++++++++++++
 ui/src/hooks/stores.ts                        |  7 ++++
 .../routes/devices.$id.settings.hardware.tsx  | 37 +++++++++++++++++++
 5 files changed, 75 insertions(+)

diff --git a/config.go b/config.go
index 23d4c84..196a73d 100644
--- a/config.go
+++ b/config.go
@@ -89,6 +89,7 @@ type Config struct {
 	KeyboardMacros       []KeyboardMacro        `json:"keyboard_macros"`
 	EdidString           string                 `json:"hdmi_edid_string"`
 	ActiveExtension      string                 `json:"active_extension"`
+	DisplayRotation      string                 `json:"display_rotation"`
 	DisplayMaxBrightness int                    `json:"display_max_brightness"`
 	DisplayDimAfterSec   int                    `json:"display_dim_after_sec"`
 	DisplayOffAfterSec   int                    `json:"display_off_after_sec"`
@@ -107,6 +108,7 @@ var defaultConfig = &Config{
 	AutoUpdateEnabled:    true, // Set a default value
 	ActiveExtension:      "",
 	KeyboardMacros:       []KeyboardMacro{},
+	DisplayRotation:      "270",
 	DisplayMaxBrightness: 64,
 	DisplayDimAfterSec:   120,  // 2 minutes
 	DisplayOffAfterSec:   1800, // 30 minutes
diff --git a/display.go b/display.go
index e2e82e1..028ad46 100644
--- a/display.go
+++ b/display.go
@@ -73,6 +73,10 @@ func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) {
 	return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src})
 }
 
+func lvDispSetRotation(rotation string) (*CtrlResponse, error) {
+	return CallCtrlAction("lv_disp_set_rotation", map[string]interface{}{"rotation": rotation})
+}
+
 func updateLabelIfChanged(objName string, newText string) {
 	if newText != "" && newText != displayedTexts[objName] {
 		_, _ = lvLabelSetText(objName, newText)
@@ -373,6 +377,7 @@ func init() {
 		waitCtrlClientConnected()
 		displayLogger.Info().Msg("setting initial display contents")
 		time.Sleep(500 * time.Millisecond)
+		lvDispSetRotation(config.DisplayRotation)
 		updateStaticContents()
 		displayInited = true
 		displayLogger.Info().Msg("display inited")
diff --git a/jsonrpc.go b/jsonrpc.go
index d35f635..05db3d5 100644
--- a/jsonrpc.go
+++ b/jsonrpc.go
@@ -38,6 +38,10 @@ type JSONRPCEvent struct {
 	Params  interface{} `json:"params,omitempty"`
 }
 
+type DisplayRotationSettings struct {
+	Rotation string `json:"rotation"`
+}
+
 type BacklightSettings struct {
 	MaxBrightness int `json:"max_brightness"`
 	DimAfter      int `json:"dim_after"`
@@ -280,6 +284,24 @@ func rpcTryUpdate() error {
 	return nil
 }
 
+func rpcSetDisplayRotation(params DisplayRotationSettings) error {
+	var err error
+	_, err = lvDispSetRotation(params.Rotation)
+	if err == nil {
+		config.DisplayRotation = params.Rotation
+		if err := SaveConfig(); err != nil {
+			return fmt.Errorf("failed to save config: %w", err)
+		}
+	}
+	return err
+}
+
+func rpcGetDisplayRotation() (*DisplayRotationSettings, error) {
+	return &DisplayRotationSettings{
+		Rotation: config.DisplayRotation,
+	}, nil
+}
+
 func rpcSetBacklightSettings(params BacklightSettings) error {
 	blConfig := params
 
@@ -1012,6 +1034,8 @@ var rpcHandlers = map[string]RPCHandler{
 	"getWakeOnLanDevices":    {Func: rpcGetWakeOnLanDevices},
 	"setWakeOnLanDevices":    {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
 	"resetConfig":            {Func: rpcResetConfig},
+	"setDisplayRotation":     {Func: rpcSetDisplayRotation, Params: []string{"params"}},
+	"getDisplayRotation":     {Func: rpcGetDisplayRotation},
 	"setBacklightSettings":   {Func: rpcSetBacklightSettings, Params: []string{"params"}},
 	"getBacklightSettings":   {Func: rpcGetBacklightSettings},
 	"getDCPowerState":        {Func: rpcGetDCPowerState},
diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts
index db1fd04..c100d88 100644
--- a/ui/src/hooks/stores.ts
+++ b/ui/src/hooks/stores.ts
@@ -292,6 +292,9 @@ interface SettingsState {
   developerMode: boolean;
   setDeveloperMode: (enabled: boolean) => void;
 
+  displayRotation: string;
+  setDisplayRotation: (rotation: string) => void;
+
   backlightSettings: BacklightSettings;
   setBacklightSettings: (settings: BacklightSettings) => void;
 }
@@ -312,6 +315,10 @@ export const useSettingsStore = create(
       developerMode: false,
       setDeveloperMode: enabled => set({ developerMode: enabled }),
 
+      displayRotation: "270",
+      setDisplayRotation: (rotation: string) =>
+        set({ displayRotation: rotation }),
+
       backlightSettings: {
         max_brightness: 100,
         dim_after: 10000,
diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx
index 9fde3e3..82cc6a1 100644
--- a/ui/src/routes/devices.$id.settings.hardware.tsx
+++ b/ui/src/routes/devices.$id.settings.hardware.tsx
@@ -15,6 +15,25 @@ export default function SettingsHardwareRoute() {
   const [send] = useJsonRpc();
   const settings = useSettingsStore();
 
+  const setDisplayRotation = useSettingsStore(state => state.setDisplayRotation);
+
+  const handleDisplayRotationChange = (rotation: string) => {
+    setDisplayRotation(rotation);
+    handleDisplayRotationSave();
+  };
+
+  const handleDisplayRotationSave = () => {
+    send("setDisplayRotation", { params: { rotation: settings.displayRotation } }, resp => {
+      if ("error" in resp) {
+        notifications.error(
+          `Failed to set display orientation: ${resp.error.data || "Unknown error"}`,
+        );
+        return;
+      }
+      notifications.success("Display orientation updated successfully");
+    });
+  };
+
   const setBacklightSettings = useSettingsStore(state => state.setBacklightSettings);
 
   const handleBacklightSettingsChange = (settings: BacklightSettings) => {
@@ -59,6 +78,24 @@ export default function SettingsHardwareRoute() {
         description="Configure display settings and hardware options for your JetKVM device"
       />
       <div className="space-y-4">
+        <SettingsItem
+          title="Display Orientation"
+          description="Set the orientation of the display"
+        >
+          <SelectMenuBasic
+            size="SM"
+            label=""
+            value={settings.displayRotation.toString()}
+            options={[
+              { value: "270", label: "Normal" },
+              { value: "90", label: "Inverted" },
+            ]}
+            onChange={e => {
+              settings.displayRotation = e.target.value;
+              handleDisplayRotationChange(settings.displayRotation);
+            }}
+          />
+        </SettingsItem>
         <SettingsItem
           title="Display Brightness"
           description="Set the brightness of the display"

From 56aa42197d96e7fa0adb67ba5244bb82bf90f810 Mon Sep 17 00:00:00 2001
From: Peder Toftegaard Olsen <peder@morgendagen.dk>
Date: Fri, 2 May 2025 18:29:21 +0200
Subject: [PATCH 2/2] Fixed lint issue.

---
 display.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/display.go b/display.go
index 028ad46..f4d2a94 100644
--- a/display.go
+++ b/display.go
@@ -377,7 +377,7 @@ func init() {
 		waitCtrlClientConnected()
 		displayLogger.Info().Msg("setting initial display contents")
 		time.Sleep(500 * time.Millisecond)
-		lvDispSetRotation(config.DisplayRotation)
+		_, _ = lvDispSetRotation(config.DisplayRotation)
 		updateStaticContents()
 		displayInited = true
 		displayLogger.Info().Msg("display inited")