Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions ryoku/hub/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## Unreleased

### Fixed
- Fastfetch renderer and recolor markers survive Settings saves. Renderer based
styles keep their readout, and wallpaper recoloring keeps its palette roles.
Rows are locked for styles that provide their own renderer.
- **Bluetooth can recover when rfkill hides the adapter.** Connections shows
the software-blocked state and keeps its power switch available so the shared
helper can bring the controller back (`quickshell/pages/ConnectionsPage.qml`).
Expand Down
72 changes: 55 additions & 17 deletions ryoku/hub/backend/fastfetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,47 @@ const (
)

var (
ffAnsiRe = regexp.MustCompile("\x1b\\[[0-9;]*m")
ffTanRe = regexp.MustCompile("\x1b\\[38;2;" + regexp.QuoteMeta(ffTan) + "m(.*?)\x1b\\[0m")
ffLabelRe = regexp.MustCompile("\x1b\\[1;38;2;" + regexp.QuoteMeta(ffBright) + "m(.*?)\x1b\\[0m")
ffKeysColor = regexp.MustCompile(`(?:38;2;)?([0-9]+;[0-9]+;[0-9]+)`)
ffHexColor = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`)
ffTrueColorRe = regexp.MustCompile("\x1b\\[((?:1;)?)38;2;([0-9]+;[0-9]+;[0-9]+)m")
ffRecolorLineRe = regexp.MustCompile(`(?m)^[\t ]*//[\t ]*ryoku:recolor[\t ]+([^\r\n]+)`)
ffRecolorPairRe = regexp.MustCompile(`([A-Za-z][A-Za-z0-9_-]*)=([A-Za-z][A-Za-z0-9_-]*)`)
ffAnsiRe = regexp.MustCompile("\x1b\\[[0-9;]*m")
ffTanRe = regexp.MustCompile("\x1b\\[38;2;" + regexp.QuoteMeta(ffTan) + "m(.*?)\x1b\\[0m")
ffLabelRe = regexp.MustCompile("\x1b\\[1;38;2;" + regexp.QuoteMeta(ffBright) + "m(.*?)\x1b\\[0m")
ffKeysColor = regexp.MustCompile(`(?:38;2;)?([0-9]+;[0-9]+;[0-9]+)`)
ffHexColor = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`)
ffTrueColorRe = regexp.MustCompile("\x1b\\[((?:1;)?)38;2;([0-9]+;[0-9]+;[0-9]+)m")
ffRecolorLineRe = regexp.MustCompile(`(?m)^[\t ]*//[\t ]*ryoku:recolor[\t ]+([^\r\n]+)`)
ffRendererLineRe = regexp.MustCompile(`(?m)^//[\t ]*ryoku:renderer[\t ]+(\S+)[\t ]*$`)
ffRecolorPairRe = regexp.MustCompile(`([A-Za-z][A-Za-z0-9_-]*)=([A-Za-z][A-Za-z0-9_-]*)`)
// ffSlotTrueColorRe matches a rice's inline fastfetch colour placeholder
// "{#38;2;R;G;B}", whose R;G;B is a fixed brand tone baked into the format
// (fastfetch resolves it to literal ANSI). The wallpaper pass rewrites the
// ones that name a palette role.
ffSlotTrueColorRe = regexp.MustCompile(`\{#((?:1;)?38;2;)([0-9]+;[0-9]+;[0-9]+)\}`)
)

func ffRendererLine(config []byte) string {
if match := ffRendererLineRe.FindSubmatch(config); match != nil {
return string(match[1])
}
return ""
}

func ffRecolorLine(config []byte) string {
if match := ffRecolorLineRe.FindSubmatch(config); match != nil {
return strings.TrimSpace(string(match[1]))
}
return ""
}

func ffWithMarkers(config []byte, renderer, recolor string) []byte {
var head []byte
if renderer != "" {
head = append(head, "// ryoku:renderer "+renderer+"\n"...)
}
if recolor != "" {
head = append(head, "// ryoku:recolor "+recolor+"\n"...)
}
return append(head, config...)
}

