Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 13 additions & 1 deletion images/chromium-headful/xorg.conf
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Section "Monitor"
Modeline "960x720_60.00" 55.86 960 1008 1104 1248 720 721 724 746 -HSync +Vsync
# 800x600 @ 60.00 Hz (GTF) hsync: 37.32 kHz; pclk: 38.22 MHz
Modeline "800x600_60.00" 38.22 800 832 912 1024 600 601 604 622 -HSync +Vsync
# 768x1024 @ 60.00 Hz (GTF) hsync: 63.60 kHz; pclk: 65.13 MHz
Modeline "768x1024_60.00" 65.13 768 816 896 1024 1024 1025 1028 1060 -HSync +Vsync
# 390x844 @ 60.00 Hz (manual, non-CVT) pclk: 27.26 MHz (mobile: iPhone 14/15)
Modeline "390x844_60" 27.26 390 406 446 520 844 845 848 874 -HSync +Vsync
# 2560x1440 @ 60.00 Hz (GTF) hsync: 89.52 kHz; pclk: 312.25 MHz
Modeline "2560x1440_60.00" 312.25 2560 2752 3024 3488 1440 1443 1448 1493 -HSync +Vsync
# 1024x768 @ 60.00 Hz (GTF) hsync: 47.70 kHz; pclk: 63.50 MHz
Expand All @@ -89,6 +93,10 @@ Section "Monitor"
Modeline "960x720_30.00" 25.33 960 960 1056 1152 720 721 724 733 -HSync +Vsync
# 800x600 @ 30.00 Hz (GTF) hsync: 18.33 kHz; pclk: 17.01 MHz
Modeline "800x600_30.00" 17.01 800 792 864 928 600 601 604 611 -HSync +Vsync
# 768x1024 @ 30.00 Hz (GTF) hsync: 31.26 kHz; pclk: 30.01 MHz
Modeline "768x1024_30.00" 30.01 768 784 864 960 1024 1025 1028 1042 -HSync +Vsync
# 390x844 @ 30.00 Hz (manual, non-CVT) pclk: 12.57 MHz (mobile: iPhone 14/15)
Modeline "390x844_30" 12.57 390 406 446 488 844 845 848 859 -HSync +Vsync
# 1920x1200 @ 30.00 Hz (GTF) hsync: 36.90 kHz; pclk: 96.00 MHz
Modeline "1920x1200_30.00" 96.00 1920 2000 2200 2528 1200 1203 1209 1235 -HSync +Vsync
# 1440x900 @ 30.00 Hz (GTF) hsync: 27.72 kHz; pclk: 52.80 MHz
Expand Down Expand Up @@ -120,6 +128,10 @@ Section "Monitor"
Modeline "1200x800_25.00" 31.48 1200 1224 1352 1540 800 801 804 818 -HSync +Vsync
# 800x1600 @ 24.92 Hz (CVT) hsync: 40.53 kHz; pclk: 41.50 MHz
Modeline "800x1600_25.00" 41.50 800 832 912 1024 1600 1603 1613 1626 -Hsync +Vsync
# 768x1024 @ 25.00 Hz (GTF) hsync: 25.97 kHz; pclk: 24.52 MHz
Modeline "768x1024_25.00" 24.52 768 784 856 944 1024 1025 1028 1039 -HSync +Vsync
# 390x844 @ 25.00 Hz (manual, non-CVT) pclk: 11.14 MHz (mobile: iPhone 14/15)
Modeline "390x844_25" 11.14 390 406 446 520 844 845 848 857 -HSync +Vsync

