From 04b4243ad31d144c4caf9be4c5096a0f27a2648e Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 17 Sep 2026 00:23:51 +0200 Subject: gui: syntax colours, a file preview, and a settings window --- gui/internal/model/highlight.go | 97 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 gui/internal/model/highlight.go (limited to 'gui/internal/model/highlight.go') diff --git a/gui/internal/model/highlight.go b/gui/internal/model/highlight.go new file mode 100644 index 0000000..e34d9d9 --- /dev/null +++ b/gui/internal/model/highlight.go @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package model + +// SpanKind is what a stretch of a configuration file is, for colouring it. +type SpanKind int + +const ( + SpanComment SpanKind = iota + SpanString + SpanHead // the symbol a form starts with + SpanAction // a head that does something to a file + SpanParen +) + +// Span is one stretch of text to colour, in character offsets - what a +// GtkTextBuffer counts in, not bytes. +type Span struct { + From, To int + Kind SpanKind +} + +// actionHeads are the forms that act on a file rather than describe one. +var actionHeads = map[string]bool{ + "copy": true, "move": true, "rename": true, "delete": true, "stop": true, +} + +// Spans reads a configuration and says what to colour: a ";" outside a +// string comments out the rest of its line; a string runs to its closing +// quote, a backslash escaping the next character; the first symbol after +// "(" is the form's head. Nothing here knows about widgets, so the rules of +// the little language stay testable (his request, 2026-09-16). +func Spans(text string) []Span { + var out []Span + runes := []rune(text) + afterParen := false + for i := 0; i < len(runes); i++ { + switch c := runes[i]; { + case c == ';': + j := i + for j < len(runes) && runes[j] != '\n' { + j++ + } + out = append(out, Span{i, j, SpanComment}) + i = j + afterParen = false + case c == '"': + j := i + 1 + for j < len(runes) { + if runes[j] == '\\' { + j += 2 + continue + } + if runes[j] == '"' { + j++ + break + } + j++ + } + if j > len(runes) { + j = len(runes) + } + out = append(out, Span{i, j, SpanString}) + i = j - 1 + afterParen = false + case c == '(' || c == ')': + out = append(out, Span{i, i + 1, SpanParen}) + afterParen = c == '(' + case c == ' ' || c == '\t' || c == '\n' || c == '\r': + // whitespace between "(" and the head is allowed + default: + j := i + for j < len(runes) && !isDelimiter(runes[j]) { + j++ + } + if afterParen { + kind := SpanHead + if actionHeads[string(runes[i:j])] { + kind = SpanAction + } + out = append(out, Span{i, j, kind}) + } + i = j - 1 + afterParen = false + } + } + return out +} + +// isDelimiter reports whether a character ends a symbol. +func isDelimiter(r rune) bool { + switch r { + case ' ', '\t', '\n', '\r', '(', ')', '"', ';': + return true + } + return false +} -- cgit v1.3