func ffBaseOS() string {
if base, ok := os.LookupEnv("RYOKU_BASE_OS"); ok {
return base
Expand Down Expand Up @@ -195,12 +221,14 @@ type ffRow struct {
}

type ffModel struct {
Logo ffLogo `json:"logo"`
Accent string `json:"accent"`
Palette string `json:"palette"`
Rows []ffRow `json:"rows"`
Display json.RawMessage `json:"display,omitempty"`
Schema string `json:"schema,omitempty"`
Logo ffLogo `json:"logo"`
Accent string `json:"accent"`
Palette string `json:"palette"`
Renderer string `json:"renderer,omitempty"`
Recolor string `json:"recolor,omitempty"`
Rows []ffRow `json:"rows"`
Display json.RawMessage `json:"display,omitempty"`
Schema string `json:"schema,omitempty"`
}

type ffPalette struct {
Expand Down Expand Up @@ -342,6 +370,16 @@ func runFastfetch(args []string) error {
return err
}
}
if m.Renderer == "" || m.Recolor == "" {
if current, err := os.ReadFile(fastfetchConfigPath()); err == nil {
if m.Renderer == "" {
m.Renderer = ffRendererLine(current)
}
if m.Recolor == "" {
m.Recolor = ffRecolorLine(current)
}
}
}
b, err := buildFastfetch(m)
if err != nil {
return err
Expand Down Expand Up @@ -434,7 +472,7 @@ func loadFastfetch() (ffModel, error) {
if err := json.Unmarshal(stripJSONC(raw), &doc); err != nil {
return ffModel{}, fmt.Errorf("parse config.jsonc: %w", err)
}
m := ffModel{Schema: doc.Schema, Display: doc.Display, Accent: ffDisplayAccent(doc.Display), Palette: readFastfetchPaletteMode(), Logo: ffNormalizeLogo(doc.Logo)}
m := ffModel{Schema: doc.Schema, Display: doc.Display, Accent: ffDisplayAccent(doc.Display), Palette: readFastfetchPaletteMode(), Logo: ffNormalizeLogo(doc.Logo), Renderer: ffRendererLine(raw), Recolor: ffRecolorLine(raw)}
baseOS := ffBaseOS()
for _, rm := range doc.Modules {
m.Rows = append(m.Rows, ffNormalizeModule(rm, baseOS))
Expand Down Expand Up @@ -605,7 +643,7 @@ func buildFastfetch(m ffModel) ([]byte, error) {
if err != nil {
return nil, err
}
return append(b, '\n'), nil
return ffWithMarkers(append(b, '\n'), m.Renderer, m.Recolor), nil
}

func ffApplyPaletteToRow(raw json.RawMessage, palette ffPalette) (json.RawMessage, error) {
Expand Down Expand Up @@ -823,7 +861,7 @@ func buildFastfetchEffectiveWithRoles(m ffModel, recolor map[string]string) ([]b
if err != nil {
return nil, err
}
return append(b, '\n'), nil
return ffWithMarkers(append(b, '\n'), m.Renderer, m.Recolor), nil
}

func fastfetchEffectiveFresh(path string, mode string, japaneseGloss bool) bool {
Expand Down
125 changes: 125 additions & 0 deletions ryoku/hub/backend/fastfetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,3 +685,128 @@ func TestShippedRiceTaglinesNameTheRunningDistribution(t *testing.T) {
}
}
}

func TestFastfetchRendererMarkerSurvivesSave(t *testing.T) {
loadSample(t)
if err := os.WriteFile(fastfetchPalettePath(), []byte(sampleWallpaperPalette), 0o644); err != nil {
t.Fatal(err)
}
const marker = "// ryoku:renderer ryoku-filigree/bin/ryoku-fetch"
path := fastfetchConfigPath()
if err := os.WriteFile(path, []byte(marker+"\n"+sampleFF), 0o644); err != nil {
t.Fatal(err)
}
m, err := loadFastfetch()
if err != nil {
t.Fatal(err)
}
if m.Renderer != "ryoku-filigree/bin/ryoku-fetch" {
t.Fatalf("renderer = %q, want ryoku-filigree/bin/ryoku-fetch", m.Renderer)
}

built, err := buildFastfetch(m)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(built), marker+"\n") {
t.Fatalf("fixed rebuild dropped the renderer marker:\n%s", built)
}
effective, err := buildFastfetchEffective(m)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(effective), marker+"\n") {
t.Fatalf("wallpaper rebuild dropped the renderer marker:\n%s", effective)
}

m.Renderer = ""
payload, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
if err := runFastfetch([]string{"save", string(payload)}); err != nil {
t.Fatalf("save: %v", err)
}
saved, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(saved), marker+"\n") {
t.Fatalf("save dropped the renderer marker:\n%s", saved)
}

if err := os.WriteFile(path, []byte(sampleFF), 0o644); err != nil {
t.Fatal(err)
}
m, err = loadFastfetch()
if err != nil {
t.Fatal(err)
}
saved, err = buildFastfetch(m)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(saved), "ryoku:renderer") {
t.Fatalf("plain preset gained a renderer marker:\n%s", saved)
}
}

func TestFastfetchRecolorLineSurvivesSave(t *testing.T) {
loadSample(t)
if err := os.WriteFile(fastfetchPalettePath(), []byte(sampleWallpaperPalette), 0o644); err != nil {
t.Fatal(err)
}
const line = "// ryoku:recolor keys=primary title=foreground output=onSurface"
path := fastfetchConfigPath()
if err := os.WriteFile(path, []byte(line+"\n"+sampleFF), 0o644); err != nil {
t.Fatal(err)
}
m, err := loadFastfetch()
if err != nil {
t.Fatal(err)
}
if want := "keys=primary title=foreground output=onSurface"; m.Recolor != want {
t.Fatalf("recolor = %q, want %q", m.Recolor, want)
}
built, err := buildFastfetch(m)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(built), line+"\n") {
t.Fatalf("rebuild dropped the recolor line:\n%s", built)
}

m.Recolor = ""
payload, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
if err := runFastfetch([]string{"save", string(payload)}); err != nil {
t.Fatalf("save: %v", err)
}
saved, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(saved), line+"\n") {
t.Fatalf("save dropped the recolor line:\n%s", saved)
}
if roles := ffRecolorRoles(saved); roles["keys"] != "primary" || roles["output"] != "onSurface" {
t.Fatalf("roles no longer parse from the saved config: %v", roles)
}

