Skip to content

Commit

Permalink
feat: dynamically expand margin up to 20% of terminal size
Browse files Browse the repository at this point in the history
  • Loading branch information
alecthomas committed Jan 21, 2024
1 parent 44434f4 commit a83fbec
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 59 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
*.o
build/*

build/
dist/
13 changes: 7 additions & 6 deletions cmd/bit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ func main() {
}, kong.Vars{
"version": version,
"describe_help": `Where ASPECT is one of:
files: list all files Bit has determined are inputs and outputs
deps: show dependency graph
targets: list all targets
ignored: list all loaded ignore patterns (from .gitignore files)
files: list all files Bit has determined are inputs and outputs
deps: show dependency graph
targets: list all targets
ignored: list all loaded ignore patterns (from .gitignore files)
`,
})
Expand All @@ -72,8 +73,8 @@ func main() {

switch {
case cli.List, cli.Describe == "targets":
for _, target := range eng.Outputs() {
fmt.Println(target)
for _, output := range eng.Outputs() {
fmt.Println(output)
}

case cli.Clean:
Expand Down
16 changes: 9 additions & 7 deletions engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ func (e *Engine) analyse(bitfile *parser.Bitfile) error {
target.inputs = &parser.RefList{Pos: entry.Pos}
}
if entry.Outputs == nil {
fmt.Println(entry.Pos)
target.outputs = &parser.RefList{Pos: entry.Pos}
}
logger := e.targetLogger(target)
Expand Down Expand Up @@ -350,6 +349,7 @@ nextTarget:
return nil
}

// Outputs returns the list of explicit outputs.
func (e *Engine) Outputs() []string {
set := map[string]bool{}
for _, target := range e.targets {
Expand Down Expand Up @@ -408,9 +408,6 @@ func (e *Engine) expandOutputs(outputs []string) ([]string, error) {
}

func (e *Engine) build(outputs []string, seen map[string]bool) error {
if len(outputs) == 0 {
outputs = e.Outputs()
}
for _, name := range outputs {
name, err := e.normalisePath(name) //nolint:govet
if err != nil {
Expand Down Expand Up @@ -442,7 +439,7 @@ func (e *Engine) build(outputs []string, seen map[string]bool) error {
}
}

log.Tracef("Building.")
log.Debugf("Building.")

// Build target.
err = target.buildFunc(log, target)
Expand Down Expand Up @@ -675,6 +672,8 @@ func (e *Engine) evaluate() error {
var changed string
if target.storedHash != target.realHash {
changed = " (changed)"
} else {
changed = " (no change)"
}
logger.Tracef("Hash: %016x -> %016x%s", target.storedHash, target.realHash, changed)
}
Expand Down Expand Up @@ -801,8 +800,11 @@ func (e *Engine) getTarget(name string) (*Target, error) {
},
vars: Vars{},
cleanFunc: e.defaultCleanFunc,
buildFunc: func(logger *logging.Logger, target *Target) error { return nil },
chdir: &parser.Ref{Text: "."},
buildFunc: func(logger *logging.Logger, target *Target) error {
logger.Tracef("No-op build for synthetic target")
return nil
},
chdir: &parser.Ref{Text: "."},
}
e.targets = append(e.targets, target)
e.outputs[RefKey(name)] = target
Expand Down
53 changes: 53 additions & 0 deletions engine/internal/eventsource/eventsource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package eventsource

import (
"github.com/alecthomas/atomic"

"github.com/alecthomas/bit/engine/internal/pubsub"
)

// EventSource is a pubsub.Topic that also stores the last published value in an atomic.Value.
//
// Updating the value will result in a publish event.
type EventSource[T any] struct {
*pubsub.Topic[T]
value *atomic.Value[T]
}

var _ atomic.Interface[int] = (*EventSource[int])(nil)

func New[T any]() *EventSource[T] {
var t T
e := &EventSource[T]{Topic: pubsub.New[T](), value: atomic.New(t)}
changes := make(chan T, 64)
e.Subscribe(changes)
go func() {
for value := range changes {
e.value.Store(value)
}
}()
return e
}

func (e *EventSource[T]) Store(value T) {
e.value.Store(value)
e.Publish(value)
}

func (e *EventSource[T]) Load() T {
return e.value.Load()
}

func (e *EventSource[T]) Swap(value T) T {
rv := e.value.Swap(value)
e.Publish(value)
return rv
}

func (e *EventSource[T]) CompareAndSwap(old, new T) bool { //nolint:predeclared
if e.value.CompareAndSwap(old, new) {
e.Publish(new)
return true
}
return false
}
94 changes: 94 additions & 0 deletions engine/internal/pubsub/pubsub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package pubsub

import "fmt"

// Control messages for the topic.
type control[T any] interface{ control() }

type subscribe[T any] chan T

func (subscribe[T]) control() {}

type unsubscribe[T any] chan T

func (unsubscribe[T]) control() {}

type stop struct{}

func (stop) control() {}

type Topic[T any] struct {
publish chan T
control chan control[T]
}

// New creates a new topic that can be used to publish and subscribe to messages.
func New[T any]() *Topic[T] {
s := &Topic[T]{
publish: make(chan T, 64),
control: make(chan control[T]),
}
go s.run()
return s
}

func (s *Topic[T]) Publish(t T) {
s.publish <- t
}

// Subscribe a channel to the topic.
//
// The channel will be closed when the topic is closed.
//
// If "c" is nil a channel will be created.
func (s *Topic[T]) Subscribe(c chan T) chan T {
if c == nil {
c = make(chan T, 16)
}
s.control <- subscribe[T](c)
return c
}

// Unsubscribe a channel from the topic, closing the channel.
func (s *Topic[T]) Unsubscribe(c chan T) {
s.control <- unsubscribe[T](c)
}

// Close the topic, blocking until all subscribers have been closed.
func (s *Topic[T]) Close() error {
s.control <- stop{}
return nil
}

func (s *Topic[T]) run() {
subscriptions := map[chan T]struct{}{}
for {
select {
case msg := <-s.control:
switch msg := msg.(type) {
case subscribe[T]:
subscriptions[msg] = struct{}{}

case unsubscribe[T]:
delete(subscriptions, msg)
close(msg)

case stop:
for ch := range subscriptions {
close(ch)
}
close(s.control)
close(s.publish)
return

default:
panic(fmt.Sprintf("unknown control message: %T", msg))
}

case msg := <-s.publish:
for ch := range subscriptions {
ch <- msg
}
}
}
}
26 changes: 26 additions & 0 deletions engine/internal/pubsub/pubsub_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package pubsub

import (
"testing"
"time"

"github.com/alecthomas/assert/v2"
)

func TestPubsub(t *testing.T) {
pubsub := New[string]()
ch := make(chan string, 64)
pubsub.Subscribe(ch)
pubsub.Publish("hello")
select {
case msg := <-ch:
assert.Equal(t, "hello", msg)

case <-time.After(time.Millisecond * 100):
t.Fail()
}
_ = pubsub.Close()
assert.Panics(t, func() { pubsub.Subscribe(ch) })
assert.Panics(t, func() { pubsub.Unsubscribe(ch) })
assert.Panics(t, func() { pubsub.Publish("hello") })
}
71 changes: 54 additions & 17 deletions engine/logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"sync"
"syscall"

"github.com/alecthomas/bit/engine/internal/eventsource"
"github.com/alecthomas/bit/engine/logging/csi"
"github.com/creack/pty"
"github.com/kballard/go-shellquote"
Expand All @@ -20,8 +21,8 @@ import (

type LogConfig struct {
Level LogLevel `help:"Log level (${enum})." enum:"trace,debug,info,notice,warn,error" default:"info"`
Debug bool `help:"Enable debug mode." xor:"trace"`
Trace bool `help:"Enable trace mode." xor:"trace"`
Debug bool `help:"Force debug logging." xor:"level"`
Trace bool `help:"Force trace logging." xor:"level"`
}

type LogLevel int
Expand Down Expand Up @@ -74,9 +75,14 @@ func (l *LogLevel) UnmarshalText(text []byte) error {
return nil
}

type terminalSize struct {
margin, width, height uint16
}

type Logger struct {
level LogLevel
scope string
size *eventsource.EventSource[terminalSize]
}

func NewLogger(config LogConfig) *Logger {
Expand All @@ -86,15 +92,24 @@ func NewLogger(config LogConfig) *Logger {
} else if config.Debug {
level = LogLevelDebug
}
return &Logger{level: level}
logger := &Logger{
level: level,
size: eventsource.New[terminalSize](),
}
logger.syncTermSize()
return logger
}

// Scope returns a new logger with the given scope.
func (l *Logger) Scope(scope string) *Logger {
if len(scope) > 16 {
scope = "…" + scope[len(scope)-15:]
// Margin is 20% of terminal.
size := l.size.Load()
margin := int(size.margin)
if len(scope) > margin {
scope = "…" + scope[len(scope)-margin+1:]
} else {
scope += strings.Repeat(" ", margin-len(scope))
}
scope = fmt.Sprintf("%-16s", scope)
scope = strings.ReplaceAll(scope, "%", "%%")
return &Logger{scope: scope, level: l.level}
}
Expand Down Expand Up @@ -184,20 +199,19 @@ func (l *Logger) Exec(dir, command string) error {
return err
}

winch := make(chan os.Signal, 1)
signal.Notify(winch, syscall.SIGWINCH)
defer signal.Stop(winch)
go func() {
for range winch {
if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil {
_ = pty.Setsize(p, &pty.Winsize{Rows: uint16(h), Cols: uint16(w) - 17})
}
}
}()
changes := l.size.Subscribe(nil)
defer l.size.Unsubscribe(changes)

// Resize the PTY to exclude the margin.
if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil {
_ = pty.Setsize(p, &pty.Winsize{Rows: uint16(h), Cols: uint16(w) - 17})
_ = pty.Setsize(p, &pty.Winsize{Rows: uint16(h), Cols: uint16(w) - (l.size.Load().margin + 1)})
}

go func() {
for size := range changes {
_ = pty.Setsize(p, &pty.Winsize{Rows: size.height, Cols: size.width - (size.margin + 1)})
}
}()
defer t.Close()
defer p.Close()
lw := l.WriterAt(LogLevelInfo)
Expand Down Expand Up @@ -318,3 +332,26 @@ func (l *Logger) writerScanner(wg *sync.WaitGroup, r *io.PipeReader, level LogLe
}
}
}

func (l *Logger) syncTermSize() {
// Initialise terminal size.
size := terminalSize{margin: 16, width: 80, height: 25}
if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil {
margin := uint16(max(16, w/5))
size = terminalSize{margin: margin, width: uint16(w), height: uint16(h)}
}
l.size.Store(size)

// Watch WINCH for changes.
winch := make(chan os.Signal, 1)
signal.Notify(winch, syscall.SIGWINCH)
defer signal.Stop(winch)
go func() {
for range winch {
if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil {
margin := uint16(max(16, w/5))
l.size.Store(terminalSize{margin: margin, width: uint16(w), height: uint16(h)})
}
}
}()
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module github.com/alecthomas/bit
go 1.21.6

require (
github.com/alecthomas/assert/v2 v2.4.1
github.com/alecthomas/assert/v2 v2.4.0
github.com/alecthomas/atomic v0.1.0-alpha2
github.com/alecthomas/kong v0.8.1
github.com/alecthomas/participle/v2 v2.1.1
github.com/alecthomas/repr v0.3.0
Expand Down

0 comments on commit a83fbec

Please sign in to comment.