cmdradio и console framework

Written by elwood

Для демонстрации возможностей console framework я долго думал над тем, чтобы написать какое-нибудь небольшое приложение, которые с одной стороны было бы достаточно функциональным (чтобы продемонстрировать возможности тулкита), а с другой – максимально простым (чтобы показать, как просто с этим тулкитом работать). Рассматривались варианты блокнота, телефонной книжки итд, но недавно я увидел проект cmdradio и понял, что это именно то, что нужно – достаточно завернуть консольный плеер радио в приятный простой UI, и даже писать ничего не придётся. Благо код оригинальной программы был настолько прост, что состоял из одного файла в 400 строк. И вот за пару дней была написана обёртка над ней – cmdradio-visual. Выглядит это следующим образом:



А кода буквально пара строк:

<Window Title="cmdradio" xmlns:x="http://consoleframework.org/xaml.xsd"
        xmlns:cmdradio="clr-namespace:cmdradio;assembly=cmdradio"
        MaxWidth="80">
  <Panel>
    <Panel Orientation="Horizontal">
      <GroupBox Title="Genres">
        <Panel Orientation="Vertical">
          <ComboBox ShownItemsCount="20"
                    MaxWidth="30"
                    SelectedItemIndex="{Binding Path=SelectedGenreIndex, Mode=OneWayToSource}"
                    Items="{Binding Path=Genres, Mode=OneWay}"/>
 
          <Panel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,1,0,0">
            <TextBlock Text="Volume"/>
            <cmdradio:VolumeControl Percent="{Binding Path=Volume}"
                                    Margin="1,0,0,0" Width="20" Height="1"/>
          </Panel>
        </Panel>
      </GroupBox>
      <GroupBox Title="Control">
        <Panel Margin="1">
          <Button Name="buttonPlay" Caption="Play" HorizontalAlignment="Stretch"/>
          <Button Name="buttonPause" Caption="Pause" HorizontalAlignment="Stretch"/>
          <Button Name="buttonStop" Caption="Stop" HorizontalAlignment="Stretch"/>
          <Button Name="buttonExit" Caption="Exit" HorizontalAlignment="Stretch"/>
        </Panel>
      </GroupBox>
    </Panel>
    <TextBlock HorizontalAlignment="Stretch" Text="{Binding Path=Status, Mode=OneWay}"/>
  </Panel>
</Window>
WindowsHost windowsHost = ( WindowsHost ) ConsoleApplication.LoadFromXaml( "cmdradio.WindowsHost.xml", null );
 
PlayerWindowModel playerWindowModel = new PlayerWindowModel(  );
Window playerWindow = (Window)ConsoleApplication.LoadFromXaml("cmdradio.PlayerWindow.xml", playerWindowModel);
Player player = new Player(  );
playerWindow.FindChildByName< Button >( "buttonPlay" ).OnClick += ( sender, eventArgs ) => {
    player.cmd = new string[] { "play", ( string ) playerWindowModel.Genres[playerWindowModel.SelectedGenreIndex] };
    player.Play();
};
playerWindow.FindChildByName< Button >( "buttonPause" ).OnClick += ( sender, eventArgs ) => {
    player.ReadCmd( new string[] {"pause"} );
};
playerWindow.FindChildByName< Button >( "buttonStop" ).OnClick += ( sender, eventArgs ) => {
    player.ReadCmd( new string[] {"stop"} );
};
playerWindow.FindChildByName< Button >( "buttonExit" ).OnClick += ( sender, eventArgs ) => {
    ConsoleApplication.Instance.Exit( );
};
windowsHost.Show( playerWindow );
foreach ( string s in player.GetGenres( )) {
    playerWindowModel.Genres.Add( s );
}
 
playerWindowModel.PropertyChanged += ( sender, eventArgs ) => {
    if ( eventArgs.PropertyName == "Volume" ) {
        player.ReadCmd( new string[]
            {
                "volume",
                string.Format( "{0}", playerWindowModel.Volume )
            });
    }
};
playerWindowModel.Status = player.Status;
player.PropertyChanged += ( sender, eventArgs ) => {
    if ( eventArgs.PropertyName == "Status" ) {
        playerWindowModel.Status = player.Status;
    }
};
 
ConsoleApplication.Instance.Run(windowsHost);

По-моему, ещё никогда писать TUI-приложения не было так просто. Архив с программой можно скачать здесь.

JavaCC: токены в зависимости от контекста

Written by elwood

JavaCC позволяет нам работать с набором токенов. Но часто бывает нужно сделать так, чтобы в некотором месте лексер не использовал набор основных токенов, а работал бы иначе. А потом снова возвращался к прежнему состоянию. Например, это может понадобиться для обработки многострочных комментариев – после встречи токена начала такого комментария лексер должен переключиться на режим, в котором бы игнорировалось всё, кроме токена его окончания. В случае комментариев это можно сделать стандартным для JavaCC способом – в настройке токенайзера:

SKIP :
{
  // Однострочный комментарий
  < "//" (~["\r", "\n"])* >
  // Начало многострочного комментария - переход к другому состоянию лексера
| < "/*" > : ML_COMMENT_STATE
}
 
// В этом состоянии есть существуют только два токена: конец комментария
// и всё остальное. По нахождении токена конца комментария состояние возвращается в DEFAULT
<ML_COMMENT_STATE> SKIP :
{
  < "*/" > : DEFAULT
| < ~[] >   
}