if err := os.WriteFile(path, []byte(sampleFF), 0o644); err != nil {
t.Fatal(err)
}
plain, err := loadFastfetch()
if err != nil {
t.Fatal(err)
}
rebuilt, err := buildFastfetch(plain)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(rebuilt), "ryoku:recolor") {
t.Fatalf("plain config gained a recolor line:\n%s", rebuilt)
}
}
36 changes: 30 additions & 6 deletions ryoku/hub/quickshell/pages/FastfetchPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Item {
void pg.rev;
return pg.ready && JSON.stringify(pg.model) !== JSON.stringify(pg.committed);
}
readonly property bool compositionLocked: !!(pg.model && pg.model.renderer)

// fastfetch's own config path; the head's EDIT CONFIG opens it raw, the same
// file the Hub writes on Save so hand-edits and the GUI share one source.
Expand Down Expand Up @@ -220,22 +221,32 @@ Item {
function setAccent(v) { var m = pg.clone(); m.accent = v; pg.commitModel(m); }
function setPalette(v) { var m = pg.clone(); m.palette = v; pg.commitModel(m); }
function setRow(i, k, v) {
if (pg.compositionLocked)
return;
var m = pg.clone();
m.rows[i][k] = v;
if (k === "text" && m.rows[i].kind === "tagline")
m.rows[i].textEdited = true;
pg.commitModel(m);
}
function moveRow(i, d) {
if (pg.compositionLocked)
return;
var j = i + d;
if (j < 0 || j >= pg.model.rows.length)
return;
var m = pg.clone();
var t = m.rows[i]; m.rows[i] = m.rows[j]; m.rows[j] = t;
pg.commitModel(m);
}
function removeRow(i) { var m = pg.clone(); m.rows.splice(i, 1); pg.commitModel(m); }
function removeRow(i) {
if (pg.compositionLocked)
return;
var m = pg.clone(); m.rows.splice(i, 1); pg.commitModel(m);
}
function addFromMenu(key) {
if (pg.compositionLocked)
return;
var m = pg.clone();
if (key === "__header")
m.rows.push({ "kind": "header", "enabled": true, "text": "SECTION", "label": "Section header" });
Expand Down Expand Up @@ -658,10 +669,12 @@ Item {
id: rt
property string glyph: ""
property color tint: Tokens.inkDim
property bool armed: true
signal tapped()
width: 26
height: 26
radius: Tokens.radius
opacity: armed ? 1 : 0.3
color: rh.hovered ? Tokens.tint10 : "transparent"
border.width: Tokens.border
border.color: rh.hovered ? Tokens.lineStrong : Tokens.line
Expand All @@ -673,8 +686,8 @@ Item {
font.family: Tokens.ui
font.pixelSize: 12 // glyph, sized to the button, no matching token
}
HoverHandler { id: rh; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: rt.tapped() }
HoverHandler { id: rh; enabled: rt.armed; cursorShape: Qt.PointingHandCursor }
TapHandler { enabled: rt.armed; onTapped: rt.tapped() }
}

// ── head: eyebrow, Fraunces title with EDIT CONFIG, subtitle ─────────────
Expand Down Expand Up @@ -1291,6 +1304,7 @@ Item {
id: addBtn
anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
glyph: "+"
armed: !pg.compositionLocked
onAct: pg.addOpen = true
}
Rectangle {
Expand All @@ -1301,6 +1315,14 @@ Item {
}
}

Text {
width: parent.width
visible: pg.compositionLocked
text: I18n.tr("This style draws its own composition in the terminal, so its rows are fixed here.")
color: Tokens.inkFaint; font.family: Tokens.ui; font.pixelSize: Tokens.fSmall
wrapMode: Text.WordWrap
}

Repeater {
model: pg.model.rows
delegate: Rectangle {
Expand All @@ -1320,8 +1342,8 @@ Item {
anchors.left: parent.left; anchors.leftMargin: Tokens.s2
anchors.verticalCenter: parent.verticalCenter
spacing: Tokens.s1
RowTool { glyph: "\u2191"; onTapped: pg.moveRow(rowItem.index, -1) }
RowTool { glyph: "\u2193"; onTapped: pg.moveRow(rowItem.index, 1) }
RowTool { glyph: "\u2191"; armed: !pg.compositionLocked; onTapped: pg.moveRow(rowItem.index, -1) }
RowTool { glyph: "\u2193"; armed: !pg.compositionLocked; onTapped: pg.moveRow(rowItem.index, 1) }
}

Text {
Expand All @@ -1342,6 +1364,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
visible: pg.rowEditable(rowItem.modelData.kind)
placeholder: pg.rowPlaceholder(rowItem.modelData.kind)
readOnly: pg.compositionLocked
text: rowItem.modelData.kind === "module" ? (rowItem.modelData.key || "") : (rowItem.modelData.text || "")
onCommitted: (v) => {
var f = rowItem.modelData.kind === "module" ? "key" : "text";
Expand All @@ -1358,9 +1381,10 @@ Item {
RowTool {
glyph: rowItem.modelData.enabled ? "\u25cf" : "\u25cb"
tint: rowItem.modelData.enabled ? Tokens.ink : Tokens.inkFaint
armed: !pg.compositionLocked
onTapped: pg.setRow(rowItem.index, "enabled", !rowItem.modelData.enabled)
}
RowTool { glyph: "\u2715"; onTapped: pg.removeRow(rowItem.index) }
RowTool { glyph: "\u2715"; armed: !pg.compositionLocked; onTapped: pg.removeRow(rowItem.index) }
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions ryoku/ui/Field.qml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Rectangle {
property string placeholder: ""
property bool tabular: false // mono for what a config file would hold
property bool toolbar: false // 36 in a toolbar, 30 in a sheet
property bool readOnly: false // shows a value the screen does not own
property bool secret: false // masked, for a password
property bool clearOnEscape: true
readonly property bool focused: input.activeFocus
Expand Down Expand Up @@ -43,6 +44,7 @@ Rectangle {
selectByMouse: true
echoMode: field.secret ? TextInput.Password : TextInput.Normal
passwordCharacter: "\u2022"
readOnly: field.readOnly
clip: true
onTextEdited: field.edited(text)
onEditingFinished: field.committed(text)
Expand Down
Loading