Compare commits

...

30 Commits

Author SHA1 Message Date
45278fbb8b Fix bug where nil cells would sometimes render without styling 2022-11-29 02:53:36 -05:00
ef5a811140 Loading of multiple fonts for bold and italic 2022-11-29 02:36:24 -05:00
e753fc11ca Implemented awful fallback bold and italic 2022-11-29 02:12:30 -05:00
e4c7dcb2e1 Add config items for bold, italic, and bold italic fonts 2022-11-29 01:38:42 -05:00
f51f9ae5c5 Implemented highlight and underline styles 2022-11-29 01:28:54 -05:00
0462afdf11 Updated the style example with more stuff 2022-11-29 00:50:23 -05:00
2fa4cc8da4 Created a style test (that does nothing as of now) 2022-11-29 00:26:26 -05:00
5ea5a302bf Haha oops 2022-11-27 00:16:10 -05:00
1435c02354 Add more xdg stuff 2022-11-26 23:47:59 -05:00
19895e6049 Added a configuration viewer example 2022-11-26 22:49:58 -05:00
73ae475a7d Fixed some bugs related to saving conf files 2022-11-26 22:49:02 -05:00
639e43cfa7 Config file fixes 2022-11-26 22:36:16 -05:00
46b2ca3d43 Added a way to save configuration files 2022-11-26 22:10:22 -05:00
3cfe8be7bb Publicize the raw io.Reader reading function 2022-11-26 21:37:26 -05:00
863e415310 Made thos errors better 2022-11-26 21:32:05 -05:00
05ddfef584 Added some documentation on how configuration files should be laid out 2022-11-26 21:04:00 -05:00
e60a990d10 Use XDG directories, and respect corresponding environment vars 2022-11-26 20:52:30 -05:00
a42dd60a16 Added a configuration system 2022-11-26 20:28:32 -05:00
a6e4ed9934 Rename unicode.go to encoding.go 2022-11-25 13:35:11 -05:00
9d2872f256 Support mode shift modifier
The code has also been reorganized and cleaned up a bit, with more
comments added.
2022-11-25 13:33:28 -05:00
e588d7d791 Modifier states returned from x backend should be 100% correct now 2022-11-24 22:16:22 -05:00
941a78eaf1 THE DRAGON HAS BEEN SLAIN
Numlock is fully supported, as well as shift lock. Of course, I
cannot properly test shift lock or caps lock because I have neither
of those things, but I assume they work as well as num lock does.
2022-11-24 22:02:32 -05:00
33ed2af075 We now take into account keypad keys
However, num lock is not accounted for. This still needs to be
implemented.
2022-11-24 18:20:47 -05:00
5a76bd0c22 Fix bug with keyboard input 2022-11-23 20:34:05 -05:00
Sasha Koshka
ae514f5ae2 Add proper and reliable (i hope) support for modifier keys 2022-11-22 00:21:35 -05:00
Sasha Koshka
8c28c57925 Support for meta and hyper keys added
Support for the compose key has also been added but it's just the
button code for now, no support for actually composing stuff.
There are plans for that in a fixme.
2022-11-21 23:43:22 -05:00
9a37fbf04a Updated examples and added more documentation 2022-11-19 18:00:47 -05:00
9a8bb85afc Replaced orange with a dim/grey color 2022-11-18 19:46:50 -05:00
f55f98651f It needed more blackjack and hookers to work correctly 2022-11-17 23:16:47 -05:00
3cb0ac64fc I made my own draw method with blackjack and hookers 2022-11-17 21:19:23 -05:00
20 changed files with 1520 additions and 437 deletions

View File

