Skip to content

Commit a83fbec

Browse files
committed
feat: dynamically expand margin up to 20% of terminal size
1 parent 44434f4 commit a83fbec

File tree

10 files changed

+261
-59
lines changed

10 files changed

+261
-59
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
*.o
2-
build/*
3-
2+
build/
43
dist/

cmd/bit/main.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,11 @@ func main() {
4646
}, kong.Vars{
4747
"version": version,
4848
"describe_help": `Where ASPECT is one of:
49-
files: list all files Bit has determined are inputs and outputs
50-
deps: show dependency graph
51-
targets: list all targets
52-
ignored: list all loaded ignore patterns (from .gitignore files)
49+
50+
files: list all files Bit has determined are inputs and outputs
51+
deps: show dependency graph
52+
targets: list all targets
53+
ignored: list all loaded ignore patterns (from .gitignore files)
5354
5455
`,
5556
})
@@ -72,8 +73,8 @@ func main() {
7273

7374
switch {
7475
case cli.List, cli.Describe == "targets":
75-
for _, target := range eng.Outputs() {
76-
fmt.Println(target)
76+
for _, output := range eng.Outputs() {
77+
fmt.Println(output)
7778
}
7879

7980
case cli.Clean:

engine/engine.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ func (e *Engine) analyse(bitfile *parser.Bitfile) error {
135135
target.inputs = &parser.RefList{Pos: entry.Pos}
136136
}
137137
if entry.Outputs == nil {
138-
fmt.Println(entry.Pos)
139138
target.outputs = &parser.RefList{Pos: entry.Pos}
140139
}
141140
logger := e.targetLogger(target)
@@ -350,6 +349,7 @@ nextTarget:
350349
return nil
351350
}
352351

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

410410
func (e *Engine) build(outputs []string, seen map[string]bool) error {
411-
if len(outputs) == 0 {
412-
outputs = e.Outputs()
413-
}
414411
for _, name := range outputs {
415412
name, err := e.normalisePath(name) //nolint:govet
416413
if err != nil {
@@ -442,7 +439,7 @@ func (e *Engine) build(outputs []string, seen map[string]bool) error {
442439
}
443440
}
444441

445-
log.Tracef("Building.")
442+
log.Debugf("Building.")
446443

447444
// Build target.
448445
err = target.buildFunc(log, target)
@@ -675,6 +672,8 @@ func (e *Engine) evaluate() error {
675672
var changed string
676673
if target.storedHash != target.realHash {
677674
changed = " (changed)"
675+
} else {
676+
changed = " (no change)"
678677
}
679678
logger.Tracef("Hash: %016x -> %016x%s", target.storedHash, target.realHash, changed)
680679
}
@@ -801,8 +800,11 @@ func (e *Engine) getTarget(name string) (*Target, error) {
801800
},
802801
vars: Vars{},
803802
cleanFunc: e.defaultCleanFunc,
804-
buildFunc: func(logger *logging.Logger, target *Target) error { return nil },
805-
chdir: &parser.Ref{Text: "."},
803+
buildFunc: func(logger *logging.Logger, target *Target) error {
804+
logger.Tracef("No-op build for synthetic target")
805+
return nil
806+
},
807+
chdir: &parser.Ref{Text: "."},
806808
}
807809
e.targets = append(e.targets, target)
808810
e.outputs[RefKey(name)] = target
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package eventsource
2+
3+
import (
4+
"github.com/alecthomas/atomic"
5+
6+
"github.com/alecthomas/bit/engine/internal/pubsub"
7+
)
8+
9+
// EventSource is a pubsub.Topic that also stores the last published value in an atomic.Value.
10+
//
11+
// Updating the value will result in a publish event.
12+
type EventSource[T any] struct {
13+
*pubsub.Topic[T]
14+
value *atomic.Value[T]
15+
}
16+
17+
var _ atomic.Interface[int] = (*EventSource[int])(nil)
18+
19+
func New[T any]() *EventSource[T] {
20+
var t T
21+
e := &EventSource[T]{Topic: pubsub.New[T](), value: atomic.New(t)}
22+
changes := make(chan T, 64)
23+
e.Subscribe(changes)
24+
go func() {
25+
for value := range changes {
26+
e.value.Store(value)
27+
}
28+
}()
29+
return e
30+
}
31+
32+
func (e *EventSource[T]) Store(value T) {
33+
e.value.Store(value)
34+
e.Publish(value)
35+
}
36+
37+
func (e *EventSource[T]) Load() T {
38+
return e.value.Load()
39+
}
40+
41+
func (e *EventSource[T]) Swap(value T) T {
42+
rv := e.value.Swap(value)
43+
e.Publish(value)
44+
return rv
45+
}
46+
47+
func (e *EventSource[T]) CompareAndSwap(old, new T) bool { //nolint:predeclared
48+
if e.value.CompareAndSwap(old, new) {
49+
e.Publish(new)
50+
return true
51+
}
52+
return false
53+
}

engine/internal/pubsub/pubsub.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package pubsub
2+
3+
import "fmt"
4+
5+
// Control messages for the topic.
6+
type control[T any] interface{ control() }
7+
8+
type subscribe[T any] chan T
9+
10+
func (subscribe[T]) control() {}
11+
12+
type unsubscribe[T any] chan T
13+
14+
func (unsubscribe[T]) control() {}
15+
16+
type stop struct{}
17+
18+
func (stop) control() {}
19+
20+
type Topic[T any] struct {
21+
publish chan T
22+
control chan control[T]
23+
}
24+
25+
// New creates a new topic that can be used to publish and subscribe to messages.
26+
func New[T any]() *Topic[T] {
27+
s := &Topic[T]{
28+
publish: make(chan T, 64),
29+
control: make(chan control[T]),
30+
}
31+
go s.run()
32+
return s
33+
}
34+
35+
func (s *Topic[T]) Publish(t T) {
36+
s.publish <- t
37+
}
38+
39+
// Subscribe a channel to the topic.
40+
//
41+
// The channel will be closed when the topic is closed.
42+
//
43+
// If "c" is nil a channel will be created.
44+
func (s *Topic[T]) Subscribe(c chan T) chan T {
45+
if c == nil {
46+
c = make(chan T, 16)
47+
}
48+
s.control <- subscribe[T](c)
49+
return c
50+
}
51+
52+
// Unsubscribe a channel from the topic, closing the channel.
53+
func (s *Topic[T]) Unsubscribe(c chan T) {
54+
s.control <- unsubscribe[T](c)
55+
}
56+
57+
// Close the topic, blocking until all subscribers have been closed.
58+
func (s *Topic[T]) Close() error {
59+
s.control <- stop{}
60+
return nil
61+
}
62+
63+
func (s *Topic[T]) run() {
64+
subscriptions := map[chan T]struct{}{}
65+
for {
66+
select {
67+
case msg := <-s.control:
68+
switch msg := msg.(type) {
69+
case subscribe[T]:
70+
subscriptions[msg] = struct{}{}
71+
72+
case unsubscribe[T]:
73+
delete(subscriptions, msg)
74+
close(msg)
75+
76+
case stop:
77+
for ch := range subscriptions {
78+
close(ch)
79+
}
80+
close(s.control)
81+
close(s.publish)
82+
return
83+
84+
default:
85+
panic(fmt.Sprintf("unknown control message: %T", msg))
86+
}
87+
88+
case msg := <-s.publish:
89+
for ch := range subscriptions {
90+
ch <- msg
91+
}
92+
}
93+
}
94+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package pubsub
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/alecthomas/assert/v2"
8+
)
9+
10+
func TestPubsub(t *testing.T) {
11+
pubsub := New[string]()
12+
ch := make(chan string, 64)
13+
pubsub.Subscribe(ch)
14+
pubsub.Publish("hello")
15+
select {
16+
case msg := <-ch:
17+
assert.Equal(t, "hello", msg)
18+
19+
case <-time.After(time.Millisecond * 100):
20+
t.Fail()
21+
}
22+
_ = pubsub.Close()
23+
assert.Panics(t, func() { pubsub.Subscribe(ch) })
24+
assert.Panics(t, func() { pubsub.Unsubscribe(ch) })
25+
assert.Panics(t, func() { pubsub.Publish("hello") })
26+
}

engine/logging/logger.go

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"sync"
1212
"syscall"
1313

14+
"github.com/alecthomas/bit/engine/internal/eventsource"
1415
"github.com/alecthomas/bit/engine/logging/csi"
1516
"github.com/creack/pty"
1617
"github.com/kballard/go-shellquote"
@@ -20,8 +21,8 @@ import (
2021

2122
type LogConfig struct {
2223
Level LogLevel `help:"Log level (${enum})." enum:"trace,debug,info,notice,warn,error" default:"info"`
23-
Debug bool `help:"Enable debug mode." xor:"trace"`
24-
Trace bool `help:"Enable trace mode." xor:"trace"`
24+
Debug bool `help:"Force debug logging." xor:"level"`
25+
Trace bool `help:"Force trace logging." xor:"level"`
2526
}
2627

2728
type LogLevel int
@@ -74,9 +75,14 @@ func (l *LogLevel) UnmarshalText(text []byte) error {
7475
return nil
7576
}
7677

78+
type terminalSize struct {
79+
margin, width, height uint16
80+
}
81+
7782
type Logger struct {
7883
level LogLevel
7984
scope string
85+
size *eventsource.EventSource[terminalSize]
8086
}
8187

8288
func NewLogger(config LogConfig) *Logger {
@@ -86,15 +92,24 @@ func NewLogger(config LogConfig) *Logger {
8692
} else if config.Debug {
8793
level = LogLevelDebug
8894
}
89-
return &Logger{level: level}
95+
logger := &Logger{
96+
level: level,
97+
size: eventsource.New[terminalSize](),
98+
}
99+
logger.syncTermSize()
100+
return logger
90101
}
91102

92103
// Scope returns a new logger with the given scope.
93104
func (l *Logger) Scope(scope string) *Logger {
94-
if len(scope) > 16 {
95-
scope = "…" + scope[len(scope)-15:]
105+
// Margin is 20% of terminal.
106+
size := l.size.Load()
107+
margin := int(size.margin)
108+
if len(scope) > margin {
109+
scope = "…" + scope[len(scope)-margin+1:]
110+
} else {
111+
scope += strings.Repeat(" ", margin-len(scope))
96112
}
97-
scope = fmt.Sprintf("%-16s", scope)
98113
scope = strings.ReplaceAll(scope, "%", "%%")
99114
return &Logger{scope: scope, level: l.level}
100115
}
@@ -184,20 +199,19 @@ func (l *Logger) Exec(dir, command string) error {
184199
return err
185200
}
186201

187-
winch := make(chan os.Signal, 1)
188-
signal.Notify(winch, syscall.SIGWINCH)
189-
defer signal.Stop(winch)
190-
go func() {
191-
for range winch {
192-
if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil {
193-
_ = pty.Setsize(p, &pty.Winsize{Rows: uint16(h), Cols: uint16(w) - 17})
194-
}
195-
}
196-
}()
202+
changes := l.size.Subscribe(nil)
203+
defer l.size.Unsubscribe(changes)
204+
197205
// Resize the PTY to exclude the margin.
198206
if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil {
199-
_ = pty.Setsize(p, &pty.Winsize{Rows: uint16(h), Cols: uint16(w) - 17})
207+
_ = pty.Setsize(p, &pty.Winsize{Rows: uint16(h), Cols: uint16(w) - (l.size.Load().margin + 1)})
200208
}
209+
210+
go func() {
211+
for size := range changes {
212+
_ = pty.Setsize(p, &pty.Winsize{Rows: size.height, Cols: size.width - (size.margin + 1)})
213+
}
214+
}()
201215
defer t.Close()
202216
defer p.Close()
203217
lw := l.WriterAt(LogLevelInfo)
@@ -318,3 +332,26 @@ func (l *Logger) writerScanner(wg *sync.WaitGroup, r *io.PipeReader, level LogLe
318332
}
319333
}
320334
}
335+
336+
func (l *Logger) syncTermSize() {
337+
// Initialise terminal size.
338+
size := terminalSize{margin: 16, width: 80, height: 25}
339+
if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil {
340+
margin := uint16(max(16, w/5))
341+
size = terminalSize{margin: margin, width: uint16(w), height: uint16(h)}
342+
}
343+
l.size.Store(size)
344+
345+
// Watch WINCH for changes.
346+
winch := make(chan os.Signal, 1)
347+
signal.Notify(winch, syscall.SIGWINCH)
348+
defer signal.Stop(winch)
349+
go func() {
350+
for range winch {
351+
if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil {
352+
margin := uint16(max(16, w/5))
353+
l.size.Store(terminalSize{margin: margin, width: uint16(w), height: uint16(h)})
354+
}
355+
}
356+
}()
357+
}

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ module github.com/alecthomas/bit
33
go 1.21.6
44

55
require (
6-
github.com/alecthomas/assert/v2 v2.4.1
6+
github.com/alecthomas/assert/v2 v2.4.0
7+
github.com/alecthomas/atomic v0.1.0-alpha2
78
github.com/alecthomas/kong v0.8.1
89
github.com/alecthomas/participle/v2 v2.1.1
910
github.com/alecthomas/repr v0.3.0

0 commit comments

Comments
 (0)