Archive

Archive for February 16, 2017

Smart Pascal: A real life desktop

February 16, 2017 6 comments

Every now and then I get feedback like “can SMS use jQuery?” or “Why don’t you use Sencha’s widgets“. And it just continues with everyone having their own favorite framework that they want SMS to adopt.

First of all, this is to completely misunderstand the architecture of Smart. You can use any framework you like. But you also have to sit down and write some code to incorporate it into the VJL. And believe me it’s not that hard.

The workbench

One of the cooler demo’s that I have been working on for .. oh, 8 hours now, is more or less a full Amiga OS 4 desktop clone. Sounds useless right? Well not really. It demonstrates some fundamental concepts:

  • How to create windows just like eh, windows have
  • How to display file-items in a listview
  • How to create controls outside the form so they remain unaffected by form switching
  • How to host demos and pre-compiled applications inside forms

But why stop there? Why not setup a node.js server and have the desktop act as a front-end to your server?

Always fun to see how far we can push the system

Check out this video on youtube 🙂

NAS front end

If you go out and buy a NAS today, chances are it comes with an HTML interface. So once you plug it into your router, you can browse to it and control the device via the browser.

Ring a bell? That is exactly what I’m doing right here. And it took me less than one working day to get this up and running. Here is how it looks right now:

Running a full remake of a classic demo in a window. No problem. You can run as much code as memory can hold

Running a full remake of a classic demo in a window. No problem. You can run as much code as memory can hold

But I want widget set x, y, z!

Had I done you the disservice of using Sencha or jQueryUI or whatever widget framework, you would have been stuck with that forever. Instead, you get a VCL like framework that is build in a way that ensures – that it can absorb and integrate any UI.

But let’s get back to that desktop.

You probably think: Ok so you have a fake desktop in a browser, it can run some JS demos and look cool. But so what?

You don’t get it. Did you know that X, the display system on Linux is by client / server system by default? Did you know that the entire Linux desktop is just an X client that connects to the server (the server being in the same distro) in order to do it’s business? If you start looking at what you can do – as opposed to what you imagine is impossible, there is a lot of cool stuff you can do for your company right now.

Doing some GPU profiling and watching the callstack

Doing some GPU profiling and watching the callstack

The first thing you need is to set up a server. A node.js server of course, that the website can talk to. Heck you can even do OAuth2 calls to dropbox and whatever online service you like – and get that on your HTML5 desktop without node.

Node however allows you to get system-level access. Listing files, loading files, saving files and even loading programs. Programs here being compiles Smart applications that the desktop can inject and execute inside a “window”.

Whenever you need something executing on the server – well then you call the server via websocket.

If node.js doesn’t do it for you then write it in Delphi or C++, it doesnt really matter. What matters is that you have a universal access point in the browser.

A quick visit to npm and git and you can download fully functional text-processors and large-scale HTML5 applications that does exactly what OpenOffice does. And with that websocket connection to your back-end, you have a real-life solution on your hands.

Here is a quick and dirty storage device API I made. Ram-disk will just store data to a B-Tree based “fake” in-memory filesystem. The real deal will come via websocket on the server.

unit Wb.desktop.Devices;

interface

uses
  System.Types, System.Types.Convert,
  System.Streams, System.Reader, System.Writer,
  System.Stream.Reader, System.Stream.Writer,
  System.Time,
  SmartCL.System,
  SmartCL.Time;

type

  TWbStorageDevice        = class;
  TWbStorageDeviceRamDisk = class;
  TWbDeviceManager        = class;
  TWbCustomFileSystem     = class;
  TWbVirtualFileSystem    = class;
  TWbRemoteFileSystem     = class;
  TWbLocalFileSystem      = class;
  TWbStorageDeviceClass = class of TWbStorageDevice;

  TWbCustomFileSystem = class(TObject)
  end;

  TWbVirtualFileSystem = class(TWbCustomFileSystem)
  end;

  TWbRemoteFileSystem = class(TWbCustomFileSystem)
  end;

  TWbLocalFileSystem = class(TWbCustomFileSystem)
  end;

  /* Requirements for using a device */
  TWbStorageDeviceOptions = set of
    (
      doRequireLogin, // Require authentication before Mount()
      doReadOnly      // Device is read-only
    );

  /* Filesystem access rights */
  TWbStorageDeviceAccess  = set of
    (
      daNone,       // none
      daReadOnly,   // read files only
      daReadWrite,  // read and write [create]
      daExecute     // can execute
    );

  TWbAuthenticatedEvent = procedure (Sender: TWbStorageDevice; Access: TWbStorageDeviceAccess);
  TWbMountEvent = procedure (Sender: TWbStorageDevice);

  /* Abstract storage device */
  TWbStorageDevice = class(TObject)
  private
    FId:            string;
    FName:          string;
    FFileSystem:    TWbCustomFileSystem;
    FOptions:       TWbStorageDeviceOptions;
    FMounted:       boolean;
    FAuthenticated: boolean;
    FManager:       TWbDeviceManager;
  protected
    procedure   SetName(const NewName: string); virtual;
    procedure   SetIdentifier(const NewId: string); virtual;
    procedure   SetFileSystem(const NewFileSystem: TWbCustomFileSystem); virtual;
    function    GetFileSystem: TWbCustomFileSystem; virtual;
    procedure   SetOptions(const NewOptions: TWbStorageDeviceOptions); virtual;
    function    GetOptions: TWbStorageDeviceOptions; virtual;
    procedure   SetAuthenticated(const NewState: boolean); virtual;
  public
    property    Name: string read FName;
    property    Identifier: string read FId;
    property    FileSystem: TWbCustomFileSystem read GetFileSystem;
    property    Options: TWbStorageDeviceOptions read GetOptions;
    property    Mounted: boolean read FMounted;
    property    Authenticated: boolean read FAuthenticated;
    property    DeviceManager: TWbDeviceManager read FManager;

    procedure   Authenticate(UserName, Password: string; const Success: TWbAuthenticatedEvent); overload;
    procedure   Authenticate(UserName, Password, Domain: string; Success: TWbAuthenticatedEvent); overload;
    procedure   Authenticate(AuthKey: string; Success: TWbAuthenticatedEvent); overload;

    procedure   Mount(const Success: TWbMountEvent);
    procedure   UnMount;

    constructor Create(const Manager: TWbDeviceManager); virtual;
    destructor  Destroy; override;
  end;

  /* RAM DISK */
  TWbStorageDeviceRamDisk = class(TWbStorageDevice)
  protected
    function    GetFileSystem: TWbCustomFileSystem; override;
  public
    constructor Create(const Manager: TWbDeviceManager); override;
  end;

  /* Cache disk */
  TWbStorageDeviceCache = class(TWbStorageDevice)
  protected
    function    GetFileSystem: TWbCustomFileSystem; override;
  public
    constructor Create(const Manager: TWbDeviceManager); override;
  end;

  TWbDeviceManager = class(TObject)
  private
    FClasses: array of TWbStorageDeviceClass;
    FObjects: array of TWbStorageDevice;
  public
    procedure RegisterDevice(const DeviceClass: TWbStorageDeviceClass);

    property  Count: integer read ( FObjects.Count );
    property  Device[const Index: integer]: TWbStorageDevice read ( FObjects[Index] ); default;

    destructor Destroy; override;
  end;

