Skip to content

Commit

Permalink
feat(ui): new tui
Browse files Browse the repository at this point in the history
wip: new ui

wip: selector

wip: header & footer

wip: selection readme

wip: add readme

wip: copy text

wip: fix readme wrap

wip: fix errorview margin

wip: underline filtered items

wip: add selector mouse wheel support

wip: fix reset formatting on line wraps

wip: initial repo ui implementation

wip: repo header

go mod tidy

add git branch symbol

fix: selector keymap

feat: display repo commits

feat: display commit diffs

godox

fix config tests

feat: render error in app style

feat: repo files tab

feat: add refs component

fix: glitches & add refs statusbar

feat: add full help toggle

fix: error view height

wip

init ui and check for errors

fix: initial repo

fix: url style

fix: selecting repo

feat: new log item style

feat: copy over ssh

clean

feat: detect private repos

feat: indicate private repos via emoji

feat: only show private repos for admins

feat: add files content line number

fix: bold selected file size

fix: remove header from repo page

fix: comitter & authored name highlight

fix: repo last updated time

feat: add statusbar symbols

fix: no repos

fix: prevent tab out of bound error

fix: decrease help columns

fix: crooked ui size rendering on startup

fix: various improvements

* don't add line numbers to markdown files
* fix selection active item styles
* fix selection readme styles
* add selection description styles

fix: add footer padding

fix: move repo readme into its own model

* fix refs switch flickering

fix: selection item truncate string

fix: footer padding

feat: repos list title & styles

fix: server cli interface

fix: simplify ui session struct

feat: selection tabs redesign

fix: no reference nil deref error

feat: add log commits loading

fix: replace tabs with space to avoid breaking files line wrapping

clean

fix(ui): selection box position when no repos

fix: log status bar after loading

fix: use actual repo name in status bar

fix(ui): styling after line breaks

fix(ui): subtle ui changes

* hide help from repo page
* file sizes now appear on the right side

fix(ui): show "no description" when a repo doesn't have one

fix(ui): glamour line wrapping

fix(ui): show footer on error

fix(ui): truncate repo clone cmd

fix(ui): truncate git clone command on repo page

fix(ui): truncate strings on small terminal width

fix(ui): switch to files tab on window resize

fix(examples): setuid imports

clean

fix: layout misc changes

fix: wrong dockerfile background highlight

fix(ui): styles on light terminal backgrounds

fix: don't use nerdfonts symbols

fix(git): cache head commits

fix(ui): loading commits spinner text

clear log selected commit after going back to commits log view

fix: respect private repos

Fixes: #81

feat: update deps
  • Loading branch information
aymanbagabas committed Jun 21, 2022
1 parent a1f5bb1 commit ffaa007
Show file tree
Hide file tree
Showing 68 changed files with 4,656 additions and 2,503 deletions.
2 changes: 1 addition & 1 deletion cmd/soft/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"syscall"
"time"

"github.com/charmbracelet/soft-serve/config"
"github.com/charmbracelet/soft-serve/server"
"github.com/charmbracelet/soft-serve/server/config"
)

