Skip to content

Commit ab4acf0

Browse files
authored
feat(tui): support horizontal flips (screen mirror) for rotations (#77)
## What does this PR do? Adds `L` keybind to flip the screen on top of rotations. [Source](https://wiki.hypr.land/Configuring/Monitors/?utm_source=chatgpt.com#rotating): ``` 0 -> normal (no transforms) 1 -> 90 degrees 2 -> 180 degrees 3 -> 270 degrees 4 -> flipped 5 -> flipped + 90 degrees 6 -> flipped + 180 degrees 7 -> flipped + 270 degrees ``` ## Why is this change important? Some users might need to flip their screens when using projectors etc. ## How to test this PR locally? `make pre-push` ## Related issues Closes #72
2 parents ecd8457 + 36d44b3 commit ab4acf0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+551
-130
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ record/preview/daemon: build/docs
258258

259259
demo: record/preview
260260

261+
record/all: demo record/previews record/preview/daemon
262+
261263
record/previews: build/docs
262264
$(MAKE) record/preview RECORD_TARGET=views
263265
$(MAKE) record/preview RECORD_TARGET=monitor_view

docs/docs/quickstart/tui.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Once you select a monitor with `Enter`, it enters **EDITING** mode. In this mode
106106
| Key | Action |
107107
|-----|--------|
108108
| `r` | Rotate the monitor by 90 degrees (cycles through 0 → 90 → 180 → 270 → 0) |
109+
| `L` | Flip (mirror) monitor horizontally |
109110

110111
:::note
111112
Cannot rotate disabled monitors

internal/hypr/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ func (m *MonitorSpec) Validate() error {
8080
if m.SdrSaturation == 0.0 {
8181
m.SdrSaturation = 1.0
8282
}
83+
if m.Transform < 0 || m.Transform > 7 {
84+
return fmt.Errorf(".transform is invalid for monitor id %d: %d < 0 || %d > 7", m.ID, m.Transform, m.Transform)
85+
}
8386

8487
return nil
8588
}

internal/tui/main_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
package tui
1+
package tui_test
22

33
import (
4+
"flag"
45
"os"
56
"testing"
67

78
"github.com/charmbracelet/lipgloss"
89
"github.com/muesli/termenv"
910
)
1011

12+
var regenerate = flag.Bool("regenerate", false, "regenerate fixtures instead of comparing")
13+
1114
func TestMain(m *testing.M) {
1215
lipgloss.SetColorProfile(termenv.Ascii)
1316
os.Exit(m.Run())

internal/tui/messages.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ const (
116116
OperationNameAdjustSdrSaturation
117117
OperationNameHydrate
118118
OperationNameReloadHyprDestination
119+
OperationNameFlipMonitor
119120
)
120121

121122
type OperationStatus struct {
@@ -171,6 +172,8 @@ func (o OperationStatus) String() string {
171172
operationName = "Render Hypr Config"
172173
case OperationNameReloadHyprDestination:
173174
operationName = "Reload Hypr Destination"
175+
case OperationNameFlipMonitor:
176+
operationName = "Flip Monitor"
174177
default:
175178
operationName = "Operation"
176179
}
@@ -202,6 +205,7 @@ func OperationStatusCmd(name OperationName, err error) tea.Cmd {
202205
OperationNameMatchingProfile,
203206
OperationNameHydrate,
204207
OperationNameReloadHyprDestination,
208+
OperationNameFlipMonitor,
205209
}
206210
showSuccessToUser := slices.Contains(criticalOperations, name)
207211
return func() tea.Msg {
@@ -260,6 +264,10 @@ type ToggleMonitorCommand struct {
260264
MonitorID int
261265
}
262266

267+
type FlipMonitorCommand struct {
268+
MonitorID int
269+
}
270+
263271
type ChangeColorPresetCommand struct {
264272
Preset ColorPreset
265273
}
@@ -501,6 +509,14 @@ func previewScaleMonitorCmd(monitorID int, scale float64) tea.Cmd {
501509
}
502510
}
503511

512+
func flipMonitorCmd(monitor *MonitorSpec) tea.Cmd {
513+
return func() tea.Msg {
514+
return FlipMonitorCommand{
515+
MonitorID: *monitor.ID,
516+
}
517+
}
518+
}
519+
504520
func toggleMonitorCmd(monitor *MonitorSpec) tea.Cmd {
505521
return func() tea.Msg {
506522
return ToggleMonitorCommand{

internal/tui/monitor_editor.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,21 @@ func (e *MonitorEditorStore) RotateMonitor(monitorID int) tea.Cmd {
137137
return OperationStatusCmd(OperationNameRotate, nil)
138138
}
139139

140+
func (e *MonitorEditorStore) FlipMonitor(monitorID int) tea.Cmd {
141+
monitor, _, err := e.FindByID(monitorID)
142+
if err != nil {
143+
return OperationStatusCmd(OperationNameFlipMonitor, err)
144+
}
145+
146+
if monitor.Disabled {
147+
return OperationStatusCmd(OperationNameFlipMonitor, ErrMonitorDisabled)
148+
}
149+
150+
monitor.ToggleFlip()
151+
152+
return OperationStatusCmd(OperationNameFlipMonitor, nil)
153+
}
154+
140155
func (e *MonitorEditorStore) ToggleVRR(monitorID int) tea.Cmd {
141156
monitor, _, err := e.FindByID(monitorID)
142157
if err != nil {

internal/tui/monitor_list.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func (m MonitorItem) DescriptionLines() []string {
105105
m.monitor.ModePretty(),
106106
m.monitor.ScalePretty(),
107107
m.monitor.VRRPretty(),
108-
m.monitor.RotationPretty(),
108+
m.monitor.RotationPretty(true),
109109
m.monitor.PositionPretty(),
110110
m.monitor.MirrorPretty(),
111111
}
@@ -125,6 +125,7 @@ type MonitorListKeyMap struct {
125125
rotate key.Binding
126126
scale key.Binding
127127
color key.Binding
128+
flip key.Binding
128129
changeMode key.Binding
129130
vrr key.Binding
130131
toggle key.Binding
@@ -169,6 +170,10 @@ func NewMonitorListKeyMap() *MonitorListKeyMap {
169170
key.WithKeys("C"),
170171
key.WithHelp("C", "color"),
171172
),
173+
flip: key.NewBinding(
174+
key.WithKeys("L"),
175+
key.WithHelp("L", "flip"),
176+
),
172177
}
173178
}
174179

@@ -183,6 +188,7 @@ func (m MonitorListKeyMap) ShortHelp(state AppState) []key.Binding {
183188
m.toggle,
184189
m.mirror,
185190
m.color,
191+
m.flip,
186192
}
187193
}
188194
return []key.Binding{
@@ -292,6 +298,12 @@ func (d MonitorDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
292298
return nil
293299
}
294300
cmds = append(cmds, toggleMonitorCmd(item.monitor))
301+
case key.Matches(msg, d.keymap.flip):
302+
logrus.Debugf("List called with flip")
303+
if !item.Editing() {
304+
return nil
305+
}
306+
cmds = append(cmds, flipMonitorCmd(item.monitor))
295307
case key.Matches(msg, d.keymap.scale):
296308
logrus.Debugf("List called with scale")
297309
if !item.Editing() {

internal/tui/monitors_preview_pane.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/charmbracelet/bubbles/key"
88
tea "github.com/charmbracelet/bubbletea"
99
"github.com/charmbracelet/lipgloss"
10+
"github.com/fiffeek/hyprdynamicmonitors/internal/utils"
1011
"github.com/sirupsen/logrus"
1112
)
1213

@@ -438,6 +439,9 @@ func (*MonitorsPreviewPane) drawMonitorLabel(isSelected bool, monitor *MonitorSp
438439
if len(label) > 6 {
439440
label = label[:6] // Truncate long names
440441
}
442+
if monitor.Flipped {
443+
label = utils.Reverse(label)
444+
}
441445
if isSelected {
442446
label = "*" + label
443447
}
@@ -545,7 +549,7 @@ func (p *MonitorsPreviewPane) renderLegend() string {
545549
coloredPattern := GetMonitorColorStyle(i).Render("██")
546550
item := fmt.Sprintf("%s %s - %s, %s, %s, %s",
547551
coloredPattern, monitor.Name, monitor.Mode(), monitor.PositionPretty(),
548-
monitor.ScalePretty(), monitor.RotationPretty())
552+
monitor.ScalePretty(), monitor.RotationPretty(false))
549553

550554
if i == p.selectedIndex {
551555
item = SelectedMonitorStyle.Render("► " + item + " ◄")

internal/tui/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
347347
case ToggleMonitorVRRCommand:
348348
logrus.Debug("Received a monitor vrr command")
349349
cmds = append(cmds, m.monitorEditor.ToggleVRR(msg.MonitorID))
350+
case FlipMonitorCommand:
351+
logrus.Debug("Received a monitor flip command")
352+
cmds = append(cmds, m.monitorEditor.FlipMonitor(msg.MonitorID))
350353
case RotateMonitorCommand:
351354
logrus.Debug("Received a monitor rotate command")
352355
cmds = append(cmds, m.monitorEditor.RotateMonitor(msg.MonitorID))

internal/tui/root_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ func TestModel_Update_UserFlows(t *testing.T) {
6969
},
7070
},
7171

72+
{
73+
name: "rotate_flip",
74+
monitorsData: defaultMonitorData,
75+
runFor: utils.JustPtr(500 * time.Millisecond),
76+
steps: []step{
77+
{
78+
msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}},
79+
expectOutputToContain: "► DP-1",
80+
},
81+
{
82+
msg: tea.KeyMsg{Type: tea.KeyEnter},
83+
expectOutputToContain: "EDITING",
84+
},
85+
{
86+
msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'r'}},
87+
expectOutputToContain: "DP-1→",
88+
},
89+
{
90+
msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'L'}},
91+
expectOutputToContain: "1-PD→",
92+
},
93+
},
94+
},
95+
7296
{
7397
name: "scale_open",
7498
monitorsData: defaultMonitorData,

0 commit comments

Comments
 (0)