diff --git a/README.md b/README.md index 124a369..40fa564 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ A live version is available at: http://play-with-docker.com/ ## Requirements -Docker 1.13-dev or higher is required as **attachable** overlay networks feature is required. -Here's a boot2docker 1.13-dev ISO you can use to spawn a VM with this version: https://dl.dropboxusercontent.com/u/29887388/boot2docker.iso +Docker 1.12.1 or higher is required. ## Installation diff --git a/api.go b/api.go index 6b6fc52..d867ca5 100644 --- a/api.go +++ b/api.go @@ -4,32 +4,37 @@ import ( "log" "net/http" - "golang.org/x/net/websocket" - "github.com/franela/play-with-docker/handlers" - "github.com/go-zoo/bone" + "github.com/franela/play-with-docker/services" + "github.com/gorilla/mux" "github.com/urfave/negroni" ) func main() { - mux := bone.New() + server := services.CreateWSServer() - mux.Get("/ping", http.HandlerFunc(handlers.Ping)) - mux.Get("/", http.HandlerFunc(handlers.NewSession)) - mux.Get("/sessions/:sessionId", http.HandlerFunc(handlers.GetSession)) - mux.Post("/sessions/:sessionId/instances", http.HandlerFunc(handlers.NewInstance)) - mux.Delete("/sessions/:sessionId/instances/:instanceName", http.HandlerFunc(handlers.DeleteInstance)) + server.On("connection", handlers.WS) + server.On("error", handlers.WSError) + + r := mux.NewRouter() + r.StrictSlash(false) + + r.HandleFunc("/ping", http.HandlerFunc(handlers.Ping)).Methods("GET") + r.HandleFunc("/", http.HandlerFunc(handlers.NewSession)).Methods("GET") + r.HandleFunc("/sessions/{sessionId}", http.HandlerFunc(handlers.GetSession)).Methods("GET") + r.HandleFunc("/sessions/{sessionId}/instances", http.HandlerFunc(handlers.NewInstance)).Methods("POST") + r.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", http.HandlerFunc(handlers.DeleteInstance)).Methods("DELETE") h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./www/index.html") }) - mux.Get("/p/:sessionId", h) - mux.Get("/assets/*", http.FileServer(http.Dir("./www"))) + r.HandleFunc("/p/{sessionId}", h).Methods("GET") + r.PathPrefix("/assets").Handler(http.FileServer(http.Dir("./www"))) - mux.Get("/sessions/:sessionId/instances/:instanceName/attach", websocket.Handler(handlers.Exec)) + r.Handle("/sessions/{sessionId}/ws/", server) n := negroni.Classic() - n.UseHandler(mux) + n.UseHandler(r) log.Fatal(http.ListenAndServe("0.0.0.0:3000", n)) diff --git a/handlers/delete_instance.go b/handlers/delete_instance.go index 393fcce..8731b68 100644 --- a/handlers/delete_instance.go +++ b/handlers/delete_instance.go @@ -4,12 +4,13 @@ import ( "net/http" "github.com/franela/play-with-docker/services" - "github.com/go-zoo/bone" + "github.com/gorilla/mux" ) func DeleteInstance(rw http.ResponseWriter, req *http.Request) { - sessionId := bone.GetValue(req, "sessionId") - instanceName := bone.GetValue(req, "instanceName") + vars := mux.Vars(req) + sessionId := vars["sessionId"] + instanceName := vars["instanceName"] s := services.GetSession(sessionId) i := services.GetInstance(s, instanceName) diff --git a/handlers/get_session.go b/handlers/get_session.go index 1df39cc..f302ec6 100644 --- a/handlers/get_session.go +++ b/handlers/get_session.go @@ -2,14 +2,17 @@ package handlers import ( "encoding/json" + "log" "net/http" "github.com/franela/play-with-docker/services" - "github.com/go-zoo/bone" + "github.com/gorilla/mux" ) func GetSession(rw http.ResponseWriter, req *http.Request) { - sessionId := bone.GetValue(req, "sessionId") + vars := mux.Vars(req) + sessionId := vars["sessionId"] + log.Println(sessionId) session := services.GetSession(sessionId) diff --git a/handlers/new_instance.go b/handlers/new_instance.go index e346a27..be44521 100644 --- a/handlers/new_instance.go +++ b/handlers/new_instance.go @@ -6,16 +6,18 @@ import ( "net/http" "github.com/franela/play-with-docker/services" - "github.com/go-zoo/bone" + "github.com/gorilla/mux" ) func NewInstance(rw http.ResponseWriter, req *http.Request) { - sessionId := bone.GetValue(req, "sessionId") + vars := mux.Vars(req) + sessionId := vars["sessionId"] s := services.GetSession(sessionId) s.Lock() if len(s.Instances) >= 5 { + s.Unlock() rw.WriteHeader(http.StatusConflict) return } diff --git a/handlers/exec.go b/handlers/ws.go similarity index 63% rename from handlers/exec.go rename to handlers/ws.go index 3927ec1..59f8365 100644 --- a/handlers/exec.go +++ b/handlers/ws.go @@ -1,22 +1,42 @@ package handlers import ( - "io" + "log" - "golang.org/x/net/context" - "golang.org/x/net/websocket" - "golang.org/x/text/encoding" - - "github.com/franela/play-with-docker/cookoo" "github.com/franela/play-with-docker/services" - "github.com/go-zoo/bone" - "github.com/twinj/uuid" + "github.com/googollee/go-socket.io" + "github.com/gorilla/mux" ) -// Echo the data received on the WebSocket. -func Exec(ws *websocket.Conn) { - sessionId := bone.GetValue(ws.Request(), "sessionId") - instanceName := bone.GetValue(ws.Request(), "instanceName") +func WS(so socketio.Socket) { + vars := mux.Vars(so.Request()) + + sessionId := vars["sessionId"] + + session := services.GetSession(sessionId) + if session == nil { + log.Printf("Session with id [%s] does not exist!\n", sessionId) + return + } + + session.AddNewClient(services.NewClient(so, session)) +} +func WSError(so socketio.Socket) { + log.Println("error ws") +} + +/* + so.Join(sessionId) + + // TODO: Reset terminal geometry + + so.On("resize", func(cols, rows int) { + // TODO: Reset terminal geometry + }) + + so.On("disconnection", func() { + //TODO: reset the best terminal geometry + }) ctx := context.Background() @@ -62,5 +82,5 @@ func Exec(ws *websocket.Conn) { case <-ctx.Done(): } } - } +*/ diff --git a/services/client.go b/services/client.go new file mode 100644 index 0000000..b5275df --- /dev/null +++ b/services/client.go @@ -0,0 +1,60 @@ +package services + +import "github.com/googollee/go-socket.io" + +type ViewPort struct { + Rows uint + Cols uint +} + +type Client struct { + SO socketio.Socket + ViewPort ViewPort +} + +func (c *Client) ResizeViewPort(cols, rows uint) { + c.ViewPort.Rows = rows + c.ViewPort.Cols = cols +} + +func NewClient(so socketio.Socket, session *Session) *Client { + so.Join(session.Id) + + c := &Client{SO: so} + + so.On("terminal in", func(name, data string) { + // User wrote something on the terminal. Need to write it to the instance terminal + instance := GetInstance(session, name) + if len(data) > 0 { + instance.Conn.Conn.Write([]byte(data)) + } + }) + + so.On("viewport resize", func(cols, rows uint) { + // User resized his viewport + c.ResizeViewPort(cols, rows) + vp := session.GetSmallestViewPort() + // Resize all terminals in the session + wsServer.BroadcastTo(session.Id, "viewport resize", vp.Cols, vp.Rows) + for _, instance := range session.Instances { + instance.ResizeTerminal(vp.Cols, vp.Rows) + } + }) + so.On("disconnection", func() { + // Client has disconnected. Remove from session and recheck terminal sizes. + for i, cl := range session.Clients { + if cl.SO.Id() == c.SO.Id() { + session.Clients = append(session.Clients[:i], session.Clients[i+1:]...) + break + } + } + vp := session.GetSmallestViewPort() + // Resize all terminals in the session + wsServer.BroadcastTo(session.Id, "viewport resize", vp.Cols, vp.Rows) + for _, instance := range session.Instances { + instance.ResizeTerminal(vp.Cols, vp.Rows) + } + }) + + return c +} diff --git a/services/docker.go b/services/docker.go index 9ee8894..74934e7 100644 --- a/services/docker.go +++ b/services/docker.go @@ -4,8 +4,6 @@ import ( "log" "strings" - ptypes "github.com/franela/play-with-docker/types" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" @@ -29,9 +27,7 @@ func GetContainerInfo(id string) (types.ContainerJSON, error) { } func CreateNetwork(name string) error { - // TODO: This line appears to give an error when running on localhost:3000 - // when driver is specified a name must be given. - opts := types.NetworkCreate{Attachable: true, Driver: "overlay"} + opts := types.NetworkCreate{} _, err := c.NetworkCreate(context.Background(), name, opts) if err != nil { @@ -71,16 +67,14 @@ func AttachExecConnection(execId string, ctx context.Context) (*types.HijackedRe return nil, err } - err = c.ContainerExecResize(ctx, execId, types.ResizeOptions{Height: 24, Width: 80}) - - if err != nil { - return nil, err - } - return &conn, nil } -func CreateInstance(net string, dindImage string) (*ptypes.Instance, error) { +func ResizeExecConnection(execId string, ctx context.Context, cols, rows uint) error { + return c.ContainerExecResize(ctx, execId, types.ResizeOptions{Height: rows, Width: cols}) +} + +func CreateInstance(net string, dindImage string) (*Instance, error) { var maximumPidLimit int64 maximumPidLimit = 150 // Set a ulimit value to prevent misuse @@ -104,7 +98,7 @@ func CreateInstance(net string, dindImage string) (*ptypes.Instance, error) { return nil, err } - return &ptypes.Instance{Name: strings.Replace(cinfo.Name, "/", "", 1), IP: cinfo.NetworkSettings.Networks[net].IPAddress}, nil + return &Instance{Name: strings.Replace(cinfo.Name, "/", "", 1), IP: cinfo.NetworkSettings.Networks[net].IPAddress}, nil } func DeleteContainer(id string) error { diff --git a/services/instance.go b/services/instance.go index 8635c87..c9f5c17 100644 --- a/services/instance.go +++ b/services/instance.go @@ -1,12 +1,25 @@ package services import ( + "context" + "io" "log" "os" - "github.com/franela/play-with-docker/types" + "golang.org/x/text/encoding" + + "github.com/docker/docker/api/types" ) +type Instance struct { + Session *Session `json:"-"` + Name string `json:"name"` + IP string `json:"ip"` + Conn *types.HijackedResponse `json:"-"` + ExecId string `json:"-"` + Ctx context.Context `json:"-"` +} + var dindImage string var defaultDindImageName string @@ -23,31 +36,78 @@ func getDindImageName() string { return dindImage } -func NewInstance(session *types.Session) (*types.Instance, error) { +func NewInstance(session *Session) (*Instance, error) { //TODO: Validate that a session can only have 5 instances - //TODO: Create in redis log.Printf("NewInstance - using image: [%s]\n", dindImage) instance, err := CreateInstance(session.Id, dindImage) + instance.Session = session if err != nil { return nil, err } if session.Instances == nil { - session.Instances = make(map[string]*types.Instance) + session.Instances = make(map[string]*Instance) } session.Instances[instance.Name] = instance + go instance.Exec() + + wsServer.BroadcastTo(session.Id, "new instance", instance.Name, instance.IP) + return instance, nil } -func GetInstance(session *types.Session, name string) *types.Instance { +type sessionWriter struct { + instance *Instance +} + +func (s *sessionWriter) Write(p []byte) (n int, err error) { + wsServer.BroadcastTo(s.instance.Session.Id, "terminal out", s.instance.Name, string(p)) + return len(p), nil +} + +func (i *Instance) ResizeTerminal(cols, rows uint) { + ResizeExecConnection(i.ExecId, i.Ctx, cols, rows) +} + +func (i *Instance) Exec() { + i.Ctx = context.Background() + + id, err := CreateExecConnection(i.Name, i.Ctx) + if err != nil { + return + } + i.ExecId = id + conn, err := AttachExecConnection(id, i.Ctx) + if err != nil { + return + } + + i.Conn = conn + + go func() { + encoder := encoding.Replacement.NewEncoder() + sw := &sessionWriter{instance: i} + io.Copy(encoder.Writer(sw), conn.Reader) + }() + + select { + case <-i.Ctx.Done(): + } +} + +func GetInstance(session *Session, name string) *Instance { //TODO: Use redis return session.Instances[name] } -func DeleteInstance(session *types.Session, instance *types.Instance) error { +func DeleteInstance(session *Session, instance *Instance) error { //TODO: Use redis delete(session.Instances, instance.Name) - return DeleteContainer(instance.Name) + err := DeleteContainer(instance.Name) + + wsServer.BroadcastTo(session.Id, "delete instance", instance.Name) + + return err } diff --git a/services/session.go b/services/session.go index 7eca586..52fa88b 100644 --- a/services/session.go +++ b/services/session.go @@ -2,30 +2,66 @@ package services import ( "log" + "math" + "sync" "time" - "github.com/franela/play-with-docker/types" + "github.com/googollee/go-socket.io" "github.com/twinj/uuid" ) -var sessions map[string]*types.Session +var wsServer *socketio.Server -func init() { - sessions = make(map[string]*types.Session) +type Session struct { + sync.Mutex + Id string `json:"id"` + Instances map[string]*Instance `json:"instances"` + Clients []*Client `json:"-"` } -func NewSession() (*types.Session, error) { - s := &types.Session{} +func (s *Session) GetSmallestViewPort() ViewPort { + minRows := s.Clients[0].ViewPort.Rows + minCols := s.Clients[0].ViewPort.Cols + + for _, c := range s.Clients { + minRows = uint(math.Min(float64(minRows), float64(c.ViewPort.Rows))) + minCols = uint(math.Min(float64(minCols), float64(c.ViewPort.Cols))) + } + + return ViewPort{Rows: minRows, Cols: minCols} +} + +func (s *Session) AddNewClient(c *Client) { + s.Clients = append(s.Clients, c) +} + +var sessions map[string]*Session + +func init() { + sessions = make(map[string]*Session) +} + +func CreateWSServer() *socketio.Server { + server, err := socketio.NewServer(nil) + if err != nil { + log.Fatal(err) + } + wsServer = server + return server +} + +func NewSession() (*Session, error) { + s := &Session{} s.Id = uuid.NewV4().String() - s.Instances = map[string]*types.Instance{} + s.Instances = map[string]*Instance{} log.Printf("NewSession id=[%s]\n", s.Id) - //TODO: Store in something like redis sessions[s.Id] = s // Schedule cleanup of the session time.AfterFunc(1*time.Hour, func() { s = GetSession(s.Id) + wsServer.BroadcastTo(s.Id, "session end") log.Printf("Starting clean up of session [%s]\n", s.Id) for _, i := range s.Instances { i.Conn.Close() @@ -41,13 +77,13 @@ func NewSession() (*types.Session, error) { }) if err := CreateNetwork(s.Id); err != nil { + log.Println("ERROR NETWORKING") return nil, err } return s, nil } -func GetSession(sessionId string) *types.Session { - //TODO: Use redis +func GetSession(sessionId string) *Session { return sessions[sessionId] } diff --git a/types/session.go b/types/session.go deleted file mode 100644 index 38af7fc..0000000 --- a/types/session.go +++ /dev/null @@ -1,21 +0,0 @@ -package types - -import ( - "sync" - - "github.com/docker/docker/api/types" - "github.com/franela/play-with-docker/cookoo" -) - -type Session struct { - sync.Mutex - Id string `json:"id"` - Instances map[string]*Instance `json:"instances"` -} - -type Instance struct { - Name string `json:"name"` - IP string `json:"ip"` - Stdout *cookoo.MultiWriter `json:"-"` - Conn *types.HijackedResponse `json:"-"` -} diff --git a/www/assets/app.js b/www/assets/app.js index 9a6ec6f..444fb31 100644 --- a/www/assets/app.js +++ b/www/assets/app.js @@ -3,34 +3,59 @@ var app = angular.module('DockerPlay', ['ngMaterial']); - app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', function($scope, $log, $http, $location, $timeout, $mdDialog) { + app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$window', function($scope, $log, $http, $location, $timeout, $mdDialog, $window) { $scope.sessionId = window.location.pathname.replace('/p/', ''); $scope.instances = []; + $scope.idx = {}; $scope.selectedInstance = null; + $scope.isAlive = true; - $scope.showAlert = function(title, content) { + angular.element($window).bind('resize', function(){ + if ($scope.selectedInstance) { + $scope.resize($scope.selectedInstance.term.proposeGeometry()); + } + }); + + + $scope.showAlert = function(title, content, parent) { $mdDialog.show( $mdDialog.alert() - .parent(angular.element(document.querySelector('#popupContainer'))) + .parent(angular.element(document.querySelector(parent || '#popupContainer'))) .clickOutsideToClose(true) .title(title) .textContent(content) .ok('Got it!') - ); - } + ); + } + + $scope.resize = function(geometry) { + $scope.socket.emit('viewport resize', geometry.cols, geometry.rows); + } + + $scope.upsertInstance = function(info) { + var i = info; + if (!$scope.idx[i.name]) { + $scope.instances.push(i); + i.buffer = ''; + $scope.idx[i.name] = i; + } else { + $scope.idx[i.name].ip = i.ip; + } + + return $scope.idx[i.name]; + } $scope.newInstance = function() { $http({ method: 'POST', url: '/sessions/' + $scope.sessionId + '/instances', }).then(function(response) { - var i = response.data; - $scope.instances.push(i); - $scope.showInstance(i); + var i = $scope.upsertInstance(response.data); + $scope.showInstance(i); }, function(response) { - if (response.status == 409) { - $scope.showAlert('Max instances reached', 'Maximum number of instances reached') - } + if (response.status == 409) { + $scope.showAlert('Max instances reached', 'Maximum number of instances reached') + } }); } @@ -39,18 +64,64 @@ method: 'GET', url: '/sessions/' + $scope.sessionId, }).then(function(response) { + var socket = io({path: '/sessions/' + sessionId + '/ws'}); + + socket.on('terminal out', function(name, data) { + var instance = $scope.idx[name]; + + if (!instance) { + // instance is new and was created from another client, we should add it + $scope.upsertInstance({name: name}); + instance = $scope.idx[name]; + } + if (!instance.term) { + instance.buffer += data; + } else { + instance.term.write(data); + } + }); + + socket.on('session end', function() { + $scope.showAlert('Session timedout!', 'Your session has expire and all your instances has been deleted.', '#sessionEnd') + $scope.isAlive = false; + }); + + socket.on('viewport', function(rows, cols) { + }); + + socket.on('new instance', function(name, ip) { + $scope.upsertInstance({name: name, ip: ip}); + $scope.$apply(function() { + if ($scope.instances.length == 1) { + $scope.showInstance($scope.instances[0]); + } + }); + }); + + socket.on('delete instance', function(name) { + $scope.removeInstance(name); + $scope.$apply(); + }); + + socket.on('viewport resize', function(cols, rows) { + // viewport has changed, we need to resize all terminals + + $scope.instances.forEach(function(instance) { + instance.term.resize(cols, rows); + }); + }); + + $scope.socket = socket; + var i = response.data; for (var k in i.instances) { var instance = i.instances[k]; - $scope.instances.push(instance); + $scope.idx[instance.name] = instance; } if ($scope.instances.length) { - $scope.showInstance($scope.instances[0]); + $scope.showInstance($scope.instances[0]); } - - // Since session exists, we check it still exists every 10 seconds - $scope.checkHandler = setInterval(checkSession, 10*1000); }, function(response) { if (response.status == 404) { document.write('session not found'); @@ -59,25 +130,41 @@ }); } - $scope.showInstance = function(instance) { - $scope.selectedInstance = instance; - if (!instance.isAttached) { - $timeout(function() {instance.term = createTerminal(instance.name);}); - instance.isAttached = true; - } else { - $timeout(function() {instance.term.focus()}); - } - } + $scope.showInstance = function(instance) { + $scope.selectedInstance = instance; + if (!instance.creatingTerminal) { + instance.creatingTerminal = true; + if (!instance.term) { + $timeout(function() { + createTerminal(instance); + instance.term.focus(); + }, 0, false); + } else { + $timeout(function() { + instance.term.focus(); + }, 0, false); + } + } + } + + $scope.removeInstance = function(name) { + if ($scope.idx[name]) { + delete $scope.idx[name]; + $scope.instances = $scope.instances.filter(function(i) { + return i.name != name; + }); + if ($scope.instances.length) { + $scope.showInstance($scope.instances[0]); + } + } + } $scope.deleteInstance = function(instance) { $http({ method: 'DELETE', url: '/sessions/' + $scope.sessionId + '/instances/' + instance.name, }).then(function(response) { - $scope.instances = $scope.instances.filter(function(i) { return i.name != instance.name }); - if ($scope.instances.length) { - $scope.showInstance($scope.instances[0]); - } + $scope.removeInstance(instance.name); }, function(response) { console.log('error', response); }); @@ -85,16 +172,35 @@ $scope.getSession($scope.sessionId); - function checkSession() { - $http({ - method: 'GET', - url: '/sessions/' + $scope.sessionId, - }).then(function(response) {}, function(response) { - if (response.status == 404) { - clearInterval($scope.checkHandler); - $scope.showAlert('Session timedout!', 'Your session has expire and all your instances has been deleted.') - } - }); - } + function createTerminal(instance, cb) { + if (instance.term) { + return instance.term; + } + + var terminalContainer = document.getElementById('terminal-'+ instance.name); + + var term = new Terminal({ + cursorBlink: false + }); + + term.open(terminalContainer); + + $scope.resize(term.proposeGeometry()); + + term.on('data', function(d) { + $scope.socket.emit('terminal in', instance.name, d); + }); + + instance.term = term; + + if (instance.buffer) { + term.write(instance.buffer); + instance.buffer = ''; + } + + if (cb) { + cb(); + } + } }]); })(); diff --git a/www/assets/main.js b/www/assets/main.js deleted file mode 100644 index 056c429..0000000 --- a/www/assets/main.js +++ /dev/null @@ -1,61 +0,0 @@ -//var term, - //protocol, - //socketURL, - //socket, - //pid, - //charWidth, - //charHeight; - -//var terminalContainer = document.getElementById('terminal-container'), - //optionElements = { - //cursorBlink: document.querySelector('#option-cursor-blink') - //}, - //colsElement = document.getElementById('cols'), - //rowsElement = document.getElementById('rows'); - -//function setTerminalSize () { - //var cols = parseInt(colsElement.value), - //rows = parseInt(rowsElement.value), - //width = (cols * charWidth).toString() + 'px', - //height = (rows * charHeight).toString() + 'px'; - - //terminalContainer.style.width = width; - //terminalContainer.style.height = height; - //term.resize(cols, rows); -//} - -//colsElement.addEventListener('change', setTerminalSize); -//rowsElement.addEventListener('change', setTerminalSize); - -//optionElements.cursorBlink.addEventListener('change', createTerminal); - -//createTerminal(); -// -// -function createTerminal(name) { - var terminalContainer = document.getElementById('terminal-' + name); - // Clean terminal - while (terminalContainer.children.length) { - terminalContainer.removeChild(terminalContainer.children[0]); - } - var term = new Terminal({ - cursorBlink: false - }); - var sessionId = location.pathname.substr(location.pathname.lastIndexOf("/")+1); - protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; - socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/sessions/' + sessionId + '/instances/' + name + '/attach'; - - term.open(terminalContainer); - - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal(term); - - return term; - -} - - -function runRealTerminal(term) { - term.attach(socket); - term._initialized = true; -} diff --git a/www/assets/style.css b/www/assets/style.css index 92b2589..47f6d22 100644 --- a/www/assets/style.css +++ b/www/assets/style.css @@ -1,3 +1,20 @@ .selected button { background-color: rgba(158,158,158,0.2); } + +md-card-content.terminal { + background-color: rgb(113, 113, 113); + padding: 0; +} + +.terminal { + background-color: rgb(113, 113, 113); +} + +.terminal .xterm-viewport { + background-color: rgb(113, 113, 113); +} + +.terminal .xterm-rows { + background-color: #000; +} diff --git a/www/assets/xterm-addons/fit.js b/www/assets/xterm-addons/fit.js new file mode 100644 index 0000000..7657c9c --- /dev/null +++ b/www/assets/xterm-addons/fit.js @@ -0,0 +1,86 @@ +/* + * Fit terminal columns and rows to the dimensions of its + * DOM element. + * + * Approach: + * - Rows: Truncate the division of the terminal parent element height + * by the terminal row height + * + * - Columns: Truncate the division of the terminal parent element width by + * the terminal character width (apply display: inline at the + * terminal row and truncate its width with the current number + * of columns) + */ +(function (fit) { + if (typeof exports === 'object' && typeof module === 'object') { + /* + * CommonJS environment + */ + module.exports = fit(require('../../dist/xterm')); + } else if (typeof define == 'function') { + /* + * Require.js is available + */ + define(['../../dist/xterm'], fit); + } else { + /* + * Plain browser environment + */ + fit(window.Terminal); + } +})(function (Xterm) { + /** + * This module provides methods for fitting a terminal's size to a parent container. + * + * @module xterm/addons/fit/fit + */ + var exports = {}; + + exports.proposeGeometry = function (term) { + var parentElementStyle = window.getComputedStyle(term.element.parentElement), + parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')), + parentElementWidth = parseInt(parentElementStyle.getPropertyValue('width')), + elementStyle = window.getComputedStyle(term.element), + elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')), + elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')), + availableHeight = parentElementHeight - elementPaddingVer, + availableWidth = parentElementWidth - elementPaddingHor, + container = term.rowContainer, + subjectRow = term.rowContainer.firstElementChild, + contentBuffer = subjectRow.innerHTML, + characterHeight, + rows, + characterWidth, + cols, + geometry; + + subjectRow.style.display = 'inline'; + subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace + characterWidth = subjectRow.getBoundingClientRect().width; + subjectRow.style.display = ''; // Revert style before calculating height, since they differ. + characterHeight = parseInt(subjectRow.offsetHeight); + subjectRow.innerHTML = contentBuffer; + + rows = parseInt(availableHeight / characterHeight); + cols = parseInt(availableWidth / characterWidth) - 1; + + geometry = {cols: cols, rows: rows}; + return geometry; + }; + + exports.fit = function (term) { + var geometry = exports.proposeGeometry(term); + + term.resize(geometry.cols, geometry.rows); + }; + + Xterm.prototype.proposeGeometry = function () { + return exports.proposeGeometry(this); + }; + + Xterm.prototype.fit = function () { + return exports.fit(this); + }; + + return exports; +}); diff --git a/www/index.html b/www/index.html index f0c3081..3620491 100644 --- a/www/index.html +++ b/www/index.html @@ -2,81 +2,91 @@
+ Your session has expired. +
++ Add instances to your playground. +
++ Sessions and all their instances are deleted after 1 hour. +
+- Add instances to your playground. -
-- Sessions and all their instances are deleted after 1 hour. -
-