A minap eszembe jutott, hogy milyen jó is lenne, ha a Sublime jelezné, ha van valami elgépelés az éppen szerkesztett fájlba. És bár létezik elég sok megoldás a problémára, ami ráadásul még több nyelvet is (Python, CSS, JavaScript, stb.) támogat, de azért mégis csak sokkal mókásabb egy sajátot írni.

A megoldás lényege, hogy valahol a vinyón van egy PHP bináris, amit megfuttatunk a -l kapcsolóval, ami nem futtatja a fájlt, hanem csak szintaxisra ellenőriz. A válasz a PHP szokásos "parse error" szövege lesz, ami azt írja ki, hogy hol hasalt el a fordító, ami nem feltétlen esik egybe azzal, hogy hol van a hiba. A kódot többféleképpen meg lehetne valósítani (pl. parancsként, amit menüből, vagy billentyűkombinációra lehet futtatni), én azt választottam, hogy mentés eseményre automatikusan fusson le, ha megfelelő a fájl kiterjesztése:

import sublime, sublime_plugin, os, subprocess, re

class PhpLintEventListener(sublime_plugin.EventListener):
    key = "php_lint_syntax_error"
    
    def on_post_save(self, view):
        if not view.settings().has("php_lint"):
            return
        
        config = view.settings().get("php_lint")
        
        bin_path = config.get("binary", "")
        name, ext = os.path.splitext(view.file_name())
        
        if not bin_path or ext not in config.get("extensions", []):
            return
        
        view.erase_regions(self.key)
        view.erase_status(self.key)
        
        shell = sublime.platform() == "windows"
        p = subprocess.Popen([bin_path, "-l",  view.file_name()], shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (stdout, stderr) = p.communicate()
        
        if "" == stderr:
            data = stdout
        else:
            data = stderr
        
        result = re.match(r"(.+?) in (.+?) on line ([0-9]+).*", data, re.M | re.S)
        if result:
            error = result.group(1)
            line = result.group(3)
            
            view.add_regions(self.key, [view.full_line(view.text_point(int(line) - 1, 0))], "markup.deleted", "dot")
            view.set_status(self.key, error + " on line " + line)

Nem egy bonyolult jószág, ha létezik a config és a fájl kiterjesztése benne van a configban megadott listában, akkor megfuttatjuk a PHP binárist és megkeressük a kimenetében, hogy hányadik sorban volt a hiba. A hibás sort kiemeljük és a hiba szövegét a státuszba ki is írjuk. A szükséges config pedig valahogy így néz ki:

"php_lint" : {
    "binary" : "d:\\php\\5.2\\php.exe",
    "extensions" : [".php"]
}

Ezt lehet pakolni a Preferences > Settings - User részbe vagy akár a projekt beállításai közé is (a .sublime-project fájlon belül a "settings" blokkba).

Több, mint 5 és fél év után elérkezett az idő, hogy nyugdíjba vonuljon a régi oldal és megszülessen az 5.0-s verzió. Mivel egyébként nagyjából 8 év magasságában járunk, elég nagy bizonyossággal állíthatom, hogy ilyen sokáig még egy design se bírta. Sok mindent nem is tudnék hozzátenni, az eredmény jól látható (ha csak nem rss olvasóban nézi a kedves olvasó a bejegyzést). Néhány dolog, ami változott:

  • A modern kor elvárásaihoz némiképp igazodva az oldal valamivel reszponzívabb lett.
  • A kommentelési lehetőség kikerült, a továbbiakban a bejegyzéshez kapcsolódó Google+ postban lehet majd megjegyezni dolgokat.
  • Like/+1 gombok sincsenek, csak hogy letisztultabbak és egyszerűbbek legyünk.
  • Bár még mindig nem vittem túlzásba a tartalmat, de már nem csak "Folyamatban..." van az Archívum oldalon. :)

Ééés nagyjából ennyi... Nem vittem túlzásba. :) Aztán biztos maradt egy rakat hiba és hiányosság benne, amit majd az elkövetkezendő évek során bőven lesz időm javítani.