# 2560x1440 @ 10.00 Hz (GTF) hsync: 14.65 kHz; pclk: 48.76 MHz
Modeline "2560x1440_10.00" 48.76 2560 2568 2816 3104 1440 1441 1444 1465 -HSync +Vsync
Expand All @@ -143,7 +155,7 @@ Section "Screen"
SubSection "Display"
Viewport 0 0
Depth 24
Modes "2560x1440_60.00" "1920x1080_60.00" "1920x1200_60.00" "1440x900_60.00" "1280x720_60.00" "1200x800_60.00" "1152x648_60.00" "1024x768_60.00" "1024x576_60.00" "960x720_60.00" "800x600_60.00" "2560x1440_30.00" "1920x1080_30.00" "1920x1200_30.00" "1440x900_30.00" "1368x768_30.00" "1280x720_30.00" "1200x800_30.00" "1152x648_30.00" "1024x768_30.00" "1024x576_30.00" "960x720_30.00" "800x600_30.00" "800x1600_30.00" "3840x2160_25.00" "2560x1440_25.00" "1920x1080_25.00" "1920x1200_25.00" "1600x900_25.00" "1440x900_25.00" "1368x768_25.00" "1200x800_25.00" "1024x768_25.00" "800x1600_25.00" "2560x1440_10.00" "1920x1080_10.00" "1920x1200_10.00" "1440x900_10.00" "1200x800_10.00" "1024x768_10.00"
Modes "2560x1440_60.00" "1920x1080_60.00" "1920x1200_60.00" "1440x900_60.00" "1280x720_60.00" "1200x800_60.00" "1152x648_60.00" "1024x768_60.00" "1024x576_60.00" "960x720_60.00" "800x600_60.00" "768x1024_60.00" "390x844_60" "2560x1440_30.00" "1920x1080_30.00" "1920x1200_30.00" "1440x900_30.00" "1368x768_30.00" "1280x720_30.00" "1200x800_30.00" "1152x648_30.00" "1024x768_30.00" "1024x576_30.00" "960x720_30.00" "800x600_30.00" "800x1600_30.00" "768x1024_30.00" "390x844_30" "3840x2160_25.00" "2560x1440_25.00" "1920x1080_25.00" "1920x1200_25.00" "1600x900_25.00" "1440x900_25.00" "1368x768_25.00" "1200x800_25.00" "1024x768_25.00" "800x1600_25.00" "768x1024_25.00" "390x844_25" "2560x1440_10.00" "1920x1080_10.00" "1920x1200_10.00" "1440x900_10.00" "1200x800_10.00" "1024x768_10.00"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the modeline names use _60.00 suffix (e.g. "390x844_60.00") but the Modes list references them without the .00 (e.g. "390x844_60", "390x844_30", "390x844_25"). The 768x1024 entries are consistent ("768x1024_60.00" etc.) but the 390x844 ones seem mismatched

EndSubSection
EndSection

Expand Down
60 changes: 60 additions & 0 deletions server/cmd/api/api/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

nekooapi "github.com/m1k1o/neko/server/lib/oapi"
"github.com/onkernel/kernel-images/server/lib/chromiumflags"
"github.com/onkernel/kernel-images/server/lib/logger"
oapi "github.com/onkernel/kernel-images/server/lib/oapi"
)
Expand Down Expand Up @@ -88,6 +89,21 @@ func (s *ApiService) PatchDisplay(ctx context.Context, req oapi.PatchDisplayRequ
restartChrome = *req.Body.RestartChromium
}

// App mode: Chromium's --app flag removes the tab bar and toolbar chrome,
// allowing the window to resize below the normal ~500px minimum width
// and ~200px minimum height.
// We automatically toggle this based on the requested viewport dimensions.
const appModeWidthThreshold = 500
const appModeHeightThreshold = 200
needsAppMode := width < appModeWidthThreshold || height < appModeHeightThreshold
if toggled, err := s.ensureAppMode(ctx, needsAppMode); err != nil {
log.Error("failed to toggle app mode", "error", err)
// Non-fatal: continue with the resize even if app mode toggle fails.
} else if toggled {
// App mode changed — force a chromium restart so it picks up the new flags.
restartChrome = true
}