var (
Expand Down
File renamed without changes.
294 changes: 260 additions & 34 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,53 +1,279 @@
package config

import (
"bytes"
"errors"
"io/fs"
"log"
"path/filepath"
"strings"
"sync"
"text/template"
"time"

"github.com/caarlos0/env/v6"
"golang.org/x/crypto/ssh"
"gopkg.in/yaml.v3"

"fmt"
"os"

"github.com/charmbracelet/soft-serve/server/config"
"github.com/go-git/go-billy/v5/memfs"
ggit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/storage/memory"
)

// Callbacks provides an interface that can be used to run callbacks on different events.
type Callbacks interface {
Tui(action string)
Push(repo string)
Fetch(repo string)
// Config is the Soft Serve configuration.
type Config struct {
Name string `yaml:"name"`
Host string `yaml:"host"`
Port int `yaml:"port"`
AnonAccess string `yaml:"anon-access"`
AllowKeyless bool `yaml:"allow-keyless"`
Users []User `yaml:"users"`
Repos []MenuRepo `yaml:"repos"`
Source *RepoSource `yaml:"-"`
Cfg *config.Config `yaml:"-"`
mtx sync.Mutex
}

// Config is the configuration for Soft Serve.
type Config struct {
BindAddr string `env:"SOFT_SERVE_BIND_ADDRESS" envDefault:""`
Host string `env:"SOFT_SERVE_HOST" envDefault:"localhost"`
Port int `env:"SOFT_SERVE_PORT" envDefault:"23231"`
KeyPath string `env:"SOFT_SERVE_KEY_PATH"`
RepoPath string `env:"SOFT_SERVE_REPO_PATH" envDefault:".repos"`
InitialAdminKeys []string `env:"SOFT_SERVE_INITIAL_ADMIN_KEY" envSeparator:"\n"`
Callbacks Callbacks
ErrorLog *log.Logger
// User contains user-level configuration for a repository.
type User struct {
Name string `yaml:"name"`
Admin bool `yaml:"admin"`
PublicKeys []string `yaml:"public-keys"`
CollabRepos []string `yaml:"collab-repos"`
}

// DefaultConfig returns a Config with the values populated with the defaults
// or specified environment variables.
func DefaultConfig() *Config {
cfg := &Config{ErrorLog: log.Default()}
if err := env.Parse(cfg); err != nil {
log.Fatalln(err)
// Repo contains repository configuration information.
type MenuRepo struct {
Name string `yaml:"name"`
Repo string `yaml:"repo"`
Note string `yaml:"note"`
Private bool `yaml:"private"`
Readme string `yaml:"readme"`
}

// NewConfig creates a new internal Config struct.
func NewConfig(cfg *config.Config) (*Config, error) {
var anonAccess string
var yamlUsers string
var displayHost string
host := cfg.Host
port := cfg.Port

pks := make([]string, 0)
for _, k := range cfg.InitialAdminKeys {
if bts, err := os.ReadFile(k); err == nil {
// pk is a file, set its contents as pk
k = string(bts)
}
var pk = strings.TrimSpace(k)
if pk == "" {
continue
}
// it is a valid ssh key, nothing to do
if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk)); err != nil {
return nil, fmt.Errorf("invalid initial admin key %q: %w", k, err)
}
pks = append(pks, pk)
}
if cfg.KeyPath == "" {
// NB: cross-platform-compatible path
cfg.KeyPath = filepath.Join(".ssh", "soft_serve_server_ed25519")

rs := NewRepoSource(cfg.RepoPath)
c := &Config{
Cfg: cfg,
}
return cfg.WithCallbacks(nil)
c.Host = cfg.Host
c.Port = port
c.Source = rs
if len(pks) == 0 {
anonAccess = "read-write"
} else {
anonAccess = "no-access"
}
if host == "" {
displayHost = "localhost"
} else {
displayHost = host
}
yamlConfig := fmt.Sprintf(defaultConfig,
displayHost,
port,
anonAccess,
len(pks) == 0,
)
if len(pks) == 0 {
yamlUsers = defaultUserConfig
} else {
var result string
for _, pk := range pks {
result += fmt.Sprintf(" - %s\n", pk)
}
yamlUsers = fmt.Sprintf(hasKeyUserConfig, result)
}
yaml := fmt.Sprintf("%s%s%s", yamlConfig, yamlUsers, exampleUserConfig)
err := c.createDefaultConfigRepo(yaml)
if err != nil {
return nil, err
}
return c, nil
}

// WithCallbacks applies the given Callbacks to the configuration.
func (c *Config) WithCallbacks(callbacks Callbacks) *Config {
c.Callbacks = callbacks
return c
// Reload reloads the configuration.
func (cfg *Config) Reload() error {
cfg.mtx.Lock()
defer cfg.mtx.Unlock()
err := cfg.Source.LoadRepos()
if err != nil {
return err
}
cr, err := cfg.Source.GetRepo("config")
if err != nil {
return err
}
cs, _, err := cr.LatestFile("config.yaml")
if err != nil {
return err
}
err = yaml.Unmarshal([]byte(cs), cfg)
if err != nil {
return fmt.Errorf("bad yaml in config.yaml: %s", err)
}
for _, r := range cfg.Source.AllRepos() {
name := r.Name()
err = r.UpdateServerInfo()
if err != nil {
log.Printf("error updating server info for %s: %s", name, err)
}
pat := "README*"
rp := ""
for _, rr := range cfg.Repos {
if name == rr.Repo {
rp = rr.Readme
r.name = rr.Name
r.description = rr.Note
r.private = rr.Private
break
}
}
if rp != "" {
pat = rp
}
rm := ""
fc, fp, _ := r.LatestFile(pat)
rm = fc
if name == "config" {
md, err := templatize(rm, cfg)
if err != nil {
return err
}
rm = md
}
r.SetReadme(rm, fp)
}
return nil
}

func createFile(path string, content string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(content)
if err != nil {
return err
}
return f.Sync()
}

func (cfg *Config) createDefaultConfigRepo(yaml string) error {
cn := "config"
rp := filepath.Join(cfg.Cfg.RepoPath, cn)
rs := cfg.Source
err := rs.LoadRepo(cn)
if errors.Is(err, fs.ErrNotExist) {
repo, err := ggit.PlainInit(rp, true)
if err != nil {
return err
}
repo, err = ggit.Clone(memory.NewStorage(), memfs.New(), &ggit.CloneOptions{
URL: rp,
})
if err != nil && err != transport.ErrEmptyRemoteRepository {
return err
}
wt, err := repo.Worktree()
if err != nil {
return err
}
rm, err := wt.Filesystem.Create("README.md")
if err != nil {
return err
}
_, err = rm.Write([]byte(defaultReadme))
if err != nil {
return err
}
_, err = wt.Add("README.md")
if err != nil {
return err
}
cf, err := wt.Filesystem.Create("config.yaml")
if err != nil {
return err
}
_, err = cf.Write([]byte(yaml))
if err != nil {
return err
}
_, err = wt.Add("config.yaml")
if err != nil {
return err
}
author := object.Signature{
Name: "Soft Serve Server",
Email: "[email protected]",
When: time.Now(),
}
_, err = wt.Commit("Default init", &ggit.CommitOptions{
All: true,
Author: &author,
Committer: &author,
})
if err != nil {
return err
}
err = repo.Push(&ggit.PushOptions{})
if err != nil {
return err
}
} else if err != nil {
return err
}
return cfg.Reload()
}

// WithErrorLogger sets the error logger for the configuration.
func (c *Config) WithErrorLogger(logger *log.Logger) *Config {
c.ErrorLog = logger
return c
func (cfg *Config) isPrivate(repo string) bool {
for _, r := range cfg.Repos {
if r.Repo == repo {
return r.Private
}
}
return false
}

func templatize(mdt string, tmpl interface{}) (string, error) {
t, err := template.New("readme").Parse(mdt)
if err != nil {
return "", err
}
buf := &bytes.Buffer{}
err = t.Execute(buf, tmpl)
if err != nil {
return "", err
}
return buf.String(), nil
}
34 changes: 26 additions & 8 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
package config

import (
"os"
"testing"

"github.com/charmbracelet/soft-serve/server/config"
"github.com/matryer/is"
)

func TestParseMultipleKeys(t *testing.T) {
func TestMultipleInitialKeys(t *testing.T) {
cfg, err := NewConfig(&config.Config{
RepoPath: t.TempDir(),
KeyPath: t.TempDir(),
InitialAdminKeys: []string{
"testdata/k1.pub",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxIobhwtfdwN7m1TFt9wx3PsfvcAkISGPxmbmbauST8 a@b",
},
})
is := is.New(t)
is.NoErr(os.Setenv("SOFT_SERVE_INITIAL_ADMIN_KEY", "testdata/k1.pub\ntestdata/k2.pub"))
t.Cleanup(func() { is.NoErr(os.Unsetenv("SOFT_SERVE_INITIAL_ADMIN_KEY")) })
cfg := DefaultConfig()
is.Equal(cfg.InitialAdminKeys, []string{
"testdata/k1.pub",
"testdata/k2.pub",
is.NoErr(err)
err = cfg.Reload()
is.NoErr(err)
is.Equal(cfg.Users[0].PublicKeys, []string{
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMwLvyV3ouVrTysUYGoJdl5Vgn5BACKov+n9PlzfPwH a@b",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxIobhwtfdwN7m1TFt9wx3PsfvcAkISGPxmbmbauST8 a@b",
}) // should have both keys
}

func TestEmptyInitialKeys(t *testing.T) {
cfg, err := NewConfig(&config.Config{
RepoPath: t.TempDir(),
KeyPath: t.TempDir(),
})
is := is.New(t)
is.NoErr(err)
is.Equal(len(cfg.Users), 0) // should not have any users
}
File renamed without changes.
Loading

0 comments on commit ffaa007

Please sign in to comment.