implementation

//#############################################################################
// TWbDeviceManager
//#############################################################################

destructor TWbDeviceManager.Destroy;
begin
  while FObjects.Count >0 do
  begin
    FObjects[0].free;
    FObjects.Delete(0,1);
  end;
  FClasses.Clear();
  inherited;
end;

procedure TWbDeviceManager.RegisterDevice(const DeviceClass: TWbStorageDeviceClass);
begin
  if FClasses.IndexOf(DeviceClass) < 0 then
  begin
    FClasses.add(DeviceClass);
    FObjects.add( DeviceClass.Create(self) );
  end;
end;

//#############################################################################
// TWbStorageDeviceCache
//#############################################################################

constructor TWbStorageDeviceCache.Create(const Manager: TWbDeviceManager);
begin
  inherited Create(Manager);
  SetName('DH0');
  SetIdentifier('{2D58F4D9-D8FE-434C-AC32-8B27EEC0AEE2}');
  SetOptions([doReadOnly]);
end;

function TWbStorageDeviceCache.GetFileSystem: TWbCustomFileSystem;
begin
  result := inherited GetFileSystem();
  if result = nil then
  begin
    result := TWbVirtualFileSystem.Create;
    SetFileSystem(result);
  end;
end;

//#############################################################################
// TWbRamDisk
//#############################################################################

constructor TWbStorageDeviceRamDisk.Create(const Manager: TWbDeviceManager);
begin
  inherited Create(Manager);
  SetName('Ram-Disk');
  SetIdentifier('{2E6D58D0-A0C3-4D62-8AC4-0300619418A6}');
  SetOptions([]);
end;

function TWbStorageDeviceRamDisk.GetFileSystem: TWbCustomFileSystem;
begin
  result := inherited GetFileSystem();
  if result = nil then
  begin
    result := TWbVirtualFileSystem.Create;
    SetFileSystem(result);
  end;
end;

//#############################################################################
// TWbStorageDevice
//#############################################################################

constructor TWbStorageDevice.Create(const Manager: TWbDeviceManager);
begin
  inherited Create;
  FManager := Manager;
end;

destructor TWbStorageDevice.Destroy;
begin
  if FFileSystem <> nil then
    FFileSystem.free;
  inherited;
end;

procedure TWbStorageDevice.Mount(const Success: TWbMountEvent);
begin
  if FMounted then
    UnMount;

  FMounted := true;

  if assigned(Success) then
  begin
    TW3Dispatch.Execute( procedure ()
      begin
        Success(self);
      end, 100);
  end;
end;

procedure TWbStorageDevice.UnMount;
begin
  if FMounted then
  begin
    FMounted := false;
  end;
end;

procedure TWbStorageDevice.SetAuthenticated(const NewState: boolean);
begin
  FAuthenticated := NewState;
end;

procedure TWbStorageDevice.SetOptions(const NewOptions: TWbStorageDeviceOptions);
begin
  FOptions := NewOptions;
end;

function TWbStorageDevice.GetOptions: TWbStorageDeviceOptions;
begin
  result := FOptions;
end;

procedure TWbStorageDevice.SetName(const NewName: string);
begin
  FName := NewName;
end;

procedure TWbStorageDevice.SetIdentifier(const NewId: string);
begin
  FId := NewId;
end;

procedure TWbStorageDevice.SetFileSystem(const NewFileSystem: TWbCustomFileSystem);
begin
  FFileSystem := NewFileSystem;
end;

function TWbStorageDevice.GetFileSystem: TWbCustomFileSystem;
begin
  result := FFileSystem;
end;

procedure TWbStorageDevice.Authenticate(UserName, Password: string; const Success: TWbAuthenticatedEvent);
begin
end;

procedure TWbStorageDevice.Authenticate(UserName, Password, Domain: string; Success: TWbAuthenticatedEvent);
begin
end;

procedure TWbStorageDevice.Authenticate(AuthKey: string; Success: TWbAuthenticatedEvent);
begin
end;

end.

So. Writing the foundation of a NAS front-end in Smart, a virtual desktop with a windowing toolkit took me less than 8 hours. How long would it take you in vanilla JS?

So forgive me if I dont take jQuery serious.