@ -33,42 +33,60 @@ func (application *Application) Run () (
return return
} }
// OnQuit registers an event handler to be called just before the application
// quits. This can happen when the user closes the application, or the backend
// experiences an unrecoverable error.
func (application *Application) OnQuit ( func (application *Application) OnQuit (
onQuit func (), onQuit func (),
) { ) {
application.callbackManager.onQuit = onQuit application.callbackManager.onQuit = onQuit
} }
// OnPress registers an event handler to be called when a key or mouse button
// is pressed.
func (application *Application) OnPress ( func (application *Application) OnPress (
onPress func (button Button), onPress func (button Button, modifiers Modifiers),
) { ) {
application.callbackManager.onPress = onPress application.callbackManager.onPress = onPress
} }
// OnPress registers an event handler to be called when a key or mouse button
// is released.
func (application *Application) OnRelease ( func (application *Application) OnRelease (
onRelease func (button Button), onRelease func (button Button),
) { ) {
application.callbackManager.onRelease = onRelease application.callbackManager.onRelease = onRelease
} }
// OnResize registers an event handler to be called when the application window
// is resized. After the event handler is called, any updates it makes will
// automatically be pushed to the screen.
func (application *Application) OnResize ( func (application *Application) OnResize (
onResize func (), onResize func (),
) { ) {
application.callbackManager.onResize = onResize application.callbackManager.onResize = onResize
} }
// OnMouseMove registers an event handler to be called when mouse motion is
// detected. The coordinates of the cell that the mouse now hovers over are
// given as input.
func (application *Application) OnMouseMove ( func (application *Application) OnMouseMove (
onMouseMove func (x, y int), onMouseMove func (x, y int),
) { ) {
application.callbackManager.onMouseMove = onMouseMove application.callbackManager.onMouseMove = onMouseMove
} }
// OnScroll registers an event handler to be called when the user uses the mouse
// scroll wheel. Horizontal and vertical amounts are given as input.
func (application *Application) OnScroll ( func (application *Application) OnScroll (
onScroll func (x, y int), onScroll func (x, y int),
) { ) {
application.callbackManager.onScroll = onScroll application.callbackManager.onScroll = onScroll
} }
// OnStart registers an event handler to be called once when the application
// starts, right before the first time updates are pushed to the screen.
// Anything done in here will be the first thing to appear on screen.
func (application *Application) OnStart ( func (application *Application) OnStart (
onStart func (), onStart func (),
) { ) {

View File

@ -3,13 +3,51 @@ package stone
import "image" import "image"
import "errors" import "errors"
// Backend represents a backend for stone. Backends can be registered for use
// with the RegisterBackend() function. All of the below methods MUST be thread
// safe!
type Backend interface { type Backend interface {
Run () // Run is the backend's event loop. It must cleanly exit when the user
// closes the window, but not before calling the OnQuit event. Run
// must call event handlers within its own event loop in a
// non-concurrent fashion.
//
// The OnStart event handler must run after the backend has been fully
// initialized, and right before updates are first pushed to the screen.
// Whatever the application draws from within this event handler must be
// the first thing that appears on-screen.
//
// The OnResize event handler must run whenever the window is resized.
// The backend must push updates to the screen after OnResize has been
// run.
//
// The backend must not push updates to the screen in any other case,
// except when its Draw() method is specifically called.
//
// The OnPress, OnRelease, OnMouseMove, and OnMouseScroll events are to
// be called when such events happen. It is reccommended to compress
// resize, mouse move, and mouse scroll events whenever possible to
// reduce the likelihood of event buildup.
Run ()
// SetTitle sets the application title. This will most often be the
// window title. This method may not always produce an effect, depending
// on the backend.
SetTitle (title string) (err error) SetTitle (title string) (err error)
SetIcon (icons []image.Image) (err error)
Draw () // SetIcon takes in a set of images of different sizes and sets the
// window's icon to them. This method may not always produce an effect,
// depending on the backend.
SetIcon (icons []image.Image) (err error)
// Draw pushes all updates made to the application's buffer to the
// screen.
Draw ()
} }
// BackendFactory must completely initialize a backend, and return it. If
// anything goes wrong, it must stop, clean up any resources and return an
// error so another backend can be chosen.
type BackendFactory func ( type BackendFactory func (
application *Application, application *Application,
callbackManager *CallbackManager, callbackManager *CallbackManager,
@ -20,6 +58,7 @@ type BackendFactory func (
var factories []BackendFactory var factories []BackendFactory
// RegisterBackend registers a backend factory.
func RegisterBackend (factory BackendFactory) { func RegisterBackend (factory BackendFactory) {
factories = append(factories, factory) factories = append(factories, factory)
} }

View File

@ -49,11 +49,25 @@ func (backend *Backend) drawCells (forceRedraw bool) (areas []image.Rectangle) {
cell := backend.application.GetForRendering(x, y) cell := backend.application.GetForRendering(x, y)
content := cell.Rune() content := cell.Rune()
style := cell.Style()
if forceRedraw && content < 32 { continue } if
forceRedraw &&
content < 32 &&
style & (
stone.StyleHighlight |
stone.StyleUnderline) == 0 {
continue
}
areas = append(areas, backend.boundsOfCell(x, y)) areas = append(areas, backend.boundsOfCell(x, y))
backend.drawRune(x, y, content, cell.Color(), !forceRedraw) backend.drawRune (
x, y,
content,
cell.Color(),
cell.Style(),
!forceRedraw)
}} }}
if backend.drawBufferBounds && forceRedraw { if backend.drawBufferBounds && forceRedraw {
@ -74,63 +88,206 @@ func (backend *Backend) drawRune (
x, y int, x, y int,
character rune, character rune,
runeColor stone.Color, runeColor stone.Color,
runeStyle stone.Style,
drawBackground bool, drawBackground bool,
) { ) {
// TODO: cache these draws as non-transparent buffers with the // TODO: cache these draws as non-transparent buffers with the
// application background color as the background. that way, we won't // application background color as the background. that way, we won't
// need to redraw the characters *or* composite them. // need to redraw the characters *or* composite them.
if drawBackground { face := backend.font.normal
fillRectangle (
&image.Uniform {
C: backend.config.Color(stone.ColorBackground),
},
backend.canvas,
backend.boundsOfCell(x, y))
}
if character < 32 { return } highlight := runeStyle & stone.StyleHighlight > 0
bold := runeStyle & stone.StyleBold > 0
italic := runeStyle & stone.StyleItalic > 0
boldTransform := false
italicTransform := false
switch {
case bold && italic:
if backend.font.boldItalic == nil {
switch {
case
backend.font.bold == nil && backend.font.italic != nil,
backend.font.bold != nil && backend.font.italic != nil:
boldTransform = true
face = backend.font.italic
case backend.font.italic == nil && backend.font.bold != nil:
italicTransform = true
face = backend.font.bold
default:
boldTransform = true
italicTransform = true
}
} else {
face = backend.font.boldItalic
}
case bold:
if backend.font.bold == nil {
boldTransform = true
} else {
face = backend.font.bold
}
case italic:
if backend.font.italic == nil {
italicTransform = true
} else {
face = backend.font.italic
}
}
var background xgraphics.BGRA
var foreground xgraphics.BGRA
if highlight {
background = backend.colors[runeColor]
foreground = backend.colors[stone.ColorBackground]
} else {
background = backend.colors[stone.ColorBackground]
foreground = backend.colors[runeColor]
}
if drawBackground || highlight {
fillRectangle (
&image.Uniform { C: background },
backend.canvas,
backend.boundsOfCell(x, y))
}
origin := backend.originOfCell(x, y + 1) origin := backend.originOfCell(x, y + 1)
destinationRectangle, mask, maskPoint, _, ok := backend.font.face.Glyph (
fixed.Point26_6 {
X: fixed.I(origin.X),
Y: fixed.I(origin.Y - backend.metrics.descent),
},
character)
if !ok { if character >= 32 {
println("warning") destinationRectangle, mask, maskPoint, _, ok := face.Glyph (
strokeRectangle ( fixed.Point26_6 {
&image.Uniform { X: fixed.I(origin.X),
C: backend.config.Color(stone.ColorForeground), Y: fixed.I(origin.Y),
}, },
backend.canvas, character)
backend.boundsOfCell(x, y))
return if !ok {
strokeRectangle (
&image.Uniform { C: foreground },
backend.canvas,
backend.boundsOfCell(x, y))
return
}
if backend.drawCellBounds {
strokeRectangle (
&image.Uniform { C: foreground },
backend.canvas,
backend.boundsOfCell(x, y))
}
// alphaMask, isAlpha := mask.(*image.Alpha)
// if isAlpha {
// backend.sprayRuneMaskAlpha (
// alphaMask, destinationRectangle,
// maskPoint, foreground, background)
// } else {
backend.sprayRuneMask (
mask, destinationRectangle,
maskPoint, foreground, background,
italicTransform, boldTransform)
// }
} }
if backend.drawCellBounds { // underline
strokeRectangle ( if runeStyle & stone.StyleUnderline > 0 {
&image.Uniform { maxX := origin.X + backend.metrics.cellWidth
C: backend.config.Color(stone.ColorForeground), y :=
}, origin.Y -
backend.canvas, backend.metrics.descent
backend.boundsOfCell(x, y)) for x := origin.X; x < maxX; x ++ {
backend.canvas.SetBGRA(x, y, foreground)
}
} }
draw.DrawMask (
backend.canvas,
destinationRectangle,
&image.Uniform {
C: backend.config.Color(runeColor),
},
image.Point { },
mask,
maskPoint,
draw.Over)
} }
func (backend *Backend) sprayRuneMask (
mask image.Image,
bounds image.Rectangle,
maskPoint image.Point,
fill xgraphics.BGRA,
background xgraphics.BGRA,
italic bool,
bold bool,
) {
maxX := bounds.Max.X - bounds.Min.X
maxY := bounds.Max.Y - bounds.Min.Y
for y := 0; y < maxY; y ++ {
var previousAlpha uint32
offset := 0
if italic {
offset = (maxY - y) / 4
}
for x := 0; x < maxX; x ++ {
_, _, _,
alpha := mask.At(x + maskPoint.X, y + maskPoint.Y).RGBA()
currentAlpha := alpha
if bold && previousAlpha > alpha {
alpha = previousAlpha
}
backend.canvas.SetBGRA (
x + bounds.Min.X + offset,
y + bounds.Min.Y - backend.metrics.descent,
xgraphics.BlendBGRA (
background,
xgraphics.BGRA {
R: fill.R,
G: fill.G,
B: fill.B,
A: uint8(alpha >> 8),
}))
previousAlpha = currentAlpha
}
if bold {
backend.canvas.SetBGRA (
bounds.Max.X + offset,
y + bounds.Min.Y - backend.metrics.descent,
xgraphics.BlendBGRA (
background,
xgraphics.BGRA {
R: fill.R,
G: fill.G,
B: fill.B,
A: uint8(previousAlpha >> 8),
}))
}
}
}
// func (backend *Backend) sprayRuneMaskAlpha (
// mask *image.Alpha,
// bounds image.Rectangle,
// maskPoint image.Point,
// fill xgraphics.BGRA,
// background xgraphics.BGRA,
// ) {
// maxX := bounds.Max.X - bounds.Min.X
// maxY := bounds.Max.Y - bounds.Min.Y
//
// for y := 0; y < maxY; y ++ {
// for x := 0; x < maxX; x ++ {
// alpha := mask.AlphaAt(x + maskPoint.X, y + maskPoint.Y).A
// backend.canvas.SetBGRA (
// x + bounds.Min.X,
// y + bounds.Min.Y - backend.metrics.descent,
// xgraphics.BlendBGRA (
// background,
// xgraphics.BGRA {
// R: fill.R,
// G: fill.G,
// B: fill.B,
// A: alpha,
// }))
// }}
// }
func fillRectangle ( func fillRectangle (
source image.Image, source image.Image,
destination draw.Image, destination draw.Image,

339
backends/x/encoding.go Normal file
View File

@ -0,0 +1,339 @@
package x
import "unicode"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/keybind"
import "git.tebibyte.media/sashakoshka/stone"
// when making changes to this file, look at keysymdef.h and
// https://tronche.com/gui/x/xlib/input/keyboard-encoding.html
var buttonCodeTable = map[xproto.Keysym] stone.Button {
0xFFFFFF: stone.ButtonUnknown,
0xFF63: stone.KeyInsert,
0xFF67: stone.KeyMenu,
0xFF61: stone.KeyPrintScreen,
0xFF6B: stone.KeyPause,
0xFFE5: stone.KeyCapsLock,
0xFF14: stone.KeyScrollLock,
0xFF7F: stone.KeyNumLock,
0xFF08: stone.KeyBackspace,
0xFF09: stone.KeyTab,
0xFF0D: stone.KeyEnter,
0xFF1B: stone.KeyEscape,
0xFF52: stone.KeyUp,
0xFF54: stone.KeyDown,
0xFF51: stone.KeyLeft,
0xFF53: stone.KeyRight,
0xFF55: stone.KeyPageUp,
0xFF56: stone.KeyPageDown,
0xFF50: stone.KeyHome,
0xFF57: stone.KeyEnd,
0xFFE1: stone.KeyLeftShift,
0xFFE2: stone.KeyRightShift,
0xFFE3: stone.KeyLeftControl,
0xFFE4: stone.KeyRightControl,
0xFFE7: stone.KeyLeftMeta,
0xFFE8: stone.KeyRightMeta,
0xFFE9: stone.KeyLeftAlt,
0xFFEA: stone.KeyRightAlt,
0xFFEB: stone.KeyLeftSuper,
0xFFEC: stone.KeyRightSuper,
0xFFED: stone.KeyLeftHyper,
0xFFEE: stone.KeyRightHyper,
0xFFFF: stone.KeyDelete,
0xFFBE: stone.KeyF1,
0xFFBF: stone.KeyF2,
0xFFC0: stone.KeyF3,
0xFFC1: stone.KeyF4,
0xFFC2: stone.KeyF5,
0xFFC3: stone.KeyF6,
0xFFC4: stone.KeyF7,
0xFFC5: stone.KeyF8,
0xFFC6: stone.KeyF9,
0xFFC7: stone.KeyF10,
0xFFC8: stone.KeyF11,
0xFFC9: stone.KeyF12,
// TODO: send this whenever a compose key, dead key, etc is pressed,
// and then send the resulting character while witholding the key
// presses that were used to compose it. As far as the program is
// concerned, a magical key with the final character was pressed and the
// KeyDead key is just so that the program might provide some visual
// feedback to the user while input is being waited for.
0xFF20: stone.KeyDead,
}
var keypadCodeTable = map[xproto.Keysym] stone.Button {
0xff80: stone.Button(' '),
0xff89: stone.KeyTab,
0xff8d: stone.KeyEnter,
0xff91: stone.KeyF1,
0xff92: stone.KeyF2,
0xff93: stone.KeyF3,
0xff94: stone.KeyF4,
0xff95: stone.KeyHome,
0xff96: stone.KeyLeft,
0xff97: stone.KeyUp,
0xff98: stone.KeyRight,
0xff99: stone.KeyDown,
0xff9a: stone.KeyPageUp,
0xff9b: stone.KeyPageDown,
0xff9c: stone.KeyEnd,
0xff9d: stone.KeyHome,
0xff9e: stone.KeyInsert,
0xff9f: stone.KeyDelete,
0xffbd: stone.Button('='),
0xffaa: stone.Button('*'),
0xffab: stone.Button('+'),
0xffac: stone.Button(','),
0xffad: stone.Button('-'),
0xffae: stone.Button('.'),
0xffaf: stone.Button('/'),
0xffb0: stone.Button('0'),
0xffb1: stone.Button('1'),
0xffb2: stone.Button('2'),
0xffb3: stone.Button('3'),
0xffb4: stone.Button('4'),
0xffb5: stone.Button('5'),
0xffb6: stone.Button('6'),
0xffb7: stone.Button('7'),
0xffb8: stone.Button('8'),
0xffb9: stone.Button('9'),
}
// keycodeToButton converts an X keycode to a stone button code. It implements
// a more fleshed out version of some of the logic found in
// xgbutil/keybind/encoding.go to get a full keycode to keysym conversion, but
// eliminates redundant work by going straight to a button code.
func (backend *Backend) keycodeToButton (
keycode xproto.Keycode,
state uint16,
) (
button stone.Button,
numberPad bool,
) {
// PARAGRAPH 3
//
// A list of KeySyms is associated with each KeyCode. The list is
// intended to convey the set of symbols on the corresponding key. If
// the list (ignoring trailing NoSymbol entries) is a single KeySym
// ``K'', then the list is treated as if it were the list ``K NoSymbol
// K NoSymbol''. If the list (ignoring trailing NoSymbol entries) is a
// pair of KeySyms ``K1 K2'', then the list is treated as if it were the
// list ``K1 K2 K1 K2''. If the list (ignoring trailing NoSymbol
// entries) is a triple of KeySyms ``K1 K2 K3'', then the list is
// treated as if it were the list ``K1 K2 K3 NoSymbol''. When an
// explicit ``void'' element is desired in the list, the value
// VoidSymbol can be used.
symbol1 := keybind.KeysymGet(backend.connection, keycode, 0)
symbol2 := keybind.KeysymGet(backend.connection, keycode, 1)
symbol3 := keybind.KeysymGet(backend.connection, keycode, 2)
symbol4 := keybind.KeysymGet(backend.connection, keycode, 3)
switch {
case symbol2 == 0 && symbol3 == 0 && symbol4 == 0:
symbol3 = symbol1
case symbol3 == 0 && symbol4 == 0:
symbol3 = symbol1
symbol4 = symbol2
case symbol4 == 0:
symbol4 = 0
}
symbol1Rune := keysymToRune(symbol1)
symbol2Rune := keysymToRune(symbol2)
symbol3Rune := keysymToRune(symbol3)
symbol4Rune := keysymToRune(symbol4)
// PARAGRAPH 4
//
// The first four elements of the list are split into two groups of
// KeySyms. Group 1 contains the first and second KeySyms; Group 2
// contains the third and fourth KeySyms. Within each group, if the
// second element of the group is NoSymbol , then the group should be
// treated as if the second element were the same as the first element,
// except when the first element is an alphabetic KeySym ``K'' for which
// both lowercase and uppercase forms are defined. In that case, the
// group should be treated as if the first element were the lowercase
// form of ``K'' and the second element were the uppercase form of
// ``K.''
cased := false
if symbol2 == 0 {
upper := unicode.IsUpper(symbol1Rune)
lower := unicode.IsLower(symbol1Rune)
if upper || lower {
symbol1Rune = unicode.ToLower(symbol1Rune)
symbol2Rune = unicode.ToUpper(symbol1Rune)
cased = true
} else {
symbol2 = symbol1
symbol2Rune = symbol1Rune
}
}
if symbol4 == 0 {
upper := unicode.IsUpper(symbol3Rune)
lower := unicode.IsLower(symbol3Rune)
if upper || lower {
symbol3Rune = unicode.ToLower(symbol3Rune)
symbol4Rune = unicode.ToUpper(symbol3Rune)
cased = true
} else {
symbol4 = symbol3
symbol4Rune = symbol3Rune
}
}
// PARAGRAPH 5
//
// The standard rules for obtaining a KeySym from a KeyPress event make
// use of only the Group 1 and Group 2 KeySyms; no interpretation of/
// other KeySyms in the list is given. Which group to use is determined
// by the modifier state. Switching between groups is controlled by the
// KeySym named MODE SWITCH, by attaching that KeySym to some KeyCode
// and attaching that KeyCode to any one of the modifiers Mod1 through
// Mod5. This modifier is called the group modifier. For any KeyCode,
// Group 1 is used when the group modifier is off, and Group 2 is used
// when the group modifier is on.
modeSwitch := state & backend.modifierMasks.modeSwitch > 0
if modeSwitch {
symbol1 = symbol3
symbol1Rune = symbol3Rune
symbol2 = symbol4
symbol2Rune = symbol4Rune
}
// PARAGRAPH 6
//
// The Lock modifier is interpreted as CapsLock when the KeySym named
// XK_Caps_Lock is attached to some KeyCode and that KeyCode is attached
// to the Lock modifier. The Lock modifier is interpreted as ShiftLock
// when the KeySym named XK_Shift_Lock is attached to some KeyCode and
// that KeyCode is attached to the Lock modifier. If the Lock modifier
// could be interpreted as both CapsLock and ShiftLock, the CapsLock
// interpretation is used.
shift :=
state & xproto.ModMaskShift > 0 ||
state & backend.modifierMasks.shiftLock > 0
capsLock := state & backend.modifierMasks.capsLock > 0
// PARAGRAPH 7
//
// The operation of keypad keys is controlled by the KeySym named
// XK_Num_Lock, by attaching that KeySym to some KeyCode and attaching
// that KeyCode to any one of the modifiers Mod1 through Mod5 . This
// modifier is called the numlock modifier. The standard KeySyms with
// the prefix ``XK_KP_'' in their name are called keypad KeySyms; these
// are KeySyms with numeric value in the hexadecimal range 0xFF80 to
// 0xFFBD inclusive. In addition, vendor-specific KeySyms in the
// hexadecimal range 0x11000000 to 0x1100FFFF are also keypad KeySyms.
numLock := state & backend.modifierMasks.numLock > 0
// PARAGRAPH 8
//
// Within a group, the choice of KeySym is determined by applying the
// first rule that is satisfied from the following list:
var selectedKeysym xproto.Keysym
var selectedRune rune
_, symbol2IsNumPad := keypadCodeTable[symbol2]
switch {
case numLock && symbol2IsNumPad:
// The numlock modifier is on and the second KeySym is a keypad
// KeySym. In this case, if the Shift modifier is on, or if the
// Lock modifier is on and is interpreted as ShiftLock, then the
// first KeySym is used, otherwise the second KeySym is used.
if shift {
selectedKeysym = symbol1
selectedRune = symbol1Rune
} else {
selectedKeysym = symbol2
selectedRune = symbol2Rune
}
case !shift && !capsLock:
// The Shift and Lock modifiers are both off. In this case, the
// first KeySym is used.
selectedKeysym = symbol1
selectedRune = symbol1Rune
case !shift && capsLock:
// The Shift modifier is off, and the Lock modifier is on and is
// interpreted as CapsLock. In this case, the first KeySym is
// used, but if that KeySym is lowercase alphabetic, then the
// corresponding uppercase KeySym is used instead.
if cased && unicode.IsLower(symbol1Rune) {
selectedRune = symbol2Rune
} else {
selectedKeysym = symbol1
selectedRune = symbol1Rune
}
case shift && capsLock:
// The Shift modifier is on, and the Lock modifier is on and is
// interpreted as CapsLock. In this case, the second KeySym is
// used, but if that KeySym is lowercase alphabetic, then the
// corresponding uppercase KeySym is used instead.
if cased && unicode.IsLower(symbol2Rune) {
selectedRune = unicode.ToUpper(symbol2Rune)
} else {
selectedKeysym = symbol2
selectedRune = symbol2Rune
}
case shift:
// The Shift modifier is on, or the Lock modifier is on and is
// interpreted as ShiftLock, or both. In this case, the second
// KeySym is used.
selectedKeysym = symbol2
selectedRune = symbol2Rune
}
/////////////////////////////////////////////////////////////////
// all of the below stuff is specific to stone's button codes. //
/////////////////////////////////////////////////////////////////
// look up in control code table
var isControl bool
button, isControl = buttonCodeTable[selectedKeysym]
if isControl { return }
// look up in keypad table
button, numberPad = keypadCodeTable[selectedKeysym]
if numberPad { return }
// otherwise, use the rune
button = stone.Button(selectedRune)
return
}
// keysymToRune takes in an X keysym and outputs a utf32 code point. This
// function does not and should not handle keypad keys, as those are handled
// by Backend.keycodeToButton.
func keysymToRune (keysym xproto.Keysym) (character rune) {
// X keysyms like 0xFF.. or 0xFE.. are non-character keys. these cannot
// be converted so we return a zero.
if (keysym >> 8) == 0xFF || (keysym >> 8) == 0xFE {
character = 0
return
}
// some X keysyms have a single bit set to 1 here. i believe this is to
// prevent conflicts with existing codes. if we mask it off we will get
// a correct utf-32 code point.
if keysym & 0xF000000 == 0x1000000 {
character = rune(keysym & 0x0111111)
return
}
// if none of these things happened, we can safely (i think) assume that
// the keysym is an exact utf-32 code point.
character = rune(keysym)
return
}

View File

@ -91,7 +91,9 @@ func (backend *Backend) handleButtonPress (
backend.compressScrollSum(&sum) backend.compressScrollSum(&sum)
backend.callbackManager.RunScroll(sum.x, sum.y) backend.callbackManager.RunScroll(sum.x, sum.y)
} else { } else {
backend.callbackManager.RunPress(stone.Button(buttonEvent.Detail + 127)) backend.callbackManager.RunPress (
stone.Button(buttonEvent.Detail + 127),
stone.Modifiers { })
} }
} }
@ -108,17 +110,27 @@ func (backend *Backend) handleKeyPress (
connection *xgbutil.XUtil, connection *xgbutil.XUtil,
event xevent.KeyPressEvent, event xevent.KeyPressEvent,
) { ) {
keyEvent := *event.KeyPressEvent keyEvent := *event.KeyPressEvent
button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State) button, num := backend.keycodeToButton(keyEvent.Detail, keyEvent.State)
backend.callbackManager.RunPress(button) backend.callbackManager.RunPress (button, stone.Modifiers {
Shift:
(keyEvent.State & xproto.ModMaskShift) > 0 ||
(keyEvent.State & backend.modifierMasks.shiftLock) > 0,
Control: (keyEvent.State & xproto.ModMaskControl) > 0,
Alt: (keyEvent.State & backend.modifierMasks.alt) > 0,
Meta: (keyEvent.State & backend.modifierMasks.meta) > 0,
Super: (keyEvent.State & backend.modifierMasks.super) > 0,
Hyper: (keyEvent.State & backend.modifierMasks.hyper) > 0,
NumberPad: num,
})
} }
func (backend *Backend) handleKeyRelease ( func (backend *Backend) handleKeyRelease (
connection *xgbutil.XUtil, connection *xgbutil.XUtil,
event xevent.KeyReleaseEvent, event xevent.KeyReleaseEvent,
) { ) {
keyEvent := *event.KeyReleaseEvent keyEvent := *event.KeyReleaseEvent
button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State) button, _ := backend.keycodeToButton(keyEvent.Detail, keyEvent.State)
backend.callbackManager.RunRelease(button) backend.callbackManager.RunRelease(button)
} }