// Route to appropriate resolution change handler
if displayMode == "xorg" {
if s.isNekoEnabled() {
Expand Down Expand Up @@ -390,3 +406,47 @@ func (s *ApiService) setResolutionViaNeko(ctx context.Context, width, height, re
log.Info("successfully changed resolution via Neko API", "width", width, "height", height, "refresh_rate", refreshRate)
return nil
}

// ensureAppMode adds or removes the --app=about:blank flag from the Chromium
// runtime flags file. It returns (true, nil) when the flag state was changed
// (meaning Chromium needs a restart), or (false, nil) when no change was needed.
func (s *ApiService) ensureAppMode(ctx context.Context, enable bool) (toggled bool, err error) {
log := logger.FromContext(ctx)
const flagsPath = "/chromium/flags"
const appFlag = "--app"

existing, err := chromiumflags.ReadOptionalFlagFile(flagsPath)
if err != nil {
return false, fmt.Errorf("failed to read flags file: %w", err)
}

hasApp := chromiumflags.HasFlagWithPrefix(existing, appFlag)

if enable && hasApp {
log.Info("app mode already enabled, no change needed")
return false, nil
}
if !enable && !hasApp {
log.Info("app mode already disabled, no change needed")
return false, nil
}

var updated []string
if enable {
log.Info("enabling app mode (--app=about:blank) for small viewport")
updated = append(existing, "--app=https://start.duckduckgo.com")
} else {
log.Info("disabling app mode for normal viewport")
updated = chromiumflags.RemoveFlagsByPrefix(existing, appFlag)
}

if err := os.MkdirAll("/chromium", 0o755); err != nil {
return false, fmt.Errorf("failed to create chromium dir: %w", err)
}
if err := chromiumflags.WriteFlagFile(flagsPath, updated); err != nil {
return false, fmt.Errorf("failed to write flags file: %w", err)
}

log.Info("app mode toggled", "enabled", enable, "flags", updated)
return true, nil
}
10 changes: 10 additions & 0 deletions server/cmd/chromium-launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ func main() {
}
}

// hasAppFlag returns true if any token in args is "--app" or starts with "--app=".
func hasAppFlag(args []string) bool {
for _, a := range args {
if a == "--app" || strings.HasPrefix(a, "--app=") {
return true
}
}
return false
}

// execLookPath helps satisfy syscall.Exec's requirement to pass an absolute path.
func execLookPath(file string) (string, error) {
if strings.ContainsRune(file, os.PathSeparator) {
Expand Down
11 changes: 11 additions & 0 deletions server/cmd/chromium-launcher/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"os/exec"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestExecLookPath(t *testing.T) {
Expand Down Expand Up @@ -34,3 +36,12 @@ func TestExecLookPath(t *testing.T) {
t.Fatalf("execLookPath PATH search failed: p=%q err=%v", p, err)
}
}

func TestHasAppFlag(t *testing.T) {
assert.False(t, hasAppFlag([]string{}), "empty args")
assert.False(t, hasAppFlag([]string{"--no-first-run", "--disable-gpu"}), "no app flag")
assert.True(t, hasAppFlag([]string{"--no-first-run", "--app=about:blank"}), "--app=about:blank")
assert.True(t, hasAppFlag([]string{"--app=https://example.com"}), "--app=URL")
assert.True(t, hasAppFlag([]string{"--app"}), "bare --app")
assert.False(t, hasAppFlag([]string{"--application-name=foo"}), "--application-name should not match")
}
25 changes: 25 additions & 0 deletions server/lib/chromiumflags/chromiumflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,31 @@ func MergeExtensionPath(args []string, extPath string) []string {
return result
}

// RemoveFlagsByPrefix returns a new slice with any tokens that match the given
// prefix removed. For example, prefix "--app" removes "--app", "--app=about:blank", etc.
// It does NOT match longer flag names that merely share the prefix
// (e.g. "--app" will not match "--application-name").
func RemoveFlagsByPrefix(tokens []string, prefix string) []string {
out := make([]string, 0, len(tokens))
for _, tok := range tokens {
if tok == prefix || strings.HasPrefix(tok, prefix+"=") {
continue
}
out = append(out, tok)
}
return out
}

// HasFlagWithPrefix returns true if any token equals prefix or starts with prefix + "=".
func HasFlagWithPrefix(tokens []string, prefix string) bool {
for _, tok := range tokens {
if tok == prefix || strings.HasPrefix(tok, prefix+"=") {
return true
}
}
return false
}

// WriteFlagFile writes the provided tokens to the given path as JSON in the
// form: { "flags": ["--foo", "--bar=1"] } with file mode 0644.
// The function creates or truncates the file.
Expand Down
58 changes: 58 additions & 0 deletions server/lib/chromiumflags/chromiumflags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"reflect"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseFlags(t *testing.T) {
Expand Down Expand Up @@ -214,6 +216,62 @@ func TestWriteFlagFileAndReadBack(t *testing.T) {

// TestWriteFlagFileFromString removed: callers should use WriteFlagFile with tokens.

func TestRemoveFlagsByPrefix(t *testing.T) {
tests := []struct {
name string
tokens []string
prefix string
want []string
}{
{
name: "remove exact match",
tokens: []string{"--foo", "--app", "--bar"},
prefix: "--app",
want: []string{"--foo", "--bar"},
},
{
name: "remove prefix=value match",
tokens: []string{"--foo", "--app=about:blank", "--bar"},
prefix: "--app",
want: []string{"--foo", "--bar"},
},
{
name: "does not remove longer flag names",
tokens: []string{"--foo", "--application-name=test", "--app=about:blank"},
prefix: "--app",
want: []string{"--foo", "--application-name=test"},
},
{
name: "nothing to remove",
tokens: []string{"--foo", "--bar"},
prefix: "--app",
want: []string{"--foo", "--bar"},
},
{
name: "empty input",
tokens: []string{},
prefix: "--app",
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := RemoveFlagsByPrefix(tt.tokens, tt.prefix)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("RemoveFlagsByPrefix() = %#v, want %#v", got, tt.want)
}
})
}
}

func TestHasFlagWithPrefix(t *testing.T) {
assert.True(t, HasFlagWithPrefix([]string{"--app=about:blank", "--foo"}, "--app"))
assert.True(t, HasFlagWithPrefix([]string{"--foo", "--app"}, "--app"))
assert.False(t, HasFlagWithPrefix([]string{"--application-name=test"}, "--app"))
assert.False(t, HasFlagWithPrefix([]string{"--foo", "--bar"}, "--app"))
assert.False(t, HasFlagWithPrefix([]string{}, "--app"))
}

func TestMergeFlags(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading