diff --git a/format/align.go b/format/align.go new file mode 100644 index 0000000..fea9b3c --- /dev/null +++ b/format/align.go @@ -0,0 +1,156 @@ +package format + +import ( + "strings" + "unicode/utf8" +) + +// Tab crée une tabulation d’une longueur donnée. +func Tab(size int) string { + return strings.Repeat(" ", size) +} + +// TabulateLeft ajoute espaces à gauche. +func TabulateLeft(str string, tabSize int) string { + return Tab(tabSize) + str +} + +// TabulateRight ajoute espaces à droite. +func TabulateRight(str string, tabSize int) string { + return str + Tab(tabSize) +} + +// Tabulate ajoute espaces à gauche et espaces à droite. +func Tabulate(str string, leftSize, rightSize int) string { + return Tab(leftSize) + str + Tab(rightSize) +} + +// AlignmentType représente un type d’alignement. +type AlignmentType int + +const ( + LEFT AlignmentType = iota + RIGHT + CENTER +) + +// Alignment permet d’aligner des chaînes selon des caractérisques prédéfinies. +type Alignment struct { + t AlignmentType + l int +} + +// NewAlignement retourne un aligneur de type t (gauche, droit ou centré) et de longueur définie. +// Ainsi un aligneur gauche défini à 10 caractères transformera une chaîne de la façon suivante : +// - si la chaîne fait plus de 10 caractères, elle est tronquée à droite +// - sinon, des espaces sont rajoutées à droite de sorte que la chaîne de sortie fasse 10 caractères. +// Un aligneur droit procède de la même manière mais de l’autre côté. +// Un aligneur centré équilibre de sorte que la chaîne d’entrée reste au milieu. +func NewAlignment(t AlignmentType, size int) Alignment { + if size <= 0 { + panic("Size of alignment must be positive") + } + + return Alignment{ + t: t, + l: size, + } +} + +func size(str string) int { return utf8.RuneCountInString(str) } + +// Formate aligne la chaîne d’entrée selon les paramètres +// prédéfinis de l’aligneur. +func (a Alignment) Format(str string) string { + l := size(str) + if l == a.l { + return str + } + d := a.l - l + runes := []rune(str) + switch a.t { + case RIGHT: + if d > 0 { + return TabulateLeft(str, d) + } + return string(runes[-d:]) + case CENTER: + if d > 0 { + left := d >> 1 + right := d - left + return Tabulate(str, left, right) + } + left := (-d) >> 1 + return string(runes[left : left+a.l]) + default: + if d > 0 { + return TabulateRight(str, d) + } + return string(runes[:a.l]) + } +} + +// Align align la chaîne selon le type d’alignement donné, +// de sorte que la chaîne de sortie ait la longueur spécifiée. +func Align(str string, align AlignmentType, size int) string { + a := NewAlignment(align, size) + + return a.Format(str) +} + +// Left aligne à gauche. +func Left(str string, size int) string { + return Align(str, LEFT, size) +} + +// Right aligne à droite. +func Right(str string, size int) string { + return Align(str, RIGHT, size) +} + +// Center centre la chaîne. +func Center(str string, size int) string { + return Align(str, CENTER, size) +} + +// ColumnSet permet de formater les entrées d’un tableau. +// Chaque entrée correspond à un aligneur de colonne. +type ColumnSet []Alignment + +// NewColumnSet retourne un formateur de tableaux. +func NewColumnSet(aligns ...Alignment) ColumnSet { + return aligns +} + +// Normalize formate les colonnes spcéfiées en entrées. +// Le nombre de colonnes doit être identique au nombre de l’aligneurs +// du formateur de colonnes. +func (cs ColumnSet) Normalize(columns []string) (out []string) { + if len(cs) != len(columns) { + panic("Number of columns must be equal to number of column set") + } + + out = make([]string, len(columns)) + for i, a := range cs { + out[i] = a.Format(columns[i]) + } + + return out +} + +// Line formate une ligne de tableau, avec : +// - columns : liste des colonnes de la ligne +// - sep : séparateur de colonnes +// - prefix : préfixe de ligne +// - suffix : suffixe de ligne +func (cs ColumnSet) Line(columns []string, sep, prefix, suffix string) string { + out := cs.Normalize(columns) + + if prefix != "" || suffix != "" { + for i, c := range out { + out[i] = prefix + c + suffix + } + } + + return strings.Join(out, sep) +} diff --git a/format/color.go b/format/color.go new file mode 100644 index 0000000..830ca88 --- /dev/null +++ b/format/color.go @@ -0,0 +1,222 @@ +package format + +import ( + "fmt" + "strings" + + . "gitea.zaclys.com/bvaudour/gob/option" +) + +// Color permet de colorer une chaîne dans un terminal de type UNIX. +type Color struct { + Option[int] +} + +const ( + black int = iota + red + green + yellow + blue + majenta + cyan + white + + light = 1 << 3 + background = 1 << 4 + dark = ^light + foreground = ^dark +) + +var colors = map[string]int{ + "black": black, + "red": red, + "green": green, + "yellow": yellow, + "blue": blue, + "majenta": majenta, + "cyan": cyan, + "white": white, +} + +var prefixes = []string{ + "fg_l_", + "fg_", + "l_", + "bg_l_", + "bg_", +} + +var prefixFuncs = map[string]func(string) Color{ + "fg_l_": fgLightColor, + "fg_": fgColor, + "l_": fgLightColor, + "bg_l_": bgLightColor, + "bg_": bgColor, +} + +// IsLight retourne vrai si la couleur est claire. +func (c Color) IsLight() bool { + v, ok := c.Get() + return ok && v&light != 0 +} + +// IsDark retourne vrai si la couleur est foncée. +func (c Color) IsDark() bool { + v, ok := c.Get() + return ok && v&light == 0 +} + +// IsBackground retourne vrai si c’est une couleur d’arrière-plan. +func (c Color) IsBackground() bool { + v, ok := c.Get() + return ok && v&background != 0 +} + +// IsForeground retourne vrai si c’est une couleur de police. +func (c Color) IsForeground() bool { + v, ok := c.Get() + return ok && v&background == 0 +} + +// Light retourne l’équivalent clair de la couleur. +func (c Color) Light() Color { + if v, ok := c.Get(); ok { + return Color{Some(v | light)} + } + return c +} + +// Dark retourne l’équivalent foncé de la couleur. +func (c Color) Dark() Color { + if v, ok := c.Get(); ok { + return Color{Some(v & dark)} + } + return c +} + +// Background retourne la couleur équivalente d’arrière-plan. +func (c Color) Background() Color { + if v, ok := c.Get(); ok { + return Color{Some(v | background)} + } + return c +} + +// Foreground retourne la couleur équivalente de police. +func (c Color) Foreground() Color { + if v, ok := c.Get(); ok { + return Color{Some(v & foreground)} + } + return c +} + +// Color retourne la composante couleur de base de la couleur. +func (c Color) Color() Color { + if v, ok := c.Get(); ok { + return Color{Some(v & white)} + } + return c +} + +// Color retourne la composante couleur foncée/claire de la couleur . +func (c Color) Base() Option[int] { + v, ok := c.Get() + if !ok { + return None[int]() + } + b := 30 + (v & white) + if c.IsBackground() { + b += 10 + } + if c.IsLight() { + b += 60 + } + + return Some(b) +} + +// String retourne la valeur d’échappement ASCII de la couleur. +func (c Color) String(force ...bool) string { + b, ok := c.Base().Get() + if !ok { + return "" + } + s := fmt.Sprint(b) + if len(force) > 0 && force[0] && !c.IsLight() && !c.IsBackground() { + s = "2;" + s + } + + return s +} + +// Format colore la chaîne donnée selon la couleur spécifiée. +func (c Color) Format(text string) string { + if !c.IsDefined() { + return text + } + return fmt.Sprintf(formatTpl, c.String(), text) +} + +func colorOf(name string) (c Color) { + if v, ok := colors[name]; ok { + c = Color{Some(v)} + } + return +} + +func fgDarkColor(name string) Color { + return colorOf(name) +} + +func fgLightColor(name string) (c Color) { + return colorOf(name).Light() +} + +func fgColor(name string) Color { + if strings.HasPrefix(name, "l_") { + n := strings.TrimPrefix(name, "l_") + return fgLightColor(n) + } + return fgDarkColor(name) +} + +func bgDarkColor(name string) Color { + return colorOf(name).Background() +} + +func bgLightColor(name string) (c Color) { + return colorOf(name).Background().Light() +} + +func bgColor(name string) Color { + if strings.HasPrefix(name, "l_") { + n := strings.TrimPrefix(name, "l_") + return bgLightColor(n) + } + return bgDarkColor(name) +} + +// ColorOf retourne la couleur au nom demandé. +// Les noms possibles sont : +// - white +// - black +// - red +// - green +// - blue +// - yellow +// - magenta +// - cyan +// Le préfixe l_ peut être rajouté à la couleur pour forcer le mode clair. +// Le préfixe bg_ ou fg_ peut être ajouté pour forcer, respectivement la couleur +// d’arrière-plan ou de police (mode par défaut). +func ColorOf(name string) Color { + for _, prefix := range prefixes { + if strings.HasPrefix(name, prefix) { + n := strings.TrimPrefix(name, prefix) + return prefixFuncs[prefix](n) + } + } + + return colorOf(name) +} diff --git a/format/format.go b/format/format.go new file mode 100644 index 0000000..30a4d1f --- /dev/null +++ b/format/format.go @@ -0,0 +1,170 @@ +package format + +import ( + "fmt" + "strings" + + "gitea.zaclys.com/bvaudour/gob/collection" +) + +const formatTpl = "\033[%sm%s\033[m" + +// Format permet d’appliquer des styles et des couleurs à des chaînes dans un terminal de type UNIX. +type Format struct { + fg Color + bg Color + styles []Style +} + +// Background retourne la couleur d’arrière-plan. +func (f Format) Background() Color { + return f.bg +} + +// Foreground retourne la couleur de police. +func (f Format) Foreground() Color { + return f.fg +} + +// Styles retourne les styles appliqués. +func (f Format) Styles() []Style { + out := make([]Style, len(f.styles)) + copy(out, f.styles) + + return out +} + +// SetBg définit la couleur d’arrière-plan au formateur. +func (f *Format) SetBg(c Color) Format { + if !c.IsForeground() { + f.bg = c + } + + return *f +} + +// RemoveBg supprime la couleur d’arrière-plan du formatteur. +func (f *Format) RemoveBg() Format { + return f.SetBg(Color{}) +} + +// SetFg définit la couleur de police du formateur. +func (f *Format) SetFg(c Color) Format { + if !c.IsBackground() { + f.fg = c + } + + return *f +} + +// RemoveFg supprime la couleur de police du formateur. +func (f *Format) RemoveFg() Format { + return f.SetFg(Color{}) +} + +// SetColor ajoute la couleur au formateur (arrière-plan ou police). +func (f *Format) SetColor(c Color) Format { + if c.IsBackground() { + return f.SetBg(c) + } else if c.IsForeground() { + return f.SetFg(c) + } + return *f +} + +// AddStyles ajoute des styles au formateur. +func (f *Format) AddStyles(st ...Style) Format { + for _, s := range st { + if s.IsDefined() { + f.styles = append(f.styles, s) + } + } + + return *f +} + +// RemoveStyles supprime les styles du formateur. +func (f *Format) RemoveStyles(st ...Style) Format { + f.styles = collection.Diff(f.styles, st) + + return *f +} + +// Push ajoute des styles et des couleurs au formateur selon les noms donnés. +// Pour plus de détail, lire la documentation sur StyleOf et ColorOf. +func (f *Format) Push(names ...string) Format { + for _, n := range names { + if st := StyleOf(n); st.IsDefined() { + f.AddStyles(st) + } else if c := ColorOf(n); c.IsDefined() { + f.SetColor(c) + } + } + + return *f +} + +// Pop supprime les styles et les couleurs selon les noms donnés. +// Pour plus de détail, lire la documentation sur StyleOf et ColorOf. +func (f *Format) Pop(names ...string) Format { + for _, n := range names { + if st := StyleOf(n); st.IsDefined() { + f.RemoveStyles(st) + } else if c := ColorOf(n); c.IsDefined() { + if c.IsBackground() { + f.RemoveBg() + } else { + f.RemoveFg() + } + } + } + + return *f +} + +// Reset réinitialise le formateur. +func (f *Format) Reset() Format { + f.fg = Color{} + f.bg = Color{} + f.styles = []Style{} + + return *f +} + +// String retourne la chaîne d’échappement ASCII du formateur. +func (f Format) String() string { + var args []string + var is_bold bool + + for _, s := range f.styles { + args = append(args, s.String()) + if s.IsNormal() || (s.IsBold() && s.IsCancel()) { + is_bold = false + } else if s.IsBold() { + is_bold = true + } + } + if f.fg.IsDefined() { + args = append(args, f.fg.String(is_bold)) + } + if f.bg.IsDefined() { + args = append(args, f.bg.String()) + } + + return strings.Join(args, ";") +} + +// Format formate le texte selon la configuration du formateur. +func (f Format) Format(text string) string { + return fmt.Sprintf(formatTpl, f, text) +} + +// FormatOf retourne un formateur selon les noms de styles et de couleurs à appliquer. +func FormatOf(formats ...string) (f Format) { + return f.Push(formats...) +} + +// Apply formate le texte donné avec les styles/couleurs donnés (par nom). +func Apply(text string, formats ...string) string { + return FormatOf(formats...).Format(text) +} diff --git a/format/style.go b/format/style.go new file mode 100644 index 0000000..51eb852 --- /dev/null +++ b/format/style.go @@ -0,0 +1,95 @@ +package format + +import ( + "fmt" + + . "gitea.zaclys.com/bvaudour/gob/option" +) + +// Style permet de définir un style à une chaîne dans un terminal de type UNIX. +type Style struct { + Option[int] +} + +const ( + normal int = 0 + bold int = 1 + italic int = 3 + underline int = 4 + blink int = 5 + inverted int = 7 + crossed int = 9 + cancel int = 20 +) + +var styles = map[string]int{ + "normal": normal, + "bold": bold, + "italic": italic, + "underline": underline, + "blink": blink, + "inverted": inverted, + "crossed": crossed, + "!bold": cancel + bold, + "!italic": cancel + italic, + "!underline": cancel + underline, + "!blink": cancel + blink, + "!inverted": cancel + inverted, + "!crossed": cancel + crossed, +} + +// IsCancel retourne vrai si le style est une annulation de style. +func (st Style) IsCancel() bool { + v, ok := st.Get() + return ok && v >= 20 +} + +// IsBold retourne vrai si le style de de type gras. +func (st Style) IsBold() bool { + v, ok := st.Get() + return ok && v|bold != 0 +} + +// IsNormal retourne vrai si aucun style n’est appliqué. +func (st Style) IsNormal() bool { + v, ok := st.Get() + return ok && v == normal +} + +// Base retourne la liste des styles. +func (st Style) Base() Option[int] { + return st.Option +} + +// String retourne la chaine d’échappement ASCII du style. +func (st Style) String() string { + if v, ok := st.Get(); ok { + return fmt.Sprintf("%d", v) + } + return "" +} + +// Format formate le texte donné selon le style. +func (st Style) Format(text string) string { + if !st.IsDefined() { + return text + } + return fmt.Sprintf(formatTpl, st.String(), text) +} + +// StyleOf retourne le style selon son nom. +// Les noms possibles sont : +// - normal : pas de style particulier +// - bold : chaîne en gras +// - italic : chaîne en italique +// - underline : chaîne soulignée +// - blink : chaîne clignotante +// - inverted : switch entre les couleurs de police et d’arrière-plan +// - crossed : chaîne rayée +// Le préfixe ! peut être ajouté pour annuler le style. +func StyleOf(name string) (st Style) { + if v, ok := styles[name]; ok { + return Style{Some(v)} + } + return +}