Szóval, már egy ideje foglalkoztat a mobil fejlesztés. Annak is főleg a zöld robotos ágazata. Gondoltam ideje lenne valami maradandóbbat alkotni a gépen heverő sok teszt projektnél, úgyhogy bele is vágtam egy listás alkalmazás elkészítésébe, annak ellenére, hogy listás alkalmazásokkal Dunát lehetne rekeszteni (vagy ha már itt tartunk, lassan már bármilyen típusú alkalmazásokkal... :)). A végeredmény (vagy inkább az első verzió) megtekinthető a Google Play-en:

Vagy mobilosoknak egy kényelmesebb market alkalmazás linken:

Amit tud:

  • Listákat kezelni! :)
  • Külső alkalmazás segítségével QR kódot beolvasni és azt elmenteni listaelemként az egyszerű és gyors felvitel érdekében.
  • Ha linket kap listaelemnek, azt feldolgozza és a meta adatokkal együtt jeleníti meg (a Facebook-os Open Graph tagek hozzák igazán lázba, mással nem nagyon foglalkozik :)).
  • Megosztani egy listát (pl. e-mailben elküldeni).

Nagyjából ennyi, nem egy bonyolult jószág. Az elkészítésében az ActionBarSherlock volt a segítségemre, ami remekül valósítja meg a 4.0-s Action Bar-t 4.0 előtti készülékeken is. Kellemes listázást. :)

Most, hogy már van egy virtuális szerverünk, kellene kezdeni vele valami mókás dolgot. Lehetne mondjuk egy távolról is vezérelhető build rendszerünk (a "távolról" mondjuk ebben az esetben talán egy kicsit túlzás :)), amit a kedvenc szövegszerkesztőnk automatikusan tudna vezérelni. Az úgy kellőképp mókásan hangzik ahhoz, hogy belevágjunk. :)

Ahhoz, hogy legyen értelme a build rendszernek, nem árt, ha van mit build-elni. Tegyük fel mondjuk a példa kedvéért, hogy CSS helyett szeretnénk Sass-t, JavaScript helyett pedig CoffeeScript-et használni. Természetesen a rendszer képességei nem merülnek ki ennyiben, lehetne mondjuk olyat is csinálni, hogy a sablon fájlokat fordítsa le PHP-ra, vagy a konfigurációs XML-ekből gyártson tömböket, esetleg minden unit tesztet lefuttasson. A lehetőségek száma, ahogy mondani szokás, végtelen, de maradjunk az eredeti kettőnél, a többi a kedves olvasó fantáziájára van bízva. Kezdjük is rögtön a telepítéssel:

Sass

sudo apt-get install ruby rubygems
sudo gem install sass

A Sass [Syntactically Awesome Stylesheets] nevezetű előfeldolgozó néhány hasznos kiegészítéssel segít ráncba szedni az elvadult CSS kóddzsungelt.

CoffeeScript

sudo apt-get install nodejs npm
sudo npm install coffee-script

A CoffeeScript a funkcionális programozási nyelvekre és talán egy csöppet a Ruby-ra emlékeztető nyelv a JavaScript egy komolyabb újragondolása (a Sass-CSS viszonylathoz képest), de mentségére legyen mondva, hogy egy az egyben valid JavaScript-re fordul.

Építkezzünk

A build-elésben segítségünkre lesz a vicces nevű PHP-s build eszköz, a Phing. A következő módon érhetjük el, hogy feltelepüljön:

sudo apt-get install php-pear
sudo pear channel-discover pear.phing.info
sudo pear install phing/phing

A példához a következő minimális projekt könyvtárszerkezetet fogjuk használni:

  • assets/
    • coffee/
    • sass/
  • www/
    • css/
    • js/
  • build.xml

Az assets/ könyvtárban vannak a nyers fájlok, a www/ könytárba mennek a generált fájlok, amiket nem árt kívülről is elérni. A kulcs pedig a build.xml, ami a Phing számára tartalmaz utasításokat. Kezdetnek valami ilyesmi például megteszi:

