Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion images/chromium-headful/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ RUN --mount=type=cache,target=/tmp/cache/ffmpeg,sharing=locked,id=$CACHEIDPREFIX
rm -rf /tmp/ffmpeg*
EOT

FROM ghcr.io/kernel/neko/base:3.0.8-v1.3.0 AS neko
FROM ghcr.io/kernel/neko/base:3.0.8-v1.4.0 AS neko
# ^--- now has event.SYSTEM_PONG with legacy support to keepalive
FROM node:22-bullseye-slim AS node-22
FROM docker.io/ubuntu:22.04
Expand Down
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.00" 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.00" 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.00" 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
Binary file added server/chromium-launcher
Binary file not shown.
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=https://start.duckduckgo.com 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=https://start.duckduckgo.com) 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
}
1 change: 1 addition & 0 deletions server/cmd/chromium-launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func main() {
}
}


// 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
1 change: 1 addition & 0 deletions server/cmd/chromium-launcher/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ func TestExecLookPath(t *testing.T) {
t.Fatalf("execLookPath PATH search failed: p=%q err=%v", p, err)
}
}

2 changes: 1 addition & 1 deletion server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,4 @@ require (
modernc.org/sqlite v1.23.1 // indirect
)

replace github.com/m1k1o/neko/server => github.com/onkernel/neko/server v0.0.0-20251008185748-46e2fc7d3866
replace github.com/m1k1o/neko/server => github.com/onkernel/neko/server v0.0.0-20260213021128-abe9ac59a634
4 changes: 2 additions & 2 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/onkernel/neko/server v0.0.0-20251008185748-46e2fc7d3866 h1:Cix/sgZLCsavpiTFxDLPbUOXob50IekCg5mgh+i4D4Q=
github.com/onkernel/neko/server v0.0.0-20251008185748-46e2fc7d3866/go.mod h1:0+zactiySvtKwfe5JFjyNrSuQLA+EEPZl5bcfcZf1RM=
github.com/onkernel/neko/server v0.0.0-20260213021128-abe9ac59a634 h1:Q8v6O/VRVLKcEHMSGC0ItDmLFShKLny/0bBggC/1jjk=
github.com/onkernel/neko/server v0.0.0-20260213021128-abe9ac59a634/go.mod h1:0+zactiySvtKwfe5JFjyNrSuQLA+EEPZl5bcfcZf1RM=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
Expand Down
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