View File

@ -32,11 +32,20 @@ func factory (
} }
// load font // load font
backend.font.face = findAndLoadFont ( backend.font.normal = findAndLoadFont (
backend.config.FontName(), backend.config.FontNameNormal(),
float64(backend.config.FontSize())) float64(backend.config.FontSize()))
if backend.font.face == nil { backend.font.bold = findAndLoadFont (
backend.font.face = basicfont.Face7x13 backend.config.FontNameBold(),
float64(backend.config.FontSize()))
backend.font.italic = findAndLoadFont (
backend.config.FontNameItalic(),
float64(backend.config.FontSize()))
backend.font.boldItalic = findAndLoadFont (
backend.config.FontNameBoldItalic(),
float64(backend.config.FontSize()))
if backend.font.normal == nil {
backend.font.normal = basicfont.Face7x13
} }
// pre-calculate colors // pre-calculate colors
@ -56,8 +65,8 @@ func factory (
} }
// calculate metrics // calculate metrics
metrics := backend.font.face.Metrics() metrics := backend.font.normal.Metrics()
glyphAdvance, _ := backend.font.face.GlyphAdvance('M') glyphAdvance, _ := backend.font.normal.GlyphAdvance('M')
backend.metrics.cellWidth = glyphAdvance.Round() backend.metrics.cellWidth = glyphAdvance.Round()
backend.metrics.cellHeight = metrics.Height.Round() backend.metrics.cellHeight = metrics.Height.Round()
backend.metrics.descent = metrics.Descent.Round() backend.metrics.descent = metrics.Descent.Round()
@ -74,7 +83,18 @@ func factory (
if err != nil { return } if err != nil { return }
backend.window, err = xwindow.Generate(backend.connection) backend.window, err = xwindow.Generate(backend.connection)
if err != nil { return } if err != nil { return }
// get keyboard mapping information
keybind.Initialize(backend.connection) keybind.Initialize(backend.connection)
backend.modifierMasks.capsLock = backend.keysymToMask(0xFFE5)
backend.modifierMasks.shiftLock = backend.keysymToMask(0xFFE6)
backend.modifierMasks.numLock = backend.keysymToMask(0xFF7F)
backend.modifierMasks.modeSwitch = backend.keysymToMask(0xFF7E)
backend.modifierMasks.hyper = backend.keysymToMask(0xffed)
backend.modifierMasks.super = backend.keysymToMask(0xffeb)
backend.modifierMasks.meta = backend.keysymToMask(0xffe7)
backend.modifierMasks.alt = backend.keysymToMask(0xffe9)
// create the window // create the window
backend.window.Create ( backend.window.Create (
@ -133,7 +153,7 @@ func factory (
Connect(backend.connection, backend.window.Id) Connect(backend.connection, backend.window.Id)
// uncomment these to draw debug bounds // uncomment these to draw debug bounds
// backend.drawCellBounds = true // backend.drawCellBounds = true
// backend.drawBufferBounds = true // backend.drawBufferBounds = true
output = backend output = backend
@ -158,6 +178,38 @@ func findAndLoadFont (name string, size float64) (face font.Face) {
return return
} }
func (backend *Backend) keysymToKeycode (
symbol xproto.Keysym,
) (
code xproto.Keycode,
) {
mapping := keybind.KeyMapGet(backend.connection)
for index, testSymbol := range mapping.Keysyms {
if testSymbol == symbol {
code = xproto.Keycode (
index /
int(mapping.KeysymsPerKeycode) +
int(backend.connection.Setup().MinKeycode))
break
}
}
return
}
func (backend *Backend) keysymToMask (
symbol xproto.Keysym,
) (
mask uint16,
) {
mask = keybind.ModGet (
backend.connection,
backend.keysymToKeycode(symbol))
return
}
// init registers this backend when the program starts. // init registers this backend when the program starts.
func init () { func init () {
stone.RegisterBackend(factory) stone.RegisterBackend(factory)

View File

@ -1,187 +0,0 @@
package x
import "unicode"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/keybind"
import "git.tebibyte.media/sashakoshka/stone"
// when making changes to this file, look at keysymdef.h and
// https://tronche.com/gui/x/xlib/input/keyboard-encoding.html
var buttonCodeTable = map[xproto.Keysym] stone.Button {
0xFFFFFF: stone.ButtonUnknown,
0xFF63: stone.KeyInsert,
0xFF67: stone.KeyMenu,
0xFF61: stone.KeyPrintScreen,
0xFF6B: stone.KeyPause,
0xFFE5: stone.KeyCapsLock,
0xFF14: stone.KeyScrollLock,
0xFF7F: stone.KeyNumLock,
0xFF08: stone.KeyBackspace,
0xFF09: stone.KeyTab,
0xFF0D: stone.KeyEnter,
0xFF1B: stone.KeyEscape,
0xFF52: stone.KeyUp,
0xFF54: stone.KeyDown,
0xFF51: stone.KeyLeft,
0xFF53: stone.KeyRight,
0xFF55: stone.KeyPageUp,
0xFF56: stone.KeyPageDown,
0xFF50: stone.KeyHome,
0xFF57: stone.KeyEnd,
0xFFE1: stone.KeyLeftShift,
0xFFE2: stone.KeyRightShift,
0xFFE3: stone.KeyLeftControl,
0xFFE4: stone.KeyRightControl,
0xFFE9: stone.KeyLeftAlt,
0xFFEA: stone.KeyRightAlt,
0xFFEB: stone.KeyLeftSuper,
0xFFEC: stone.KeyRightSuper,
0xFFFF: stone.KeyDelete,
0xFFBE: stone.KeyF1,
0xFFBF: stone.KeyF2,
0xFFC0: stone.KeyF3,
0xFFC1: stone.KeyF4,
0xFFC2: stone.KeyF5,
0xFFC3: stone.KeyF6,
0xFFC4: stone.KeyF7,
0xFFC5: stone.KeyF8,
0xFFC6: stone.KeyF9,
0xFFC7: stone.KeyF10,
0xFFC8: stone.KeyF11,
0xFFC9: stone.KeyF12,
}
func (backend *Backend) keycodeToButton (
keycode xproto.Keycode,
state uint16,
) (
button stone.Button,
) {
// FIXME: also set shift to true if the lock modifier is on and the lock
// modifier is interpreted as shiftLock
shift := state & xproto.ModMaskShift > 0
// FIXME: only set this to true if the lock modifier is on and the lock
// modifier is interpreted as capsLock
capsLock := state & xproto.ModMaskLock > 0
symbol1 := keybind.KeysymGet(backend.connection, keycode, 0)
symbol2 := keybind.KeysymGet(backend.connection, keycode, 1)
symbol3 := keybind.KeysymGet(backend.connection, keycode, 2)
symbol4 := keybind.KeysymGet(backend.connection, keycode, 3)
cased := false
// third paragraph
switch {
case symbol2 == 0 && symbol3 == 0 && symbol4 == 0:
symbol3 = symbol1
case symbol3 == 0 && symbol4 == 0:
symbol3 = symbol1
symbol2 = symbol2
case symbol4 == 0:
symbol4 = 0
}
symbol1Rune := keysymToRune(symbol1)
symbol2Rune := keysymToRune(symbol2)
symbol3Rune := keysymToRune(symbol3)
symbol4Rune := keysymToRune(symbol4)
// FIXME: we ignore mode switch stuff
_ = symbol4Rune
// fourth paragraph
if symbol2 == 0 {
upper := unicode.IsUpper(symbol1Rune)
lower := unicode.IsLower(symbol1Rune)
if upper || lower {
symbol1Rune = unicode.ToLower(symbol1Rune)
symbol2Rune = unicode.ToUpper(symbol1Rune)
cased = true
} else {
symbol2 = symbol1
}
}
if symbol4 == 0 {
upper := unicode.IsUpper(symbol3Rune)
lower := unicode.IsLower(symbol3Rune)
if upper || lower {
symbol3Rune = unicode.ToLower(symbol3Rune)
symbol4Rune = unicode.ToUpper(symbol3Rune)
cased = true
} else {
symbol4 = symbol3
}
}
var selectedKeysym xproto.Keysym
var selectedRune rune
// big ol list in the middle
switch {
// FIXME: take into account numlock
case !shift && !capsLock:
selectedKeysym = symbol1
selectedRune = symbol1Rune
case !shift && capsLock:
if cased && unicode.IsLower(symbol1Rune) {
selectedRune = symbol2Rune
} else {
selectedKeysym = symbol1
selectedRune = symbol1Rune
}
case shift && capsLock:
if cased && unicode.IsLower(symbol2Rune) {
selectedRune = unicode.ToUpper(symbol2Rune)
} else {
selectedKeysym = symbol2
selectedRune = symbol2Rune
}
case shift:
selectedKeysym = symbol2
selectedRune = symbol2Rune
}
// look up in table
var isControl bool
button, isControl = buttonCodeTable[selectedKeysym]
// if it wasn't found,
if !isControl {
button = stone.Button(selectedRune)
}
return
}
func keysymToRune (keysym xproto.Keysym) (character rune) {
// X keysyms like 0xFF.. or 0xFE.. are non-character keys. these cannot
// be converted so we return a zero.
if (keysym >> 8) == 0xFF || (keysym >> 8) == 0xFE {
character = 0
return
}
// some X keysyms have a single bit set to 1 here. i believe this is to
// prevent conflicts with existing codes. if we mask it off we will get
// a correct utf-32 code point.
if keysym & 0xF000000 == 0x1000000 {
character = rune(keysym & 0x0111111)
return
}
// if none of these things happened, we can safely (i think) assume that
// the keysym is an exact utf-32 code point.
character = rune(keysym)
return
}

View File

@ -27,7 +27,10 @@ type Backend struct {
lock sync.Mutex lock sync.Mutex
font struct { font struct {
face font.Face normal font.Face
bold font.Face
italic font.Face
boldItalic font.Face
} }
colors [8]xgraphics.BGRA colors [8]xgraphics.BGRA
@ -43,6 +46,18 @@ type Backend struct {
descent int descent int
} }
modifierMasks struct {
capsLock uint16
shiftLock uint16
numLock uint16
modeSwitch uint16
alt uint16
meta uint16
super uint16
hyper uint16
}
windowBoundsClean bool windowBoundsClean bool
} }

View File

@ -8,8 +8,8 @@ type Color uint8
const ( const (
ColorBackground Color = 0x0 ColorBackground Color = 0x0
ColorForeground Color = 0x1 ColorForeground Color = 0x1
ColorRed Color = 0x2 ColorDim Color = 0x2
ColorOrange Color = 0x3 ColorRed Color = 0x3
ColorYellow Color = 0x4 ColorYellow Color = 0x4
ColorGreen Color = 0x5 ColorGreen Color = 0x5
ColorBlue Color = 0x6 ColorBlue Color = 0x6
@ -21,11 +21,11 @@ const (
type Style uint8 type Style uint8
const ( const (
StyleNormal Style = iota StyleNormal Style = 0
StyleBold Style = iota >> 1 StyleBold Style = 1
StyleItalic StyleItalic Style = 2
StyleUnderline StyleUnderline Style = 4
StyleHighlight StyleHighlight Style = 8
StyleBoldItalic Style = StyleBold | StyleItalic StyleBoldItalic Style = StyleBold | StyleItalic
) )
@ -44,7 +44,7 @@ func (cell Cell) Color () (color Color) {
} }
// Style returns the styling information associated with the cell // Style returns the styling information associated with the cell
func (cell Cell) Style (style Style) { func (cell Cell) Style () (style Style) {
style = cell.style style = cell.style
return return
} }
@ -192,9 +192,13 @@ func (buffer *DamageBuffer) Write (bytes []byte) (bytesWritten int, err error) {
bytesWritten = len(bytes) bytesWritten = len(bytes)
for _, character := range text { for _, character := range text {
buffer.setRune(buffer.dot.x, buffer.dot.y, character) if character == '\n' {
buffer.dot.x ++ buffer.dot.x = 0
if buffer.dot.x > buffer.width { break } buffer.dot.y ++
} else {
buffer.setRune(buffer.dot.x, buffer.dot.y, character)
buffer.dot.x ++
}
} }
return return

236
config.go
View File

@ -1,171 +1,139 @@
package stone package stone
import "os"
import "bufio"
import "strings"
import "strconv"
import "image/color" import "image/color"
import "path/filepath"
// Config stores configuration parameters. Backends only should honor parameters import "git.tebibyte.media/sashakoshka/stone/config"
// that they can support.
// Config stores global, read-only configuration parameters that apply to all
// applications. Backends only should honor parameters that they can support.
type Config struct { type Config struct {
private config.Config
colors [8]color.Color colors [8]color.Color
padding int padding int
center bool center bool
fontSize int fontSize int
fontName string fontNameNormal string
fontNameBold string
fontNameItalic string
fontNameBoldItalic string
} }
// Color returns the color value at the specified index. // Color returns the color value at the specified index.
func (config *Config) Color (index Color) (value color.Color) { func (public *Config) Color (index Color) (value color.Color) {
value = config.colors[index] value = public.colors[index]
return return
} }
// Padding specifies how many cell's worth of padding should be on all sides of // Padding specifies how many cell's worth of padding should be on all sides of
// the buffer. // the buffer.
func (config *Config) Padding () (padding int) { func (public *Config) Padding () (padding int) {
padding = config.padding padding = public.padding
return return
} }
// Center returns whether the buffer should be displayed in the center of the // Center returns whether the buffer should be displayed in the center of the
// window like in kitty, or aligned to one corner like in gnome-terminal. // window like in kitty, or aligned to one corner like in gnome-terminal.
func (config *Config) Center () (center bool) { func (public *Config) Center () (center bool) {
center = config.center center = public.center
return return
} }
// FontSize specifies how big the font should be. // FontSize specifies how big the font should be.
func (config *Config) FontSize () (fontSize int) { func (public *Config) FontSize () (fontSize int) {
fontSize = config.fontSize fontSize = public.fontSize
return return
} }
// FontName specifies the name of the font to use. // FontNameNormal specifies the name of the font to use for normal text.
func (config *Config) FontName () (fontName string) { func (public *Config) FontNameNormal () (fontName string) {
fontName = config.fontName fontName = public.fontNameNormal
return return
} }
func (config *Config) load () { // FontNameBold specifies the name of the font to use for bold text.
config.colors = [8]color.Color { func (public *Config) FontNameBold () (fontName string) {
// background fontName = public.fontNameBold
color.RGBA { R: 0, G: 0, B: 0, A: 0 }, return
// foreground }
color.RGBA { R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF },
// red // FontName specifies the name of the font to use for text.
color.RGBA { R: 0xFF, G: 0x00, B: 0x00, A: 0xFF }, func (public *Config) FontNameItalic () (fontName string) {
// orange fontName = public.fontNameItalic
color.RGBA { R: 0xFF, G: 0x80, B: 0x00, A: 0xFF }, return
// yellow }
color.RGBA { R: 0xFF, G: 0xFF, B: 0x00, A: 0xFF },
// green // FontName specifies the name of the font to use for text.
color.RGBA { R: 0x00, G: 0xFF, B: 0x00, A: 0xFF }, func (public *Config) FontNameBoldItalic () (fontName string) {
// blue fontName = public.fontNameBoldItalic
color.RGBA { R: 0x00, G: 0x80, B: 0xFF, A: 0xFF }, return
// purple }
color.RGBA { R: 0x80, G: 0x40, B: 0xFF, A: 0xFF },
func (public *Config) load () {
public.private = config.Config {
LegalParameters: map[string] config.Type {
"fontNormal": config.TypeString,
"fontBold": config.TypeString,
"fontItalic": config.TypeString,
"fontBoldItalic": config.TypeString,
"fontSize": config.TypeInteger,
"padding": config.TypeInteger,
"center": config.TypeBoolean,
"colorBackground": config.TypeColor,
"colorForeground": config.TypeColor,
"colorDim": config.TypeColor,
"colorRed": config.TypeColor,
"colorYellow": config.TypeColor,
"colorGreen": config.TypeColor,
"colorBlue": config.TypeColor,
"colorPurple": config.TypeColor,
},
Parameters: map[string] any {
"fontNormal": "",
"fontBold": "",
"fontItalic": "",
"fontBoldItalic": "",
"fontSize": 11,
"padding": 2,
"center": false,
"colorBackground":
color.RGBA { R: 0, G: 0, B: 0, A: 0 },
"colorForeground":
color.RGBA { R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF },
"colorDim":
color.RGBA { R: 0x80, G: 0x80, B: 0x80, A: 0xFF },
"colorRed":
color.RGBA { R: 0xFF, G: 0x00, B: 0x00, A: 0xFF },
"colorYellow":
color.RGBA { R: 0xFF, G: 0xFF, B: 0x00, A: 0xFF },
"colorGreen":
color.RGBA { R: 0x00, G: 0xFF, B: 0x00, A: 0xFF },
"colorBlue":
color.RGBA { R: 0x00, G: 0x80, B: 0xFF, A: 0xFF },
"colorPurple":
color.RGBA { R: 0x80, G: 0x40, B: 0xFF, A: 0xFF },
},
} }
config.fontName = ""
config.fontSize = 11
config.padding = 2
config.loadFile("/etc/stone/stone.conf") public.private.Load("stone")
homeDirectory, err := os.UserHomeDir() params := public.private.Parameters
if err != nil { return }
config.loadFile(filepath.Join(homeDirectory, "/.config/stone/stone.conf")) public.fontNameNormal = params["fontNormal"].(string)
public.fontNameBold = params["fontBold"].(string)
public.fontNameItalic = params["fontItalic"].(string)
public.fontNameBoldItalic = params["fontBoldItalic"].(string)
public.fontSize = params["fontSize"].(int)
public.padding = params["padding"].(int)
public.center = params["center"].(bool)
public.colors[ColorBackground] = params["colorBackground"].(color.RGBA)
public.colors[ColorForeground] = params["colorForeground"].(color.RGBA)
public.colors[ColorDim] = params["colorDim" ].(color.RGBA)
public.colors[ColorRed] = params["colorRed" ].(color.RGBA)
public.colors[ColorYellow] = params["colorYellow" ].(color.RGBA)
public.colors[ColorGreen] = params["colorGreen" ].(color.RGBA)
public.colors[ColorBlue] = params["colorBlue" ].(color.RGBA)
public.colors[ColorPurple] = params["colorPurple" ].(color.RGBA)
return return
} }
func (config *Config) loadFile (path string) {
file, err := os.Open(path)
if err != nil { return }
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) == 0 {
continue
}
if line[0] == '#' {
continue
}
key, value, found := strings.Cut(scanner.Text(), ":")
if !found {
println (
"config: error in file", path +
": key-value separator missing")
println(scanner.Text())
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
var valueInt int
var valueColor color.Color
var valueBoolean bool
if value == "true" {
valueBoolean = true
}
if value[0] == '#' {
if len(value) != 7 {
println (
"config: error in file", path +
": malformed color literal")
continue
}
colorInt, err := strconv.ParseUint(value[1:7], 16, 24)
if err != nil {
println (
"config: error in file", path +
": malformed color literal")
continue
}
valueColor = color.RGBA {
R: uint8(colorInt >> 16),
G: uint8(colorInt >> 8),
B: uint8(colorInt),
A: 0xFF,
}
} else {
valueInt, _ = strconv.Atoi(value)
}
switch key {
case "fontNormal":
config.fontName = value
case "fontSize":
config.fontSize = valueInt
case "padding":
config.padding = valueInt
case "center":
config.center = valueBoolean
case "colorBackground":
config.colors[ColorBackground] = valueColor
case "colorForeground":
config.colors[ColorForeground] = valueColor
case "colorRed":
config.colors[ColorRed] = valueColor
case "colorOrange":
config.colors[ColorOrange] = valueColor
case "colorYellow":
config.colors[ColorYellow] = valueColor
case "colorGreen":
config.colors[ColorGreen] = valueColor
case "colorBlue":
config.colors[ColorBlue] = valueColor
case "colorPurple":
config.colors[ColorPurple] = valueColor
}
}
}

324
config/config.go Normal file
View File

@ -0,0 +1,324 @@
package config
import "io"
import "os"
import "fmt"
import "sort"
import "bufio"
import "strings"
import "strconv"
import "image/color"
import "path/filepath"
// when making changes to this file, look at
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
// Error represents an error that can be returned by functions or methods in
// this module.
type Error int
const (
// ErrorIllegalName is thrown when an application name contains illegal
// characters such as a slash.
ErrorIllegalName Error = iota
// ErrorNoSeparator is thrown when a configuration file has an
// incorrectly formatted key-value pair.
ErrorNoSeparator
// ErrorUnknownParameter is thrown when an unknown key is encountered in
// a configuration file.
ErrorUnknownParameter
// ErrorWrongColorLength is thrown when a configuration file has a color
// literal with a total length unequal to 7.
ErrorWrongColorLength
// ErrorMalformedColorLiteral is thrown when a configuration file has an
// improperly formatted color literal, or a color literal was expected
// and something else was encountered.
ErrorMalformedColorLiteral
// ErrorMalformedIntegerLiteral is thrown when a configuration file has
// an improperly formatted integer literal, or an integer literal was
// expected and something else was encountered.
ErrorMalformedIntegerLiteral
// ErrorMalformedFloatLiteral is thrown when a configuration file has
// an improperly formatted float literal, or a float literal was
// expected and something else was encountered.
ErrorMalformedFloatLiteral
)
// Error returns a description of the error.
func (err Error) Error () (description string) {
switch err {
case ErrorIllegalName:
description = "name contains illegal characters"
case ErrorNoSeparator:
description = "key:value pair has no separator"
case ErrorUnknownParameter:
description = "unknown parameter"
case ErrorWrongColorLength:
description = "color literal has the wrong length"
case ErrorMalformedColorLiteral:
description = "malformed color literal"
case ErrorMalformedIntegerLiteral:
description = "malformed integer literal"
case ErrorMalformedFloatLiteral:
description = "malformed float literal"
}
return
}
// Type represents the data type of a configuration parameter.
type Type int
const (
// string
// It is just a basic string with inner whitespace preserved. No quotes
// should be used in the file.
TypeString Type = iota
// Type: image/color.RGBA
// Represented as a 24 bit hexadecimal number (case insensitive)
// preceded with a # sign where the first two digits represent the red
// channel, the middle two digits represent the green channel, and the
// last two digits represent the blue channel.
TypeColor
// Type: int
// An integer literal, like 123456789
TypeInteger
// Type: float64
// A floating point literal, like 1234.56789
TypeFloat
// Type: bool
// Values true, yes, on, and 1 are all truthy (case insensitive) and
// anything else is falsy.
TypeBoolean
)
// Config holds a list of configuration parameters.
type Config struct {
// LegalParameters holds the names and types of all parameters that can
// be parsed. If the parser runs into a parameter that is not listed
// here, it will print out an error message and keep on parsing.
LegalParameters map[string] Type
// Parameters holds the names and values of all parsed parameters. If a
// value is non-nil, it can be safely type asserted into whatever type
// was requested.
Parameters map[string] any
}
// Load loads and parses the files /etc/xdg/<name>/<name>.conf and
// <home>/.config/<name>/<name>.conf, unless the corresponding XDG environment
// variables are set - then it uses those.
func (config *Config) Load (name string) (err error) {
if nameIsIllegal(name) {
err = ErrorIllegalName
return
}
for _, directory := range configDirs {
path := filepath.Join(directory, name, name + ".conf")
file, fileErr := os.Open(path)
if fileErr != nil { continue }
parseErr := config.LoadFrom(file)
defer file.Close()
if parseErr != nil {
println (
"config: error in file", path +
":", parseErr.Error())
}
}
return
}
// LoadFrom parses a configuration file from an io.Reader. Configuration files
// are divided into lines where each line may be blank, a comment, or a
// key-value pair. If the line is blank or begins with a # character, it is
// ignored. Else, the line must have a key and a value separated by a colon.
// Before they are processed, leading and trailing whitespace is trimmed from
// the key and the value. Keys are case sensitive.
func (config *Config) LoadFrom (reader io.Reader) (err error) {
if config.LegalParameters == nil {
config.LegalParameters = make(map[string] Type)
}
if config.Parameters == nil {
config.Parameters = make(map[string] any)
}
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) == 0 {
continue
}
if line[0] == '#' {
continue
}
key, value, found := strings.Cut(scanner.Text(), ":")
if !found {
err = ErrorNoSeparator
return
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
what, isKnown := config.LegalParameters[key]
if !isKnown {
err = ErrorUnknownParameter
return
}
switch what {
case TypeString:
config.Parameters[key] = value
case TypeColor:
var valueColor color.Color
valueColor, err = parseColor(value)
if err != nil { return }
config.Parameters[key] = valueColor
case TypeInteger:
var valueInt int
valueInt, err = strconv.Atoi(value)
if err != nil {
err = ErrorMalformedIntegerLiteral
return
}
config.Parameters[key] = valueInt
case TypeFloat:
var valueFloat float64
valueFloat, err = strconv.ParseFloat(value, 64)
if err != nil {
err = ErrorMalformedFloatLiteral
return
}
config.Parameters[key] = valueFloat
case TypeBoolean:
value = strings.ToLower(value)
truthy :=
value == "true" ||
value == "yes" ||
value == "on" ||
value == "1"
config.Parameters[key] = truthy
}
}
return
}
// Save overwrites the main user configuration file, which is located at
// <home>/.config/<name>/<name>.conf unless $XDG_CONFIG_HOME has been set, in
// which case the value of that variable is used instead.
func (config *Config) Save (name string) (err error) {
if nameIsIllegal(name) {
err = ErrorIllegalName
return
}
err = os.MkdirAll(configHome, 0755)
if err != nil { return }
file, err := os.OpenFile (
filepath.Join(configHome, name, name + ".conf"),
os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0744)
if err != nil { return }
defer file.Close()
err = config.SaveTo(file)
if err != nil { return }
return
}
// SaveTo writes the configuration data to the specified io.Writer. Keys are
// alphabetically sorted.
func (config *Config) SaveTo (writer io.Writer) (err error) {
keys := make([]string, len(config.Parameters))
index := 0
for key, _ := range config.Parameters {
keys[index] = key
index ++
}
sort.Strings(keys)
for _, key := range keys {
value := config.Parameters[key]
switch value.(type) {
case string:
fmt.Fprintf(writer,"%s: %s\n", key, value.(string))
case color.RGBA:
colorValue := value.(color.RGBA)
colorInt :=
uint64(colorValue.R) << 16 |
uint64(colorValue.G) << 8 |
uint64(colorValue.B)
fmt.Fprintf(writer,"%s: #%06x\n", key, colorInt)
case int:
fmt.Fprintf(writer,"%s: %d\n", key, value.(int))
case float64:
fmt.Fprintf(writer,"%s: %f\n", key, value.(float64))
case bool:
fmt.Fprintf(writer,"%s: %t\n", key, value.(bool))
default:
fmt.Fprintf(writer,"# %s: unknown type\n", key)
}
}
return
}
func parseColor (value string) (valueColor color.Color, err error) {
if value[0] == '#' {
if len(value) != 7 {
err = ErrorWrongColorLength
return
}
var colorInt uint64
colorInt, err = strconv.ParseUint(value[1:7], 16, 24)
if err != nil {
err = ErrorMalformedColorLiteral
return
}
valueColor = color.RGBA {
R: uint8(colorInt >> 16),
G: uint8(colorInt >> 8),
B: uint8(colorInt),
A: 0xFF,
}
} else {
err = ErrorMalformedColorLiteral
return
}
return
}
func nameIsIllegal (name string) (legal bool) {
legal = strings.ContainsAny(name, "/\\|:.%")
return
}

54
config/xdg.go Normal file
View File

@ -0,0 +1,54 @@
package config
import "os"
import "strings"
import "path/filepath"
var homeDirectory string
var configHome string
var configDirs []string
var dataHome string
var cacheHome string
func init () {
var err error
homeDirectory, err = os.UserHomeDir()
if err != nil {
panic("could not get user home directory: " + err.Error())
}
configHome = os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
configHome = filepath.Join(homeDirectory, "/.config/")
}
configDirsString := os.Getenv("XDG_CONFIG_DIRS")
if configDirsString == "" {
configDirsString = "/etc/xdg/"
}
configDirs = append(strings.Split(configDirsString, ":"), configHome)
dataHome = os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
dataHome = filepath.Join(homeDirectory, "/.local/share/")
}
cacheHome = os.Getenv("XDG_CACHE_HOME")
if cacheHome == "" {
cacheHome = filepath.Join(homeDirectory, "/.cache/")
}
}
// DataHome returns the path to the directory where user data should be stored.
func DataHome (name string) (home string) {
home = filepath.Join(dataHome, name)
return
}
// CacheHome returns the path to the directory where cache files should be
// stored.
func CacheHome (name string) (home string) {
home = filepath.Join(cacheHome, name)
return
}

View File

@ -2,7 +2,7 @@ package stone
type CallbackManager struct { type CallbackManager struct {
onQuit func () onQuit func ()
onPress func (button Button) onPress func (button Button, modifiers Modifiers)
onRelease func (button Button) onRelease func (button Button)
onResize func () onResize func ()
onMouseMove func (x, y int) onMouseMove func (x, y int)
@ -15,9 +15,9 @@ func (manager *CallbackManager) RunQuit () {
manager.onQuit() manager.onQuit()
} }
func (manager *CallbackManager) RunPress (button Button) { func (manager *CallbackManager) RunPress (button Button, modifiers Modifiers) {
if manager.onPress == nil { return } if manager.onPress == nil { return }
manager.onPress(button) manager.onPress(button, modifiers)
} }
func (manager *CallbackManager) RunRelease (button Button) { func (manager *CallbackManager) RunRelease (button Button) {

View File

@ -58,7 +58,7 @@ func redraw () {
application.SetRune(0, height - 1, '+') application.SetRune(0, height - 1, '+')
for x := 0; x < width; x ++ { for x := 0; x < width; x ++ {
application.SetColor(x, height / 2, stone.Color(x % 6 + 2)) application.SetColor(x, height / 2, stone.Color(x % 5 + 3))
} }
for x := 1; x < width - 1; x ++ { for x := 1; x < width - 1; x ++ {

View File

@ -0,0 +1,106 @@
package main
import "os"
import "image"
import "image/color"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/stone"
import "git.tebibyte.media/sashakoshka/stone/config"
import _ "git.tebibyte.media/sashakoshka/stone/backends/x"
var application = &stone.Application { }
var inputState struct {
x int
y int
}
var globalConfig config.Config
func main () {
application.SetTitle("configuration viewer")
application.SetSize(32, 16)
iconFile16, err := os.Open("assets/scaffold16.png")
if err != nil { panic(err) }
icon16, _, err := image.Decode(iconFile16)
if err != nil { panic(err) }
iconFile16.Close()
iconFile32, err := os.Open("assets/scaffold32.png")
if err != nil { panic(err) }
icon32, _, err := image.Decode(iconFile32)
if err != nil { panic(err) }
iconFile16.Close()
application.SetIcon([]image.Image { icon16, icon32 })
application.OnPress(onPress)
application.OnRelease(onRelease)
application.OnMouseMove(onMouseMove)
application.OnStart(onStart)
application.OnResize(redraw)
err = application.Run()
if err != nil { panic(err) }
}
func onStart () {
// this is just copy pasted from config.go
globalConfig = config.Config {
LegalParameters: map[string] config.Type {
"fontNormal": config.TypeString,
"fontSize": config.TypeInteger,
"padding": config.TypeInteger,
"center": config.TypeBoolean,
"colorBackground": config.TypeColor,
"colorForeground": config.TypeColor,
"colorDim": config.TypeColor,
"colorRed": config.TypeColor,
"colorYellow": config.TypeColor,
"colorGreen": config.TypeColor,
"colorBlue": config.TypeColor,
"colorPurple": config.TypeColor,
},
Parameters: map[string] any {
"fontNormal": "",
"fontSize": 11,
"padding": 2,
"center": false,
"colorBackground":
color.RGBA { R: 0, G: 0, B: 0, A: 0 },
"colorForeground":
color.RGBA { R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF },
"colorDim":
color.RGBA { R: 0x80, G: 0x80, B: 0x80, A: 0xFF },
"colorRed":
color.RGBA { R: 0xFF, G: 0x00, B: 0x00, A: 0xFF },
"colorYellow":
color.RGBA { R: 0xFF, G: 0xFF, B: 0x00, A: 0xFF },
"colorGreen":
color.RGBA { R: 0x00, G: 0xFF, B: 0x00, A: 0xFF },
"colorBlue":
color.RGBA { R: 0x00, G: 0x80, B: 0xFF, A: 0xFF },
"colorPurple":
color.RGBA { R: 0x80, G: 0x40, B: 0xFF, A: 0xFF },
},
}
globalConfig.Load("stone")
redraw()
}
func redraw () {
// i fucking love go interfaces
application.Clear()
application.SetDot(0, 0)
globalConfig.SaveTo(application)
}
func onPress (button stone.Button, modifiers stone.Modifiers) {
}
func onRelease (button stone.Button) {
}
func onMouseMove (x, y int) {
inputState.x = x
inputState.y = y
}

View File

@ -34,7 +34,7 @@ func main () {
if err != nil { panic(err) } if err != nil { panic(err) }
} }
func onPress (button stone.Button) { func onPress (button stone.Button, modifiers stone.Modifiers) {
if button == stone.MouseButtonLeft { if button == stone.MouseButtonLeft {
mousePressed = true mousePressed = true
application.SetRune(0, 0, '+') application.SetRune(0, 0, '+')
@ -50,7 +50,8 @@ func onRelease (button stone.Button) {
} }
} }
func onMouseMove (x, y int) { if mousePressed { func onMouseMove (x, y int) {
if mousePressed {
application.SetRune(x, y, '#') application.SetRune(x, y, '#')
application.Draw() application.Draw()
} }

View File

@ -0,0 +1,51 @@
package main
import "os"
import "fmt"
import "image"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/stone"
import _ "git.tebibyte.media/sashakoshka/stone/backends/x"
var application = &stone.Application { }
func main () {
application.SetTitle("press any key")
application.SetSize(8, 1)
iconFile16, err := os.Open("assets/scaffold16.png")
if err != nil { panic(err) }
icon16, _, err := image.Decode(iconFile16)
if err != nil { panic(err) }
iconFile16.Close()
iconFile32, err := os.Open("assets/scaffold32.png")
if err != nil { panic(err) }
icon32, _, err := image.Decode(iconFile32)
if err != nil { panic(err) }
iconFile16.Close()
application.SetIcon([]image.Image { icon16, icon32 })
application.OnPress(onPress)
application.OnRelease(onRelease)
err = application.Run()
if err != nil { panic(err) }
}
func onPress (button stone.Button, modifiers stone.Modifiers) {
fmt.Printf (
"=>>\t0x%X\tsh: %t\tctrl: %t\talt: %t\tm: %t\ts: %t \th: %t\tnumpad: %t\n",
button,
modifiers.Shift,
modifiers.Control,
modifiers.Alt,
modifiers.Meta,
modifiers.Super,
modifiers.Hyper,
modifiers.NumberPad)
}
func onRelease (button stone.Button) {
fmt.Printf("<--\t0x%X\n", button)
}

77
examples/style/main.go Normal file
View File

@ -0,0 +1,77 @@
package main
import "os"
import "fmt"
import "image"
import "math/rand"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/stone"
import _ "git.tebibyte.media/sashakoshka/stone/backends/x"
var application = &stone.Application { }
func main () {
application.SetTitle("style demo")
application.SetSize(11, 8)
iconFile16, err := os.Open("assets/scaffold16.png")
if err != nil { panic(err) }
icon16, _, err := image.Decode(iconFile16)
if err != nil { panic(err) }
iconFile16.Close()
iconFile32, err := os.Open("assets/scaffold32.png")
if err != nil { panic(err) }
icon32, _, err := image.Decode(iconFile32)
if err != nil { panic(err) }
iconFile16.Close()
application.SetIcon([]image.Image { icon16, icon32 })
application.OnStart(redraw)
application.OnResize(redraw)
application.OnPress(onPress)
err = application.Run()
if err != nil { panic(err) }
}
func onPress (button stone.Button, modifiers stone.Modifiers) {
redraw()
application.Draw()
}
func redraw () {
width, _ := application.Size()
application.SetDot(0, 0)
fmt.Fprint (
application,
"normal\n",
"bold\n",
"italic\n",
"underline\n",
"all 3\n",
"highlighted\n",
"all 4\n",
"highlight?")
fillStyle(0, width, stone.StyleNormal)
fillStyle(1, width, stone.StyleBold)
fillStyle(2, width, stone.StyleItalic)
fillStyle(3, width, stone.StyleUnderline)
fillStyle(4, width, stone.StyleBoldItalic | stone.StyleUnderline)
fillStyle(5, width, stone.StyleHighlight)
fillStyle(6, width, stone.StyleBoldItalic | stone.StyleUnderline |
stone.StyleHighlight)
if rand.Int() % 2 == 0 {
fillStyle(7, width, stone.StyleNormal)
} else {
fillStyle(7, width, stone.StyleHighlight)
}
}
func fillStyle (yOffset, width int, style stone.Style) {
for x := 0; x < width; x ++ {
application.SetStyle(x, yOffset, style)
application.SetColor(x, yOffset, stone.Color(x % 7 + 1))
}
}

View File

@ -1,17 +1,20 @@
package main package main
import "os" import "os"
import "fmt"
import "image" import "image"
import _ "image/png" import _ "image/png"
import "git.tebibyte.media/sashakoshka/stone" import "git.tebibyte.media/sashakoshka/stone"
import _ "git.tebibyte.media/sashakoshka/stone/backends/x" import _ "git.tebibyte.media/sashakoshka/stone/backends/x"
var application = &stone.Application { } var application = &stone.Application { }
var caret = 0 var caretX = 0
var caretY = 2
var page = 1
func main () { func main () {
application.SetTitle("hellorld") application.SetTitle("hellorld")
application.SetSize(32, 16) application.SetSize(32, 28)
iconFile16, err := os.Open("assets/scaffold16.png") iconFile16, err := os.Open("assets/scaffold16.png")
if err != nil { panic(err) } if err != nil { panic(err) }
@ -25,32 +28,57 @@ func main () {
iconFile16.Close() iconFile16.Close()
application.SetIcon([]image.Image { icon16, icon32 }) application.SetIcon([]image.Image { icon16, icon32 })
application.OnStart(redraw)
application.OnPress(onPress)
application.OnResize(redraw)
channel, err := application.Run() err = application.Run()
if err != nil { panic(err) } if err != nil { panic(err) }
application.Draw() application.Draw()
}
for { func redraw () {
event := <- channel application.Clear()
switch event.(type) { _, height := application.Size()
case stone.EventQuit: application.SetDot(0, 0)
os.Exit(0) fmt.Fprint(application, "type some text below:")
caretX = 0
caretY = 2
application.SetDot(0, height - 1)
fmt.Fprintf(application, "page %d", page)
drawCaret()
}
case stone.EventPress: func drawCaret () {
button := event.(stone.EventPress).Button application.SetRune(caretX, caretY, '+')
if button.Printable() { application.SetColor(caretX, caretY, stone.ColorDim)
application.SetRune(caret, 0, rune(button)) }
caret ++
width, _ := application.Size()
if caret >= width {
caret = 0
}
application.Draw()
}
case stone.EventResize: func onPress (button stone.Button, modifiers stone.Modifiers) {
application.Draw() width, height := application.Size()
if button == stone.KeyEnter {
application.SetRune(caretX, caretY, 0)
caretX = 0
caretY ++
} else if button.Printable() {
application.SetRune(caretX, caretY, rune(button))
application.SetColor(caretX, caretY, stone.ColorForeground)
caretX ++
if caretX >= width {
caretX = 0
caretY ++
} }
} }
if caretY >= height - 2 {
page ++
redraw()
}
drawCaret()
application.Draw()
} }

View File

@ -34,26 +34,30 @@ const (
KeyLeftControl Button = 22 KeyLeftControl Button = 22
KeyRightControl Button = 23 KeyRightControl Button = 23
KeyLeftAlt Button = 24 KeyLeftAlt Button = 24
KeyRightAlt Button = 25 KeyRightAlt Button = 25
KeyLeftSuper Button = 26 KeyLeftMeta Button = 26
KeyRightSuper Button = 27 KeyRightMeta Button = 27
KeyLeftSuper Button = 28
KeyRightSuper Button = 29
KeyLeftHyper Button = 30
KeyRightHyper Button = 31
KeyDelete Button = 127 KeyDelete Button = 127
MouseButton1 Button = 128 MouseButton1 Button = 128
MouseButton2 Button = 129 MouseButton2 Button = 129
MouseButton3 Button = 130 MouseButton3 Button = 130
MouseButton4 Button = 131 MouseButton4 Button = 131
MouseButton5 Button = 132 MouseButton5 Button = 132
MouseButton6 Button = 133 MouseButton6 Button = 133
MouseButton7 Button = 134 MouseButton7 Button = 134
MouseButton8 Button = 135 MouseButton8 Button = 135
MouseButton9 Button = 136 MouseButton9 Button = 136
MouseButtonLeft Button = MouseButton1 MouseButtonLeft Button = MouseButton1
MouseButtonMiddle Button = MouseButton2 MouseButtonMiddle Button = MouseButton2
MouseButtonRight Button = MouseButton3 MouseButtonRight Button = MouseButton3
MouseButtonBack Button = MouseButton8 MouseButtonBack Button = MouseButton8
MouseButtonForward Button = MouseButton9 MouseButtonForward Button = MouseButton9
KeyF1 Button = 144 KeyF1 Button = 144
KeyF2 Button = 145 KeyF2 Button = 145
@ -67,6 +71,8 @@ const (
KeyF10 Button = 153 KeyF10 Button = 153
KeyF11 Button = 154 KeyF11 Button = 154
KeyF12 Button = 155 KeyF12 Button = 155
KeyDead Button = 156
) )
// Printable returns whether or not the character could show up on screen. If // Printable returns whether or not the character could show up on screen. If
@ -76,3 +82,22 @@ func (button Button) Printable () (printable bool) {
printable = unicode.IsPrint(rune(button)) printable = unicode.IsPrint(rune(button))
return return
} }
// Modifiers lists what modifier keys are being pressed. This is used in
// conjunction with a button code in a button press event. These should be used
// instead of attempting to track the state of the modifier keys, because there
// is no guarantee that one press event will be coupled with one release event.
type Modifiers struct {
Shift bool
Control bool
Alt bool
Meta bool
Super bool
Hyper bool
// NumberPad does not represent a key, but it behaves like one. If it is
// set to true, the button was pressed on the number pad. It is treated
// as a modifier key because if you don't care whether a key was pressed
// on the number pad or not, you can just ignore this value.
NumberPad bool
}