<?xml version="1.0" encoding="UTF-8"?>
<project name="MyProject" default="assets">
    <property name="sass_dir" value="${project.basedir}/assets/sass/" />
    <property name="coffee_dir" value="${project.basedir}/assets/coffee/" />
    
    <property name="css_target" value="${project.basedir}/www/css/" />
    <property name="js_target" value="${project.basedir}/www/js/" />
    
    <target name="sass">
        <exec command="sass --no-cache --update &quot;${sass_dir}:${css_target}&quot;" />
    </target>
    
    <target name="coffee">
        <exec command="coffee -o &quot;${js_target}&quot; &quot;${coffee_dir}&quot;" />
    </target>
    
    <target name="assets">
        <phingcall target="sass" />
        <phingcall target="coffee" />
    </target>
</project>

A projekt könyvtárában (egy szinten a build.xml-lel) állva ki tudjuk adni a következő három parancsot, amivel a Sass-t, CoffeeScript-et és mindkettőt egyszerre lefordíthatjuk (ha nem adunk meg semmit a phing után, akkor a default assets fog lefutni):

phing sass
phing coffee
phing assets

Az alkalmazás

Már csak azt kellene elérni, hogy a fájlok módosítása után a megfelelő parancs automatikusan lefusson. Bár mind a Sass, mind a CoffeeScript rendelkezik megoldással a problémára (--watch), a későbbiekre is gondolva inkább valami általánosabb megoldásra lenne szükség. Mondjuk egy webes minialkalmazás, ami a kapott paraméterek függvényében meghívja a megfelelő parancsot. A kódszerkesztő alkalmazásunk pedig ezt a paraméterezett URL-t hívogathatná. Mondjuk valami ilyesmit:

http://builder.vbox/index.php?project=projektneve&target=target

Az a bizonyos index.php pedig valami ilyen kis egyszerű dolog lenne (az egyszerűség ára az, hogy élünk azzal a feltételezéssel, hogy az egyes projektek a /var/www/projektneve.vbox/ könyvtárban vannak és eltekintünk az olyan dolgoktól, mint például a target valódiságának az ellenőrzése):

$dir = '/var/www/' . get('project') . '.vbox/';
if (!file_exists($dir . 'build.xml')) {
    exit('invalid_project');
}

$target = get('target');
if (!$target) {
    exit('no_target');
}

chdir($dir);
shell_exec('phing ' . escapeshellcmd($target));
exit('ok');

function get($name) {
    if (empty($_GET[$name])) {
        return '';
    }
    return preg_replace('/[^a-z0-9\-\.]/i', '', $_GET[$name]);
}

A plugin

Ezzel meg is van a távolról is vezérelhető egyszerű kis build rendszerünk. A kedvenc szerkesztőnkhöz írhatunk egy plugin-t, ami mentésre meghívja a megfelelő URL-t, hogy legenerálódjanak a CSS/JS fájlok (vagy akár tetszőleges billentyűkombinációra tetszőleges build parancsot meghívhatunk). Sublime Text 2 esetén valahogy így nézne ki a dolog:

import sublime, sublime_plugin
import urllib, urllib2
import os, threading

class BuilderThread(threading.Thread):
    def __init__(self, project, target):
        threading.Thread.__init__(self)
        self.project = project
        self.target = target
    def run(self):
        try:
            data = urllib.urlencode({
                'project': self.project,
                'target': self.target,
            })
            request = urllib2.Request('http://builder.vbox/index.php?' + data)
            response = urllib2.urlopen(request).read()
            return
        except (urllib2.HTTPError) as (e):
            err = '%s: HTTP error %s contacting API' % (__name__, str(e.code))
        except (urllib2.URLError) as (e):
            err = '%s: URL error %s contacting API' % (__name__, str(e.reason)) 
        
        sublime.error_message(err)

class BuilderEventListener(sublime_plugin.EventListener):
    def __init__(self):
        sublime_plugin.EventListener.__init__(self)
    
    def on_post_save(self, view):
        name, ext = os.path.splitext(view.file_name())
        project = view.settings().get('builder_project_name', False)
        
        if (False == project):
            return
        
        if (ext == '.coffee'):
            BuilderThread(project, 'coffee').start()
        elif (ext == '.scss' or ext == '.sass'):
            BuilderThread(project, 'sass').start()

A plugin használ egy builder_project_name nevű beállítást, amit az éppen aktuális projekt .sublime-project fájljában érdemes megadni:

{
    "settings": {
        "builder_project_name": "test-project"
    }
}

Ezzel meg is volnánk, kellemes építkezést.