А иногда бывает так, что перейти к другому набору токенов нужно не в токенайзере, а именно в парсере. Например, когда парсер встретил специальный токен и потом идут данные в другом формате. При парсинге различных DSL это может встречаться, например, в следующем варианте. Допустим у нас есть токен t_identifier, который начинается с буквы и далее идут буквы или цифры с подчёркиваниями. То есть – обычный идентификатор. Но мы хотим добавить поддержку директивы, синтаксис которой содержит ключевое слово, которое является валидным идентификатором. Например, “ignore”. Если не добавить специальный токен для этой сигнатуры, лексер при встрече с ним выплюнет нам токен-идентификатор. А если добавить токен , то мы резервируем это слово, делая его недоступным в других контекстах, там, где слово ignore могло бы быть именем переменной. В JavaCC FAQ написано о том, что можно изменить лексическое состояние из кода парсера, но в этом случае можно огрести проблем с конвейером токенайзера. Но нашелся хитрый мужик, который запилил для этого удобный костыль (его же и приводят в этом FAQ в качестве безопасного способа). Итак, пишем код:

SKIP :
{
  " "
| "\t"
| "\n"
| "\r"
}
 
TOKEN: {
  <t_cat: "category">
| < tt_identifier: <LETTER> ( <LETTER>|<DIGIT> )* >
  // Строка в кавычках
| < tt_string: "\"" (~["\"","\\","\n","\r"] | "\\" (["n","t","b","r","f","\\","\'","\""] | ["0"-"7"] (["0"-"7"])? | ["0"-"3"] ["0"-"7"] ["0"-"7"]))* "\"" >
| < #LETTER: [ "_", "a"-"z", "A"-"Z", "а"-"я", "А"-"Я" ] >
| < #DIGIT: [ "0"-"9"] >
}
 
// Да, для каждого лексического состояния нужно определить не только TOKEN, но и SKIP
// и всё остальное, что используется
<MYSTATE> SKIP :
{
  " "
| "\t"
| "\n"
| "\r"
}
 
<MYSTATE> TOKEN: {
    < tt_mystate_string: "\"" (~["\"","\\","\n","\r"] | "\\" (["n","t","b","r","f","\\","\'","\""] | ["0"-"7"] (["0"-"7"])? | ["0"-"3"] ["0"-"7"] ["0"-"7"]))* "\"" >
    | <tt_mystate_ignore: "ignore" >
    | <tt_mystate_semicolon: ";">
}
 
// Часть того самого хака, необходимая для работы функции SetState().
TOKEN_MGR_DECLS : {
  void backup(int n) { input_stream.backup(n); }
}
 
// Функция SetState() необходима для безопасного перехода к другому лексическому состоянию
// (так как токенайзер конвейеризирует поток токенов, и нужно этот конвейер сбросить).
JAVACODE
private void SetState(int state) {
  if (state != token_source.curLexState) {
    Token root = new Token(), last=root;
    root.next = null;
 
    // First, we build a list of tokens to push back, in backwards order
    while (token.next != null) {
      Token t = token;
      // Find the token whose token.next is the last in the chain
      while (t.next != null && t.next.next != null)
        t = t.next;
 
      // put it at the end of the new chain
      last.next = t.next;
      last = t.next;
 
      // If there are special tokens, these go before the regular tokens,
      // so we want to push them back onto the input stream in the order
      // we find them along the specialToken chain.
 
      if (t.next.specialToken != null) {
        Token tt=t.next.specialToken;
        while (tt != null) {
          last.next = tt;
          last = tt;
          tt.next = null;
          tt = tt.specialToken;
        }
      }
      t.next = null;
    };
 
    while (root.next != null) {
      token_source.backup(root.next.image.length());
      root.next = root.next.next;
    }
    jj_ntk = -1;
    token_source.SwitchTo(state);
  }
}
 
// Далее уже идёт наш код парсера
private void Category() :
{
    Token tname;
}
{
    <t_cat> <tt_string> ( "my state" MyState() | ";" )
}
 
private void MyState() : {
    int entryState = token_source.curLexState;
}
{   
    { SetState(MYSTATE); }
    <tt_mystate_string> [<tt_mystate_ignore> <tt_mystate_string>] <tt_mystate_semicolon>
    { SetState(entryState); }
}

Неудобство заключается в том, что в изменённом (не-DEFAULT) состоянии нельзя использовать строковые литералы просто так, не добавляя для них токены (то есть вместо “;” мы должны записать <tt_mystate_semicolon>). Возможно, это баг, но может оказаться и специальным ограничением. Ветку с обсуждением этой проблемы можно прочитать здесь. Впрочем, несмотря на эту проблему, у нас всё получилось, и мы вернули себе полный контроль над процессом.

Юбилей: 5 лет блогу

Written by elwood

dzhelso_glavnaya

Как быстро летит время. Сегодня исполняется 5 лет с момента публикации первой блогозаписи в этом журнале. За эти пять лет я написал всего лишь 60 постов, это значит, что в среднем я сочинял по одной записи в месяц 🙂 В последний год, правда, я писал более активно, надеюсь, что темп будет сохраняться, благо темы и идеи, которые хотелось бы осветить, имеются в достатке.