okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
f72cc87
main
1package main 2 3import ( 4"fmt" 5"image/color" 6"log" 7"math/rand" 8"os" 9"slices" 10"sync" 11 12 uv"github.com/charmbracelet/ultraviolet" 13"github.com/charmbracelet/ultraviolet/screen" 14"github.com/charmbracelet/x/ansi" 15"github.com/charmbracelet/x/term" 16) 17 18const rootID = "root" 19 20// AppWindow represents a window in the application. 21type AppWindow struct { 22id string 23win * uv.Window 24ctx * screen.Context 25z int 26st uv.Style 27} 28 29// Bounds returns the bounds of the window. 30func (aw * AppWindow )Bounds () uv.Rectangle { 31return aw .win .Bounds () 32} 33 34// Draw draws the window onto the given target window at the specified position. 35func (aw * AppWindow )Draw (scr uv.Screen ,rect uv.Rectangle ) { 36aw .win .Draw (scr ,rect ) 37} 38 39// Resize resizes the window to the given width and height. 40func (aw * AppWindow )Resize (width ,height int ) { 41aw .win .Resize (width ,height ) 42} 43 44// Context returns a new drawing context for the window. 45func (aw * AppWindow )Context ()* screen.Context { 46return aw .ctx 47} 48 49type App struct { 50scr * uv.Window 51root * AppWindow 52wins map [string ]* AppWindow 53zwins []* AppWindow 54active string 55mtx sync.RWMutex 56quit bool 57lastClicked string 58mouseDown bool 59} 60 61// EventHandler represents an event handler function. It receives the focused 62// window and the event as parameters. It returns true if the event was 63// handled, false otherwise. 64type EventHandler func (win * uv.Window ,ev uv.Event )bool 65 66// NewApp creates a new [App] instance. 67func NewApp (width ,height int )* App { 68a := new (App ) 69a .active = rootID 70a .scr = uv .NewWindow (width ,height ,ansi .GraphemeWidth ) 71root := & AppWindow { 72id :rootID , 73win :a .scr .NewWindow (0 ,0 ,width ,height ), 74ctx :screen .NewContext (a .scr ), 75z :0 , 76 } 77a .root = root 78a .wins = map [string ]* AppWindow { 79root .id :root , 80 } 81a .zwins = []* AppWindow {root } 82return a 83} 84 85// BringToFront brings the window with the given id to the front. 86func (a * App )BringToFront (id string ) { 87a .mtx .Lock () 88defer a .mtx .Unlock () 89 90awin ,ok := a .wins [id ] 91if ! ok { 92return 93 } 94 95// Remove the window from its current position. 96for i ,zw := range a .zwins { 97if zw .id == id { 98a .zwins = append (a .zwins [:i ],a .zwins [i + 1 :]... ) 99break 100 } 101 } 102 103// Append it to the end of the slice. 104a .zwins = append (a .zwins ,awin ) 105} 106 107// CreateWindow creates a new window with the given id, position and size. 108func (a * App )CreateWindow (id string ,x ,y ,width ,height int )* AppWindow { 109a .mtx .Lock () 110defer a .mtx .Unlock () 111 112var style uv.Style 113style .Bg = ansi .IndexedColor (rand .Intn (256 )) 114 115win := a .root .win .NewWindow (x ,y ,width ,height ) 116win .Fill (& uv.Cell { 117Content :" " , 118Width :1 , 119Style :style , 120 }) 121 122awin := & AppWindow { 123id :id , 124win :win , 125ctx :screen .NewContext (win ), 126st :style , 127 } 128a .wins [id ]= awin 129a .zwins = append (a .zwins ,awin ) 130return awin 131} 132 133// DestroyWindow destroys the window with the given id. 134func (a * App )DestroyWindow (id string ) { 135a .mtx .Lock () 136defer a .mtx .Unlock () 137_ ,ok := a .wins [id ] 138if ! ok { 139return 140 } 141log .Printf ("destroying window %q" ,id ) 142delete (a .wins ,id ) 143for i ,zw := range a .zwins { 144if zw .id == id { 145a .zwins = append (a .zwins [:i ],a .zwins [i + 1 :]... ) 146break 147 } 148 } 149if a .active == id { 150a .active = rootID 151 } 152} 153 154// SetActiveID sets the currently active window to the given id. 155func (a * App )SetActiveID (id string ) { 156a .mtx .Lock () 157defer a .mtx .Unlock () 158a .active = id 159} 160 161// ActiveID returns the currently active window. If no active window is set, it 162// returns an empty string. 163func (a * App )ActiveID ()string { 164return a .active 165} 166 167// Window returns the window associated with the given id. If no window is 168// found, it returns nil. 169func (a * App )Window (id string )* uv.Window { 170a .mtx .RLock () 171defer a .mtx .RUnlock () 172if win ,ok := a .wins [id ];ok { 173return win .win 174 } 175return nil 176} 177 178// ParentID returns the parent window ID of the given window ID. If no parent is 179// found, it returns an empty string. 180func (a * App )ParentID (id string )string { 181a .mtx .RLock () 182defer a .mtx .RUnlock () 183win ,ok := a .wins [id ] 184if ! ok { 185return "" 186 } 187parent := win .win .Parent () 188if parent == nil { 189return "" 190 } 191for _ ,aw := range a .wins { 192if aw .win == parent { 193return aw .id 194 } 195 } 196return "" 197} 198 199// Draw draws the applications windows to the root window. 200func (a * App )Draw (scr uv.Screen ,area uv.Rectangle ) { 201a .mtx .Lock () 202defer a .mtx .Unlock () 203 204slices .SortStableFunc (a .zwins ,func (aw1 ,aw2 * AppWindow )int { 205return aw1 .z - aw2 .z 206 }) 207 208screen .Clear (a .root .win ) 209for _ ,zw := range a .zwins { 210if zw .id == rootID { 211continue 212 } 213cell0 := zw .win .CellAt (0 ,0 ) 214var bg color.Color 215if cell0 != nil { 216bg = cell0 .Style .Bg 217 } 218log .Printf ("drawing window %q at z-index %d bg color %v" ,zw .id ,zw .z ,bg ) 219zw .Draw (a .root .win ,zw .Bounds ()) 220 } 221 222a .root .Draw (scr ,area ) 223} 224 225// HandleEvent implements an [EventHandler] for the application. 226func (a * App )HandleEvent (id string ,ev uv.Event )bool { 227win := a .Window (id ) 228if win == nil { 229return false 230 } 231 232switch ev := ev .(type ) { 233case uv.KeyPressEvent : 234switch { 235case ev .MatchString ("ctrl+c" ,"esc" ): 236a .quit = true 237return true 238default : 239activeWin ,ok := a .wins [a .ActiveID ()] 240if ! ok { 241log .Printf ("no active window found for id %q" ,a .ActiveID ()) 242break 243 } 244 245ctx := activeWin .Context () 246ctx .SetStyle (activeWin .st ) 247ctx .SetForeground (color .Black ) 248 249switch { 250case ev .MatchString ("backspace" ): 251x ,y := ctx .Position () 252x -- 253if x < 0 { 254x = activeWin .Bounds ().Dx ()- 1 255y -- 256if y < 0 { 257y = 0 258 } 259 } 260ctx .SetPosition (x ,y ) 261ctx .Printf (" " ) 262ctx .SetPosition (x ,y ) 263return true 264case ev .MatchString ("enter" ): 265ctx .Printf ("\n" ) 266return true 267case len (ev .Text )> 0 : 268ctx .ev .Text ) 269return true 270 } 271 } 272case uv.MouseMotionEvent : 273if a .mouseDown && a .lastClicked == id { 274// Move the window. 275bounds := win .Bounds () 276newX := ev .X - bounds .Dx ()/ 2 277newY := ev .Y - bounds .Dy ()/ 2 278log .Printf ("moving window %q from %v to (%d, %d)" ,id ,bounds .Min ,newX ,newY ) 279win .MoveTo (newX ,newY ) 280return true 281 } 282case uv.MouseReleaseEvent : 283a .mouseDown = false 284a .lastClicked = "" 285return true 286case uv.MouseClickEvent : 287a .mouseDown = true 288 289switch ev .Button { 290case uv .MouseLeft : 291log .Printf ("mouse left click for %q at (%d, %d)" ,id ,ev .X ,ev .Y ) 292 293for i := len (a .zwins )- 1 ;i >= 0 ;i -- { 294zw := a .zwins [i ] 295pos := uv .Pos (ev .X ,ev .Y ) 296bounds := zw .win .Bounds () 297if zw .id != rootID && pos .In (bounds ) { 298log .Printf ("clicked window %s at %v (bounds: %v)" ,zw .id ,pos ,bounds ) 299a .SetActiveID (zw .id ) 300a .BringToFront (zw .id ) 301a .lastClicked = zw .id 302return true 303 } 304 } 305 306log .Printf ("no window clicked on at (%d, %d)" ,ev .X ,ev .Y ) 307 308if id == rootID { 309log .Printf ("clicked root window at (%d, %d)" ,ev .X ,ev .Y ) 310 311// Create a new window when we click anywhere in the root window. 312rootSize := a .root .Bounds ().Size () 313width := rand .Intn (20 ) 314height := rand .Intn (10 ) 315if width == 0 || height == 0 { 316// Try again 317return a .HandleEvent (id ,ev ) 318 } 319 320x := ev .X - width / 2 321y := ev .Y - height / 2 322if x < 0 { 323x = 0 324 } 325if y < 0 { 326y = 0 327 } 328if x + width > rootSize .X { 329x = rootSize .X - width 330 } 331if y + height > rootSize .Y { 332y = rootSize .Y - height 333 } 334 335winID := fmt .Sprintf ("win-%d" ,len (a .wins )) 336a .CreateWindow (winID ,x ,y ,width ,height ) 337a .SetActiveID (winID ) 338 339return true 340 } 341 342case uv .MouseRight : 343// Destroy the clicked window. 344for i := len (a .zwins )- 1 ;i >= 0 ;i -- { 345zw := a .zwins [i ] 346pos := uv .Pos (ev .X ,ev .Y ) 347if pos .In (zw .win .Bounds ())&& zw .id != rootID { 348log .Printf ("right-clicked window %q at (%d, %d), destroying" ,zw .id ,ev .X ,ev .Y ) 349a .DestroyWindow (zw .id ) 350 } 351 } 352 353return true 354 } 355 } 356 357return false 358} 359 360// Resize resizes the screen to the given width and height. 361func (a * App )Resize (width ,height int )error { 362a .mtx .Lock () 363defer a .mtx .Unlock () 364a .scr .Resize (width ,height ) 365return nil 366} 367 368// Run starts the application event loop on the given terminal. 369func (a * App )Run (input uv.File ,output uv.File ,environ []string )error { 370term := uv .NewTerminal (uv .NewConsole (input ,output ,environ ),nil ) 371// We're using the alternate screen buffer, we need to ensure we're using 372// the fullscreen and absolute cursor movement flags. 373scr := term .Screen () 374scr .EnterAltScreen () 375scr .HideCursor () 376 377if err := term .Start ();err != nil { 378return fmt .Errorf ("failed to start terminal: %w" ,err ) 379 } 380 381defer term .Stop () 382 383scr .SetMouseMode (uv .MouseModeDrag ) 384 385for ! a .quit { 386select { 387case ev := <- term .Events (): 388log .Printf ("event: %#v" ,ev ) 389 390switch ev := ev .(type ) { 391case uv.WindowSizeEvent : 392// We need to update our terminal size and root window size. 393scr .Resize (ev .Width ,ev .Height ) 394a .root .Resize (ev .Width ,ev .Height ) 395 } 396 397focusedID := a .ActiveID () 398if len (focusedID )== 0 { 399// Ignore events if no window is focused. 400continue 401 } 402 403for ! a .HandleEvent (focusedID ,ev ) { 404if parentID := a .ParentID (focusedID );parentID != "" { 405log .Printf ("event not handled by %q, passing to parent %q" ,focusedID ,parentID ) 406focusedID = parentID 407 }else { 408break 409 } 410 } 411 412if err := scr .Display (a );err != nil { 413return fmt .Errorf ("failed to display terminal: %w" ,err ) 414 } 415 } 416 } 417 418return nil 419} 420 421func init () { 422f ,err := os .OpenFile ("uv.log" ,os .O_CREATE | os .O_WRONLY | os .O_TRUNC ,0o644 ) 423if err != nil { 424log .Fatalf ("failed to open log file: %v" ,err ) 425 } 426log .SetOutput (f ) 427log .SetFlags (log .LstdFlags | log .Lshortfile ) 428} 429 430func main () { 431stdin ,stdout ,environ := os .Stdin ,os .Stdout ,os .Environ () 432physicalWidth ,physicalHeight ,err := term .GetSize (stdout .Fd ()) 433if err != nil { 434log .Fatalf ("failed to get terminal size: %v" ,err ) 435 } 436 437app := NewApp (physicalWidth ,physicalHeight ) 438if err := app .Run (stdin ,stdout ,environ );err != nil { 439log .Fatalf ("application error: %v" ,err ) 440 } 441}