diff --git a/mixly/files/add.png b/mixly/files/add.png new file mode 100644 index 00000000..cbde9962 Binary files /dev/null and b/mixly/files/add.png differ diff --git a/mixly/files/background.jpg b/mixly/files/background.jpg new file mode 100644 index 00000000..5c672043 Binary files /dev/null and b/mixly/files/background.jpg differ diff --git a/mixly/files/blank.png b/mixly/files/blank.png new file mode 100644 index 00000000..96446457 Binary files /dev/null and b/mixly/files/blank.png differ diff --git a/mixly/files/bootstrap.min.css b/mixly/files/bootstrap.min.css new file mode 100644 index 00000000..a9558eb0 --- /dev/null +++ b/mixly/files/bootstrap.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/mixly/files/default.png b/mixly/files/default.png new file mode 100644 index 00000000..8e2a9908 Binary files /dev/null and b/mixly/files/default.png differ diff --git a/mixly/files/fonts/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2 b/mixly/files/fonts/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2 new file mode 100644 index 00000000..52cbd3ad Binary files /dev/null and b/mixly/files/fonts/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2 differ diff --git a/mixly/files/fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2 b/mixly/files/fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2 new file mode 100644 index 00000000..bb0923bf Binary files /dev/null and b/mixly/files/fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2 differ diff --git a/mixly/files/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2 b/mixly/files/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2 new file mode 100644 index 00000000..36195bdd Binary files /dev/null and b/mixly/files/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2 differ diff --git a/mixly/files/icons/mixly-192.png b/mixly/files/icons/mixly-192.png new file mode 100644 index 00000000..bf94e64a Binary files /dev/null and b/mixly/files/icons/mixly-192.png differ diff --git a/mixly/files/icons/mixly-512.png b/mixly/files/icons/mixly-512.png new file mode 100644 index 00000000..1ae277e5 Binary files /dev/null and b/mixly/files/icons/mixly-512.png differ diff --git a/mixly/files/jquery.mb.YTPlayer.min.css b/mixly/files/jquery.mb.YTPlayer.min.css new file mode 100644 index 00000000..66a84f96 --- /dev/null +++ b/mixly/files/jquery.mb.YTPlayer.min.css @@ -0,0 +1 @@ +@charset "UTF-8";.mb_YTPBar,.mb_YTPBar span.mb_YTPUrl a{color:#fff}@font-face{font-family:ytpregular;src:url(font/ytp-regular.eot)}@font-face{font-family:ytpregular;src:url(data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAA5sABEAAAAAFCAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABgAAAABwAAAAcZ9iuNUdERUYAAAGcAAAAHQAAACAAdAAET1MvMgAAAbwAAABJAAAAYHUMUrFjbWFwAAACCAAAAKkAAAGKn5XycWN2dCAAAAK0AAAANgAAADYNLQohZnBnbQAAAuwAAAGxAAACZVO0L6dnYXNwAAAEoAAAAAgAAAAIAAAAEGdseWYAAASoAAAGVQAAB4jz86dSaGVhZAAACwAAAAAzAAAANgbKONpoaGVhAAALNAAAACAAAAAkESQLXGhtdHgAAAtUAAAAVAAAARxOmwVwbG9jYQAAC6gAAAAjAAAAkFoEXRRtYXhwAAALzAAAACAAAAAgAWoB625hbWUAAAvsAAAA+wAAAeok3Eb+cG9zdAAADOgAAADAAAABN99tv1lwcmVwAAANqAAAALkAAAFY3I6ikndlYmYAAA5kAAAABgAAAAbHMlGnAAAAAQAAAADMPaLPAAAAAM3Nk7QAAAAAzc13sXjaY2BkYGDgA2IJBhBgYmAEQjcgZgHzGAAHTAB5AAAAeNpjYGbZwDiBgZWBhdWY5SwDA8MsCM10liGNKQ3IB0rBASMDEgj1DvdjcGDgfcDAlvYPqJJVldEZpoZVkuUZkFJgYAQAUUULewAAAHjaY2BgYGaAYBkGRgYQaAHyGMF8FoYMIC3GIAAUYQOyeBkUGKIYqhgWKHAp6CvEP2D4/x+sAyTuyJAIFGeAizP+//r/8f/D//f+n/HA8oHo/WcKblDzsQBGoOkwSUYmIMGErgDiRLyAhZWNnYOTi5uHl49fQFBIWERUTFxCUkpaRhYiLyevoKikrKKqpq6hqaWto6unb2BoZGxiambOQF1gQZYuAIQnH4IAAAAAAAAAAAABegEnAHEAswC9AOAA5QD+ARcBIwBdAHIBtgBcAGAAZgByAI8AogErAbIAUwBEBREAAHjaXVG7TltBEN0NDwOBxNggOdoUs5mQxnuhBQnE1Y1iZDuF5QhpN3KRi3EBH0CBRA3arxmgoaRImwYhF0h8Qj4hEjNriKI0Ozuzc86ZM0vKkap36WvPU+ckkMLdBs02/U5ItbMA96Tr642MtIMHWmxm9Mp1+/4LBpvRlDtqAOU9bykPGU07gVq0p/7R/AqG+/wf8zsYtDTT9NQ6CekhBOabcUuD7xnNussP+oLV4WIwMKSYpuIuP6ZS/rc052rLsLWR0byDMxH5yTRAU2ttBJr+1CHV83EUS5DLprE2mJiy/iQTwYXJdFVTtcz42sFdsrPoYIMqzYEH2MNWeQweDg8mFNK3JMosDRH2YqvECBGTHAo55dzJ/qRA+UgSxrxJSjvjhrUGxpHXwKA2T7P/PJtNbW8dwvhZHMF3vxlLOvjIhtoYEWI7YimACURCRlX5hhrPvSwG5FL7z0CUgOXxj3+dCLTu2EQ8l7V1DjFWCHp+29zyy4q7VrnOi0J3b6pqqNIpzftezr7HA54eC8NBY8Gbz/v+SoH6PCyuNGgOBEN6N3r/orXqiKu8Fz6yJ9O/sVoAAAAAAQAB//8AD3jaTZVrbBxXFcfvufNe72Nmdx77tmfHO2N76117784OTr154YAbR7RQuUQhttoSuXZKFQVKKYqgiFJAgkpIkVClIn8opSomjXY3VHHTFldEIYpay1hR+ID4Bha27FoIEQGpd8Idu4lY7c6eOfee//2f3+zeizAaQwif4iYRgwRUbgGqjLYFNvVxtcVzfxltM5iGqMUEaS5ItwU+vTPahiBPFFMpmoo5hnv8XnjFn+Um7/xmjF1GCLHoPf+fgsUVEYcSKIcGkYbaWYxKLZ3bgGa50qpACQ0NeyYoYILaDTqpurUK2FZBUYlJY8ukEc0egLpbo+kY8O/BQcx2dvwP2Fh6/Q+Gl19fyroubHmer7rpjHllPZ/NKB+tp2/4/TzxSx0zo/74uUY29vJZOEHIfng4lzz7cjyXzn/jJwqCwCOLdj2iPSP3F/hUAHF3v+Cviee5DIqhJDLRACLoPGpHECq1M7Sd5iDZ/W6zQW8mu9Ecql7SI6xYaiOpnxCydwPNWqWJ/tSSjY1mqtqU5ZYNpWal2pJiGy0XSi1bVuKX1Fyh1GuMoJYeUeJvy/GEVbTpfTOjHJRVzUim0tlcwekbKD1QrgR5M97OV8nIyMjQsKPUEKWGNEVFFBwqEs/yHMEVFMM1PIc4FhiWQVxHcxjD0zzXEkgbmHe5G1eA9T955453xd+B9tbpi6vj10+fvj6+evH0Fju7vPDU5szVY8euzmw+tXABv7kEov/v33WOv+v/C8LG9M2xD19/EquzCyuHVuY6R25Obz35+odw4NDKwuzWHAK86q9x21wKYYQkjFeZ3M5f/TUmw6Qo12P+38Wf0zEZpVABlVANfQu1owHXXMD1AdIyQhvNgeou2b1LAuhAkVwyExRps/ppAE230qrTX1MrEVXil5W4qlm9thMAMpR2MtVHAbXMnBJvZ8oVGjdZ5XK6u6cwNExqdNJ9dnm4D+8eIeYeM7hH0b3H9bcQuczdeH75ef+TxTveO/5tuDK2Mrs5d+HmzQtzm7MrbP6ZqxMrrz2+vf34aysTV5+5iN9YhMi51W93Tiz5/wFp+ujy/MntGXx+dfrjqflrO788Ob989MaMP716+Nr8FOpCjbvnw032BUrm82gKfQc10SJaAwwZGINHEUrksaEndI3XCppBavWaU7Nrda/u7QfPsnmBF1ReK4NjCxbkgVRJdW/MdmiyjHkhCgKvGkrNq+uGngPLUDXVioJTcGxONWguENOIYmkq1lQqaDu2q1AqKi6qRh6CN0uqhlkn1WIwt1Z3FTqH6lt2kWLkqZpQ2F1H4D3X1CzFUkCp1R8EVaeKGr3mgXpyd3OKZTcgioMi3qImqA2FaFSYrkHd7BYESnSMdqAx1HNgg/6pG0Bo95RAGehqoNAuaRHR90wGdXyJtkAJ1DxSDVQCfS8ocui+EohqagNjFroniyLAOYbBgvSQxuXxiUSCGQXReJBnjafhbf6xBs8P9ZclLLJdTJfdL3bLRsgd50Nf52P7JIWjInYqFuZhUGErucF0Qj/zNJtPGArDz7EYFi0chvSpw8C/mJRgRVLfgrEf7RvowhyjJ3JPfPlX/h8N/6fZryX7bh/pJsPj4QLX9Ra89NL3QQkljmOqnognU6HcxKkoI/JsaJ8cDcfCqZAMC2cfFeSoHu+WFEmWzIQqx8PVmCThSFqPKqLIsgxJx0QYZt1iocjgfrPbjIoiltkXxzxTlE5FVTL1zb7YmTOSzXGiEBU0ZgHzXexjd9HklDtTc2P7iR4/Wmqk/jGhfZXjZW1bYFVp3y01G+ocrh/K9VST3+05OUsaEnAYGKZRfWIpDQaXT2Ej2/vCl1S5nNe7jHq5eCAlM7rOpFx8PP1Zf/NzCUdkpXjUhHmdfdi/Xv31D6WccPAIDjNMmPnBzC+ErAipZzPf++LkQyGRhTDEpCNkbmLpz8892zmE3+8swq1YODIqf2Z7lO8RdJHn7RS8kpY6r0qhAg7xXIHnhViu+zBDbhcx16UOfGVgaGkoXe6LhwS+h7NgSa+vR7ESZvPyq6VUqN+SC0ZSTPm3oETGoxGIh/p60w3naIyJ/Gywf9CMnnAemR3524hT5DErxOwBhR55COMw3e+u0T0tOEsR0JMx+NBHftD/AJ+D/f7v/TW+9t+P+Bo9e/7vNYz+By6FsKkAAAB42mNgZGBgYGRwbI8IWhzPb/OVQZ6DAQTOni3fCKP/+/x7yrOBNRTI5WBgAokCAG3mDbAAeNpjYGRgYFX9t5eBgeftf5//WTwbGIAiKMAdAJycBph42mN6w+DCwcDAAMIsZ8D0HhBNLIap52D478fBwHQRyvbBpZ7nLYMtKeZjt5OJhxT1TKsYGFhDETTjcSAG0gyPoRgozigIpL0hNEiOBcgFAEBoNC142mNgYNCBwjoccALDBEY9RhsgPIMMmZcRhHtIhkcA9pQspAAAAQAAAEcBVAALAAAAAAACAAEAAgAWAAABAACTAAAAAHjalZCxTgJBFEXPApJoYYgF9VZUSIAFTdDCnmiIgsTKsASQuGiCu0YaCr4OfomKOzsTCHRmMzPn3blz38sCFyzJ4uXOgbKWZY+8KssZLqk7zkp9cJyjSOT4jD9WjvPSt46vKHoFx2txyfGGqnfPO18kyohSGjBjJPqRFmqPmWolWkZ9o0uHZ/EkfTNgTo0KVX017ujRps+TyDqvT7xW9U/UV1Vz9ZryrQn8o8QOL1JsdVA/5IwZpv7f/YsKTW50O1PqpzKNZyw1UnKov2c9dbkD7c1/zdhXFSrNdIz3HbuaJFH1KM9CZyDN3N3SoiFupfP66mbOYAd8k0EGAHjabc05TwJhHITxZ0BBBc/P4IkI7y4sh0dBsosHKiqHeLUiiTE0FH56Xdl/6TS/ZIoZUszzM+ad/3IOSilNmm122GWPfQ4ocEiRI0qUcXj4VKgSUKNOgybHnHDKGSER7Xjjgkuu6HDNDbd0ueOeB3r0GTDkkRFPPPPCK29a0KIyympJy1pRTnmtak3r2tCmtjLjz+/ph5edfU2cc2Fiy/3px4Xpmb5ZMatmYNbMutkwm2Yr0W8nBnOj+OcXVDk0PnjaRc67DoJAEAVQFuT9fqsJCSZ2+w12QkNjrCCx9w+sbSy19DsGK/9Ob3RZujk3k7nzZp8bsbvSkXXoR8Yew9gavN9QNHSUHTFch4oMfuoV0uqGNL4nv25emq3yHzzADwVcwOsFHMCtBWzAWQlYgJ0ImIA1rRmAeRbQAWM6vQD04A9GgXglRBo4Kh+19gJGYDgzBqOnZALGO8kUTLaSGZhWkjmYrSULMA8kS7CYi5ZgKTlQxr/W1F5aAAAAAAFRp8cxAAA=) format('woff'),url(font/ytp-regular.ttf) format('truetype');font-weight:400;font-style:normal}.mb_YTPlayer:focus{outline:0}.YTPWrapper{display:block;transform:translateZ(0) translate3d(0,0,0);transform-style:preserve-3d;perspective:1000;-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box}.mb_YTPlayer .loading{position:absolute;top:10px;right:10px;font-size:12px;color:#fff;background:rgba(0,0,0,.51);text-align:center;padding:2px 4px;border-radius:5px;font-family:"Droid Sans",sans-serif;-webkit-animation:fade .1s infinite alternate;animation:fade .1s infinite alternate}@-webkit-keyframes fade{0%{opacity:.5}100%{opacity:1}}@keyframes fade{0%{opacity:.5}100%{opacity:1}}.YTPFullscreen{display:block!important;position:fixed!important;width:100%!important;height:100%!important;top:0!important;left:0!important;margin:0!important;border:none!important;opacity:1!important;background-color:#000!important;padding:0!important}.mbYTP_wrapper iframe{max-width:4000px!important}.inline_YTPlayer{margin-bottom:20px;vertical-align:top;position:relative;left:0;overflow:hidden;border-radius:4px;box-shadow:0 0 5px rgba(0,0,0,.7);background:rgba(0,0,0,.5)}.inline_YTPlayer img{border:none!important;margin:0!important;padding:0!important;transform:none!important}.mb_YTPBar,.mb_YTPBar .buttonBar{left:0;padding:5px;width:100%;box-sizing:border-box}.mb_YTPBar .ytpicon{font-size:20px;font-family:ytpregular}.mb_YTPBar .mb_YTPUrl.ytpicon{font-size:30px}.mb_YTPBar{transition:opacity .5s;display:block;height:10px;background:#333;position:fixed;bottom:0;text-align:left;z-index:1000;font:14px/16px sans-serif;opacity:.1}.mb_YTPBar.visible,.mb_YTPBar:hover{opacity:1}.mb_YTPBar .buttonBar{transition:all .5s;background:0 0;font:12px/14px Calibri;position:absolute;top:-30px;height:40px}.mb_YTPBar:hover .buttonBar{background:rgba(0,0,0,.4)}.mb_YTPBar span{display:inline-block;font:16px/20px Calibri,sans-serif;position:relative;width:30px;height:25px;vertical-align:middle}.mb_YTPBar span.mb_YTPTime{width:130px}.mb_YTPBar span.mb_OnlyYT,.mb_YTPBar span.mb_YTPUrl{position:absolute;width:auto;display:block;top:6px;right:10px;cursor:pointer}.mb_YTPBar span.mb_YTPUrl img{width:60px}.mb_YTPBar span.mb_OnlyYT{left:300px;right:auto}.mb_YTPBar span.mb_OnlyYT img{width:25px}.mb_YTPBar .mb_YTPMuteUnmute,.mb_YTPBar .mb_YTPPlaypause,.mb_YTPlayer .mb_YTPBar .mb_YTPPlaypause img{cursor:pointer}.mb_YTPBar .mb_YTPProgress{height:10px;width:100%;background:#222;bottom:0;left:0}.mb_YTPBar .mb_YTPLoaded{height:10px;width:0;background:#444;left:0}.mb_YTPBar .mb_YTPseekbar{height:10px;width:0;background:#bb110e;bottom:0;left:0;box-shadow:rgba(82,82,82,.47) 1px 1px 3px}.mb_YTPBar .YTPOverlay{backface-visibility:hidden;-webkit-backface-visibility:hidden;-webkit-transform-style:"flat";box-sizing:border-box}.YTPOverlay.raster{background:url(images/raster.png)}.YTPOverlay.raster.retina{background:url(images/raster@2x.png)}.YTPOverlay.raster-dot{background:url(images/raster_dot.png)}.YTPOverlay.raster-dot.retina{background:url(images/raster_dot@2x.png)}.mb_YTPBar .simpleSlider{position:relative;width:100px;height:10px;border:1px solid #fff;overflow:hidden;box-sizing:border-box;margin-right:10px;cursor:pointer!important;border-radius:3px}.mb_YTPBar.compact .simpleSlider{width:40px}.mb_YTPBar .simpleSlider.muted{opacity:.3}.mb_YTPBar .level{position:absolute;left:0;bottom:0;background-color:#fff;box-sizing:border-box}.mb_YTPBar .level.horizontal{height:100%;width:0}.mb_YTPBar .level.vertical{height:auto;width:100%} \ No newline at end of file diff --git a/mixly/files/magnificpopup.css b/mixly/files/magnificpopup.css new file mode 100644 index 00000000..7f2979dc --- /dev/null +++ b/mixly/files/magnificpopup.css @@ -0,0 +1 @@ +.mfp-bg{top:0;left:0;width:100%;height:100%;z-index:1042;overflow:hidden;position:fixed;background:#0b0b0b;opacity:.8;filter:alpha(opacity=80)}.mfp-wrap{top:0;left:0;width:100%;height:100%;z-index:1043;position:fixed;outline:none!important;-webkit-backface-visibility:hidden}.mfp-container{text-align:center;position:absolute;width:100%;height:100%;left:0;top:0;padding:0 8px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.mfp-container:before{content:'';display:inline-block;height:100%;vertical-align:middle}.mfp-align-top .mfp-container:before{display:none}.mfp-content{position:relative;display:inline-block;vertical-align:middle;margin:0 auto;text-align:left;z-index:1045}.mfp-inline-holder .mfp-content,.mfp-ajax-holder .mfp-content{width:100%;cursor:auto}.mfp-ajax-cur{cursor:progress}.mfp-zoom-out-cur,.mfp-zoom-out-cur .mfp-image-holder .mfp-close{cursor:-moz-zoom-out;cursor:-webkit-zoom-out;cursor:zoom-out}.mfp-zoom{cursor:pointer;cursor:-webkit-zoom-in;cursor:-moz-zoom-in;cursor:zoom-in}.mfp-auto-cursor .mfp-content{cursor:auto}.mfp-close,.mfp-arrow,.mfp-preloader,.mfp-counter{-webkit-user-select:none;-moz-user-select:none;user-select:none}.mfp-loading.mfp-figure{display:none}.mfp-hide{display:none!important}.mfp-preloader{color:#ccc;position:absolute;top:50%;width:auto;text-align:center;margin-top:-.8em;left:8px;right:8px;z-index:1044}.mfp-preloader a{color:#ccc}.mfp-preloader a:hover{color:#fff}.mfp-s-ready .mfp-preloader{display:none}.mfp-s-error .mfp-content{display:none}button.mfp-close,button.mfp-arrow{overflow:visible;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;display:block;outline:none;padding:0;z-index:1046;-webkit-box-shadow:none;box-shadow:none}button::-moz-focus-inner{padding:0;border:0}.mfp-close{width:44px;height:44px;line-height:44px;position:absolute;right:0;top:0;text-decoration:none;text-align:center;opacity:.65;filter:alpha(opacity=65);padding:0 0 18px 10px;color:#fff;font-style:normal;font-size:28px;font-family:Arial,Baskerville,monospace}.mfp-close:hover,.mfp-close:focus{opacity:1;filter:alpha(opacity=100)}.mfp-close:active{top:1px}.mfp-close-btn-in .mfp-close{color:#333}.mfp-image-holder .mfp-close,.mfp-iframe-holder .mfp-close{color:#fff;right:-6px;text-align:right;padding-right:6px;width:100%}.mfp-counter{position:absolute;top:0;right:0;color:#ccc;font-size:12px;line-height:18px;white-space:nowrap}.mfp-arrow{position:absolute;opacity:.65;filter:alpha(opacity=65);margin:0;top:50%;margin-top:-55px;padding:0;width:90px;height:110px;-webkit-tap-highlight-color:transparent}.mfp-arrow:active{margin-top:-54px}.mfp-arrow:hover,.mfp-arrow:focus{opacity:1;filter:alpha(opacity=100)}.mfp-arrow:before,.mfp-arrow:after,.mfp-arrow .mfp-b,.mfp-arrow .mfp-a{content:'';display:block;width:0;height:0;position:absolute;left:0;top:0;margin-top:35px;margin-left:35px;border:medium inset transparent}.mfp-arrow:after,.mfp-arrow .mfp-a{border-top-width:13px;border-bottom-width:13px;top:8px}.mfp-arrow:before,.mfp-arrow .mfp-b{border-top-width:21px;border-bottom-width:21px;opacity:.7}.mfp-arrow-left{left:0}.mfp-arrow-left:after,.mfp-arrow-left .mfp-a{border-right:17px solid #fff;margin-left:31px}.mfp-arrow-left:before,.mfp-arrow-left .mfp-b{margin-left:25px;border-right:27px solid #3f3f3f}.mfp-arrow-right{right:0}.mfp-arrow-right:after,.mfp-arrow-right .mfp-a{border-left:17px solid #fff;margin-left:39px}.mfp-arrow-right:before,.mfp-arrow-right .mfp-b{border-left:27px solid #3f3f3f}.mfp-iframe-holder{padding-top:40px;padding-bottom:40px}.mfp-iframe-holder .mfp-content{line-height:0;width:100%;max-width:900px}.mfp-iframe-holder .mfp-close{top:-40px}.mfp-iframe-scaler{width:100%;height:0;overflow:hidden;padding-top:56.25%}.mfp-iframe-scaler iframe{position:absolute;display:block;top:0;left:0;width:100%;height:100%;box-shadow:0 0 8px rgba(0,0,0,.6);background:#000}img.mfp-img{width:auto;max-width:100%;height:auto;display:block;line-height:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:40px 0;margin:0 auto}.mfp-figure{line-height:0}.mfp-figure:after{content:'';position:absolute;left:0;top:40px;bottom:40px;display:block;right:0;width:auto;height:auto;z-index:-1;box-shadow:0 0 8px rgba(0,0,0,.6);background:#444}.mfp-figure small{color:#bdbdbd;display:block;font-size:12px;line-height:14px}.mfp-figure figure{margin:0}.mfp-bottom-bar{margin-top:-36px;position:absolute;top:100%;left:0;width:100%;cursor:auto}.mfp-title{text-align:left;line-height:18px;color:#f3f3f3;word-wrap:break-word;padding-right:36px}.mfp-image-holder .mfp-content{max-width:100%}.mfp-gallery .mfp-image-holder .mfp-figure{cursor:pointer}@media screen and (max-width:800px) and (orientation:landscape),screen and (max-height:300px){.mfp-img-mobile .mfp-image-holder{padding-left:0;padding-right:0}.mfp-img-mobile img.mfp-img{padding:0}.mfp-img-mobile .mfp-figure:after{top:0;bottom:0}.mfp-img-mobile .mfp-figure small{display:inline;margin-left:5px}.mfp-img-mobile .mfp-bottom-bar{background:rgba(0,0,0,.6);bottom:0;margin:0;top:auto;padding:3px 5px;position:fixed;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.mfp-img-mobile .mfp-bottom-bar:empty{padding:0}.mfp-img-mobile .mfp-counter{right:5px;top:3px}.mfp-img-mobile .mfp-close{top:0;right:0;width:35px;height:35px;line-height:35px;background:rgba(0,0,0,.6);position:fixed;text-align:center;padding:0}}@media all and (max-width:900px){.mfp-arrow{-webkit-transform:scale(.75);transform:scale(.75)}.mfp-arrow-left{-webkit-transform-origin:0;transform-origin:0}.mfp-arrow-right{-webkit-transform-origin:100%;transform-origin:100%}.mfp-container{padding-left:6px;padding-right:6px}}.mfp-ie7 .mfp-img{padding:0}.mfp-ie7 .mfp-bottom-bar{width:600px;left:50%;margin-left:-300px;margin-top:5px;padding-bottom:5px}.mfp-ie7 .mfp-container{padding:0}.mfp-ie7 .mfp-content{padding-top:44px}.mfp-ie7 .mfp-close{top:0;right:0;padding-top:0} \ No newline at end of file diff --git a/mixly/files/mixly.icns b/mixly/files/mixly.icns new file mode 100644 index 00000000..456186c8 Binary files /dev/null and b/mixly/files/mixly.icns differ diff --git a/mixly/files/mixly.ico b/mixly/files/mixly.ico new file mode 100644 index 00000000..cf2ee95b Binary files /dev/null and b/mixly/files/mixly.ico differ diff --git a/mixly/files/mixly_uncompressed.ico b/mixly/files/mixly_uncompressed.ico new file mode 100644 index 00000000..ea9de51a Binary files /dev/null and b/mixly/files/mixly_uncompressed.ico differ diff --git a/mixly/files/owl.carousel.min.css b/mixly/files/owl.carousel.min.css new file mode 100644 index 00000000..1ece042a --- /dev/null +++ b/mixly/files/owl.carousel.min.css @@ -0,0 +1,6 @@ +/** + * Owl Carousel v2.2.1 + * Copyright 2013-2017 David Deutsch + * Licensed under () + */ +.owl-carousel,.owl-carousel .owl-item{-webkit-tap-highlight-color:transparent;position:relative}.owl-carousel{display:none;width:100%;z-index:1}.owl-carousel .owl-stage{position:relative;-ms-touch-action:pan-Y;-moz-backface-visibility:hidden}.owl-carousel .owl-stage:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}.owl-carousel .owl-stage-outer{position:relative;overflow:hidden;-webkit-transform:translate3d(0,0,0)}.owl-carousel .owl-item,.owl-carousel .owl-wrapper{-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0)}.owl-carousel .owl-item{min-height:1px;float:left;-webkit-backface-visibility:hidden;-webkit-touch-callout:none}.owl-carousel .owl-item img{display:block;width:100%}.owl-carousel .owl-dots.disabled,.owl-carousel .owl-nav.disabled{display:none}.no-js .owl-carousel,.owl-carousel.owl-loaded{display:block}.owl-carousel .owl-dot,.owl-carousel .owl-nav .owl-next,.owl-carousel .owl-nav .owl-prev{cursor:pointer;cursor:hand;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel.owl-loading{opacity:0;display:block}.owl-carousel.owl-hidden{opacity:0}.owl-carousel.owl-refresh .owl-item{visibility:hidden}.owl-carousel.owl-drag .owl-item{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel.owl-grab{cursor:move;cursor:grab}.owl-carousel.owl-rtl{direction:rtl}.owl-carousel.owl-rtl .owl-item{float:right}.owl-carousel .animated{animation-duration:1s;animation-fill-mode:both}.owl-carousel .owl-animated-in{z-index:0}.owl-carousel .owl-animated-out{z-index:1}.owl-carousel .fadeOut{animation-name:fadeOut}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.owl-height{transition:height .5s ease-in-out}.owl-carousel .owl-item .owl-lazy{opacity:0;transition:opacity .4s ease}.owl-carousel .owl-item img.owl-lazy{transform-style:preserve-3d}.owl-carousel .owl-video-wrapper{position:relative;height:100%;background:#000}.owl-carousel .owl-video-play-icon{position:absolute;height:80px;width:80px;left:50%;top:50%;margin-left:-40px;margin-top:-40px;background:url(owl.video.play.png) no-repeat;cursor:pointer;z-index:1;-webkit-backface-visibility:hidden;transition:transform .1s ease}.owl-carousel .owl-video-play-icon:hover{-ms-transform:scale(1.3,1.3);transform:scale(1.3,1.3)}.owl-carousel .owl-video-playing .owl-video-play-icon,.owl-carousel .owl-video-playing .owl-video-tn{display:none}.owl-carousel .owl-video-tn{opacity:0;height:100%;background-position:center center;background-repeat:no-repeat;background-size:contain;transition:opacity .4s ease}.owl-carousel .owl-video-frame{position:relative;z-index:1;height:100%;width:100%} \ No newline at end of file diff --git a/mixly/files/responsive.css b/mixly/files/responsive.css new file mode 100644 index 00000000..f125f784 --- /dev/null +++ b/mixly/files/responsive.css @@ -0,0 +1 @@ +@media only screen and (min-width:1200px) and (max-width:1367px){.download-btns a{margin-top:25px!important}}@media only screen and (min-width:992px) and (max-width:1200px){.main-menu nav ul li a{padding:29px 13px}.flex-direction{flex-direction:column}.wrapper:before{content:'';float:right;display:block;width:300px;height:150px;margin:0 0 15px 15px}.zin1{z-index:1}.vis-btns a{z-index:2}.buttons-box{position:absolute;top:0;right:0;width:300px}.div1{width:202px;height:131px;border-right:none;border-bottom:1px solid #000}.div2{width:202px;height:107px}.div2 span{line-height:107px}.screen-area img.screen-img{bottom:171px;width:434px;height:453px}.download-btns a{margin-top:25px!important}#one{float:none;margin-right:0;width:auto;border:0;border-bottom:2px solid #000}.blog-post-area{padding-top:90px}.bp-with-sidebar{padding-bottom:80px}.pagination-area{padding-bottom:70px}.blog-details{padding:100px 0}}@media only screen and (min-width:768px) and (max-width:991px){.menu-area{display:block}.download-btns a{margin-top:25px!important}.logo{padding:21px 0 17px}.slider-area{height:646px;padding-top:0}.service-single h2{font-size:22px}.service-single p{font-size:14px}.about-area{padding:90px 0 70px}.feature-area{padding:90px 0 40px}.ft-content{margin-top:0}.ft-single h2{font-size:22px}.screen-area,.testimonial-area,.pricing-area,.clinet-area,.contact-area{padding:90px 0}.screen-area img.screen-img{bottom:137px;width:529px;height:584px}.prc-head h5{font-size:35px}.prc-head span{font-size:21px;margin-bottom:7px}.team-area{padding:90px 0 70px}.single-team{margin-bottom:20px}.download-btns a{margin-bottom:20px}.blog-post{padding:90px 0 20px}.single-post{margin-bottom:50px;border-bottom:1px solid #efebeb;padding-bottom:30px}.blog-post-area{padding-top:80px}.bp-with-sidebar{padding-bottom:70px}.pagination-area{padding-bottom:70px}.blog-details{padding:90px 0}.sidebar--area{padding-left:0}.crumbs-inner h2{font-size:31px}}@media screen and (max-width:991px){.download-btns a{margin-top:25px!important}}@media only screen and (max-width:767px){.screen-area img.screen-img{display:none}.download-btns a{margin-top:25px!important}}@media only screen and (min-width:481px) and (max-width:767px){#preloader{display:none!important}.menu-area{display:block}.logo{padding:21px 0 17px}.download-btns a{margin-top:25px!important}.slider-area{height:auto;padding:114px 0 149px}.slider-inner{text-align:left}.slider-inner h2{font-size:33px}.slider-inner h5{font-size:23px;line-height:27px;margin-bottom:42px;margin-top:5px}.slider-inner-black{text-align:left}.slider-inner-black h2{font-size:33px}.slider-inner-black h5{font-size:23px;line-height:27px;margin-bottom:42px;margin-top:5px}.service-single{margin-bottom:30px}.service-single h2{font-size:25px}.about-area{padding:60px 0}.section-title p{font-size:16px}.feature-area{padding:80px 0 40px}.ft-content{margin-top:0}.ft-screen-img{margin-bottom:40px}.ach-single{margin-bottom:70px}.achivement-area{padding:80px 0 10px}.screen-area,.testimonial-area,.video-area,.call-to-action,.clinet-area,.contact-area{padding:80px 0}.screen-slider{margin-top:0}.screen-slider .owl-dots{margin-top:40px}.video-area h2{font-size:25px}.pricing-area,.team-area{padding:80px 0 60px}.single-price,.single-team{margin-bottom:30px}.download-btns a{margin:5px 0}.blog-post{padding:80px 0 40px}.single-post{margin-bottom:50px;border-bottom:1px solid #efebeb;padding-bottom:30px}.contact_info{padding-left:0;margin-top:40px}.crumbs-area{display:block;height:354px}.crumbs-inner{width:80%;margin-top:148px;padding:40px 0}.blog-post-area{padding-top:70px}.bp-with-sidebar{padding-bottom:60px}.pagination-area{padding-bottom:80px;padding-top:20px}.blog-details{padding:70px 0}.children{padding-left:12px}.sidebar--area{padding-left:0;max-width:420px;margin:auto;margin-top:55px}.crumbs-inner h2{font-size:26px}}@media only screen and (min-width:600px) and (max-width:767px){.col-6{float:left;width:50%}.download-btns a{margin-top:25px!important}}@media only screen and (min-width:240px) and (max-width:480px){#preloader{display:none!important}.menu-area{display:block}.logo{padding:21px 0 17px}.download-btns a{margin-top:25px!important}.slider-area{height:auto;padding:114px 0 149px}.slider-inner{text-align:left}.slider-inner h2{font-size:23px}.slider-inner h5{font-size:20px;line-height:27px;margin-bottom:42px;margin-top:0}.slider-inner-black{text-align:left}.slider-inner-black h2{font-size:23px}.slider-inner-black h5{font-size:20px;line-height:27px;margin-bottom:42px;margin-top:0}.service-single{margin-bottom:30px}.service-single h2{font-size:25px}.about-area{padding:50px 0}.section-title p{font-size:16px}.feature-area{padding:70px 0 30px}.ft-content{margin-top:0}.ft-screen-img{margin-bottom:40px}.ach-single{margin-bottom:70px}.achivement-area{padding:70px 0 0}.screen-area,.testimonial-area,.video-area,.call-to-action,.clinet-area,.contact-area{padding:70px 0}.screen-slider{margin-top:0}.screen-slider .owl-dots{margin-top:40px}.video-area h2{font-size:25px}.pricing-area,.team-area{padding:70px 0 40px}.single-price,.single-team{margin-bottom:30px}.download-btns a{margin:5px 0}.blog-post{padding:70px 0 20px}.single-post{margin-bottom:50px;border-bottom:1px solid #efebeb;padding-bottom:30px}.contact_info{padding-left:0;margin-top:40px}.crumbs-area{display:block;height:354px}.crumbs-inner h2{font-size:22px}.crumbs-inner{width:100%;margin-top:148px;padding:40px 0}.blog-post-area{padding-top:60px}.bp-with-sidebar{padding-bottom:60px}.pagination-area{padding-bottom:70px;padding-top:20px}.pagination ul{padding:4px 0}.pagination ul li a,.pagination ul li span{padding:10px 5px;font-size:11px}.blog-details{padding:60px 0}.children{padding-left:12px}.sidebar--area{padding-left:0;margin-top:60px}#preloader{display:none!important}#one{float:none;margin-right:0;width:auto;border:0;border-bottom:2px solid #000}} \ No newline at end of file diff --git a/mixly/files/slicknav.min.css b/mixly/files/slicknav.min.css new file mode 100644 index 00000000..389d9952 --- /dev/null +++ b/mixly/files/slicknav.min.css @@ -0,0 +1,5 @@ +/*! + * SlickNav Responsive Mobile Menu v1.0.10 + * (c) 2016 Josh Cope + * licensed under MIT + */.slicknav_btn,.slicknav_nav .slicknav_item{cursor:pointer}.slicknav_menu,.slicknav_menu *{box-sizing:border-box}.slicknav_btn{position:relative;display:block;vertical-align:middle;float:right;padding:.438em .625em;line-height:1.125em}.slicknav_btn .slicknav_icon-bar+.slicknav_icon-bar{margin-top:.188em}.slicknav_menu .slicknav_menutxt{display:block;line-height:1.188em;float:left;color:#fff;font-weight:700;text-shadow:0 1px 3px #000}.slicknav_menu .slicknav_icon{float:left;width:1.125em;height:.875em;margin:.188em 0 0 .438em}.slicknav_menu .slicknav_icon:before{background:0 0;width:1.125em;height:.875em;display:block;content:"";position:absolute}.slicknav_menu .slicknav_no-text{margin:0}.slicknav_menu .slicknav_icon-bar{display:block;width:1.125em;height:.125em;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,.25);box-shadow:0 1px 0 rgba(0,0,0,.25)}.slicknav_menu:after,.slicknav_menu:before{content:" ";display:table}.slicknav_menu:after{clear:both}.slicknav_nav li,.slicknav_nav ul{display:block}.slicknav_nav .slicknav_arrow{font-size:.8em;margin:0 0 0 .4em}.slicknav_nav .slicknav_item a{display:inline}.slicknav_nav .slicknav_row,.slicknav_nav a{display:block}.slicknav_nav .slicknav_parent-link a{display:inline}.slicknav_menu{*zoom:1;font-size:16px;background:#4c4c4c;padding:5px}.slicknav_nav,.slicknav_nav ul{list-style:none;overflow:hidden;padding:0}.slicknav_menu .slicknav_icon-bar{background-color:#fff}.slicknav_btn{margin:5px 5px 6px;text-decoration:none;text-shadow:0 1px 1px rgba(255,255,255,.75);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;background-color:#222}.slicknav_nav{clear:both;color:#fff;margin:0;font-size:.875em}.slicknav_nav ul{margin:0 0 0 20px}.slicknav_nav .slicknav_row,.slicknav_nav a{padding:5px 10px;margin:2px 5px}.slicknav_nav .slicknav_row:hover{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;background:#ccc;color:#fff}.slicknav_nav a{text-decoration:none;color:#fff}.slicknav_nav a:hover{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;background:#ccc;color:#222}.slicknav_nav .slicknav_txtnode{margin-left:15px}.slicknav_nav .slicknav_item a,.slicknav_nav .slicknav_parent-link a{padding:0;margin:0}.slicknav_brand{float:left;color:#fff;font-size:18px;line-height:30px;padding:7px 12px;height:44px} \ No newline at end of file diff --git a/mixly/files/style.css b/mixly/files/style.css new file mode 100644 index 00000000..602c59a6 --- /dev/null +++ b/mixly/files/style.css @@ -0,0 +1 @@ +@font-face {font-family: 'Poppins';font-style: normal;font-weight: 700;src: local('Poppins Bold'), local('Poppins-Bold'), url(fonts/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face {font-family: 'Poppins';font-style: normal;font-weight: 500;src: local('Poppins Medium'), local('Poppins-Medium'), url(fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face {font-family: 'Poppins';font-style: normal;font-weight: 400;src: local('Poppins Regular'), local('Poppins-Regular'), url(fonts/pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2');unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;};.clear{clear:both}::-moz-selection{background:#00b0ff;color:#fff;text-shadow:none}::selection{background:#00b0ff;color:#fff;text-shadow:none}.no-mar{margin:0!important}.no-pad{padding:0!important}.no-top-pad{padding-top:0!important}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.bg-theme{background-color:#ef525b}.bg-gray{background-color:#fafafa}*,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}*:focus{outline:0}html{font-size:62.5%;-webkit-font-smoothing:antialiased}body{background:#fff;font-weight:400;font-size:15px;/*letter-spacing:1.5px;*/color:#888;line-height:30px;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%}img{max-width:100%;height:auto}button,input,textarea{letter-spacing:1px}.header-area{position:fixed;left:0;top:0;width:100%;z-index:998}.menu-area{display:flex;align-items:center}.logo{}.logo a{display:inline-block}.logo a img{height:50px}.main-menu{text-align:right}.main-menu nav ul{}.main-menu nav ul li{display:inline-block}.main-menu nav ul li a{display:block;font-size:14px;font-weight:400;color:#fff;letter-spacing:0;padding:27px 36px;position:relative;padding-left:0}.main-menu nav ul li a:before{content:'';position:absolute;left:0;bottom:22px;height:2px;width:0%;background-color:#fff;-webkit-transition:all .3s ease 0s;-o-transition:all .3s ease 0s;transition:all .3s ease 0s}.main-menu nav ul li:hover a:before,.main-menu nav ul li.active a:before{width:70%}.main-menu-black{text-align:right}.main-menu-black nav ul{}.main-menu-black nav ul li{display:inline-block}.main-menu-black nav ul li a{display:block;font-size:14px;font-weight:400;color:#000;letter-spacing:0;padding:27px 36px;position:relative;padding-left:0}.main-menu-black nav ul li a:before{content:'';position:absolute;left:0;bottom:22px;height:2px;width:0%;background-color:#000;-webkit-transition:all .3s ease 0s;-o-transition:all .3s ease 0s;transition:all .3s ease 0s}.main-menu-black nav ul li:hover a:before,.main-menu-black nav ul li.active a:before{width:70%}.mobile_menu{}.slicknav_menu .slicknav_menutxt{display:none}.slicknav_menu{background:0 0;margin-top:21px}.slicknav_menu .slicknav_icon-bar{height:2px;width:19px;margin:3px 0;-webkit-transition:all .3s ease 0s;transition:all .3s ease 0s}.slicknav_btn{background-color:transparent;position:relative;margin-top:-51px}.slicknav_menu .slicknav_open .slicknav_icon-bar:nth-child(2){opacity:0;-ms-filter:"alpha(opacity=0)"}.slicknav_menu .slicknav_open .slicknav_icon-bar:nth-child(1){-webkit-transform:rotate(45deg) translate(1px,7px);transform:rotate(45deg) translate(1px,7px)}.slicknav_menu .slicknav_open .slicknav_icon-bar:nth-child(3){-webkit-transform:rotate(-45deg) translateY(-6px);transform:rotate(-45deg) translateY(-6px)}.slicknav_menu{margin-top:0}.slicknav_nav{background:#400bc4;margin:10px;padding:6px 0}.slicknav_nav a:hover{background:#fefefe none repeat scroll 0 0;border-radius:0}.slicknav_nav a{font-size:14px;letter-spacing:.01em}.text-white ul:first-child li:before{content:"\2714\0020"}.slider-area{display:flex;align-items:center;height:390px;background:url(background.jpg) center center/cover no-repeat!important;padding-top:80px}.height-500{height:500px!important}.microbit-col{background-image:linear-gradient(122deg,#00c800 -3%,#3eb6fd 49%)!important}.microbit-black{color:#fff!important}.border-microbit-black{border-color:#fff!important}.pi-col{background:#c51a4a!important}.python-col{background:#4b8bbe!important}.circuitpy-col{background:#64338e!important}.microbit-back:before{background-color:rgba(0,237,0,.7)!important}.pi-back:before{background-color:rgba(197,26,74,.7)!important}.python-back:before{background-color:rgba(75,139,190,.9)!important}.circuitpy-back:before{background-color:rgba(100,51,142,.7)!important}.microbit-image{background:url(https://i.ibb.co/F3Sr3g7/25965969530-d6f0047d0a-o.png) center center/cover no-repeat;background-color:rgba(62,182,253,.8)!important}.w-390{width:360px!important;height:680px!important}.pi-image{background:url(https://i.ibb.co/d7FW3D5/40759292832-4ae5e77534-k.jpg) center center/cover no-repeat;background-color:rgba(197,26,74,.7)!important}.video-image{background:url(https://i.ibb.co/VWLnY27/v310-banner.png) center center/cover no-repeat!important;background-color:rgba(27,23,61,.9)!important}.python-image{background:url(https://cdn-images-1.medium.com/max/1600/0*_Tc8a2lHRSbsyzhI.) center center/cover no-repeat}.circuitpy-image{background:url(https://nerdtech.biz/user/pages/05.blog/cpe/CPE.jpg) center center/cover no-repeat}.slider-img{}.slider-img img{max-height:100%}.slider-inner{}.slider-inner h2{font-size:48px;font-weight:700;color:#fff;letter-spacing:0;line-height:49px}.slider-inner h5{font-size:30px;font-weight:500;color:#fff;letter-spacing:0;line-height:39px;margin-bottom:50px;margin-top:5px}.slider-inner a{display:inline-block;position:relative;height:60px;width:270px;background:0 0;border:2px solid #fff;border-radius:30px;text-align:center;line-height:56px;color:#fff;font-size:20px;font-weight:400;letter-spacing:0;padding-left:44px}.slider-inner-black h2{font-size:48px;font-weight:700;color:#fff;letter-spacing:0;line-height:49px}.slider-inner-black h5{font-size:30px;font-weight:500;color:#fff;letter-spacing:0;line-height:39px;margin-bottom:50px;margin-top:5px}.slider-inner-black a{display:inline-block;position:relative;height:60px;width:270px;background:0 0;border:2px solid #000;border-radius:30px;text-align:center;line-height:56px;color:#000;font-size:20px;font-weight:400;letter-spacing:0;padding-left:44px}.slider-inner a i{position:absolute;left:16px;top:6px;height:44px;width:44px;border:2px solid #fff;border-radius:50%;line-height:40px;font-size:16px;padding-left:2px}.slider-inner-black a i{position:absolute;left:16px;top:6px;height:44px;width:44px;border:2px solid #000;border-radius:50%;line-height:40px;font-size:16px;padding-left:2px}.with-gradiant{position:relative}.with-gradiant:before{content:'';position:absolute;left:0;top:0;height:100%;width:100%;opacity:.8;background:transparent linear-gradient(90deg,#ffdc57,#5b1ffa) repeat scroll 0 0}.full-opacity:before{opacity:1;background:transparent linear-gradient(90deg,#ffdc57,#5b1ffa) repeat scroll 0 0}.warm-canvas{background:transparent linear-gradient(90deg,#ffdc57,#5b1ffa) repeat scroll 0 0}canvas.worms.sketch{max-width:100%}.service-area{position:relative;margin-top:-100px;background-color:transparent}.service-single{padding:20px 0;background:#fff;border-radius:20px;text-align:center;box-shadow:0 0 8px rgba(0,0,0,.1)}.service-single img{max-width:150px;margin-bottom:15px}.service-single h2{font-size:30px;font-weight:500;color:#232323;letter-spacing:0;margin-bottom:10px}.service-single p{font-weight:500;color:#666;font-size:15px}.about-area{}.section-title{text-align:center;margin-bottom:50px}.section-title h2{font-size:30px;font-weight:500;color:#232323;letter-spacing:0;line-height:37px;position:relative;margin-bottom:20px;padding-bottom:20px}.section-title.text-white h2{color:#fff}.section-title h2:before{content:'';position:absolute;left:0;bottom:0;height:2px;width:130px;background-color:#1b173d;left:calc(50% - 65px)}.microbit-green h2:before{background-image:linear-gradient(122deg,#00c800 -3%,#3eb6fd 49%)}.pi-red h2:before{background-color:#c51a4a!important}.circuitpy-purple h2:before{background-color:#64338e!important}.python-blue h2:before{background-color:#4b8bbe!important}.section-title.text-white h2:before{background-color:#fff}.section-title.text-white h2:after{background-color:#fff}.section-title p{font-size:20px;font-weight:500;color:#666}.section-title.text-white p{color:#fff}.about-left-img{}.about-left-img img{max-width:100%}.about-content{}.about-content p{color:#666;margin-bottom:25px}.feature-area{}.ft-content{margin-top:76px}.ft-single{margin-bottom:46px}.ft-single img{float:left;margin-right:15px}.ft-content.rtl img{float:right;margin-left:15px;margin-right:0}.ft-content.rtl .meta-content{text-align:right}.ft-single h2{font-size:25px;font-weight:500;color:#232323;letter-spacing:0;line-height:31px;margin-bottom:8px}.ft-single p{color:#666}.ft-screen-img{text-align:center}.ft-screen-img img{max-width:100%}.achivement-area{position:relative;background:url(../img/bg/ach-bg-img.jpg) center center/cover no-repeat}.achivement-area:before{content:'';position:absolute;left:0;top:0;height:100%;width:100%;background-color:rgba(27,23,61,.9)}.ach-single{text-align:center}.ach-single .icon{font-size:24px;color:#fff;margin-bottom:35px}.ach-single>p,.ach-single>span{font-size:41px;font-weight:700;color:#fff;letter-spacing:0;display:inline-block;margin-bottom:20px}.ach-single h5{font-size:19px;font-weight:400;color:#fff;letter-spacing:0}.screen-area{position:relative}.screen-area img.screen-img{position:absolute;left:50%;bottom:152px;width:506px;height:546px;z-index:111;transform:translateX(-50%)}.screen-slider{margin-top:116px}.screen-slider .owl-dots{text-align:center;margin-top:130px}.screen-slider .owl-dots .owl-dot{height:15px;width:15px;border:1px solid #5a10fb;border-radius:50%;display:inline-block;margin:0 2px;-webkit-transition:all .3s ease 0s;-o-transition:all .3s ease 0s;transition:all .3s ease 0s}.screen-slider .owl-dots .active{background-color:#5a10fb}.testimonial-area{}.testimonial-slider{}.single-tst{text-align:center;border:2px solid #5a10fb;border-radius:10px;margin-top:109px;padding:0 8%;padding-bottom:24px}.single-tst img{max-width:134px;margin:auto;position:relative;margin-top:-70px;z-index:1;margin-bottom:22px;border-radius:50%;box-shadow:0 0 8px rgba(0,0,0,.1)}.single-tst h4{font-size:20px;font-weight:500;color:#232323;letter-spacing:0}.single-tst span{font-size:15px;color:#666;letter-spacing:0;font-weight:500}.single-tst p{color:#666;margin:8px 0 20px}.tst-social li{display:inline-block}.tst-social li a{font-size:20px;color:#555;margin:0 5px}.tst-social li a:hover{color:#5a0efb}.testimonial-slider .owl-dots{text-align:center;margin-top:50px}.testimonial-slider .owl-dots .owl-dot{height:15px;width:15px;border:1px solid #5a10fb;border-radius:50%;display:inline-block;margin:0 2px;-webkit-transition:all .3s ease 0s;-o-transition:all .3s ease 0s;transition:all .3s ease 0s}.testimonial-slider .owl-dots .active{background-color:#5a10fb}.video-area{text-align:center;position:relative}.video-area:before{content:'';position:absolute;left:0;top:0;height:100%;width:100%;background-color:rgba(27,23,61,.9)}.video-area h2{font-size:40px;font-weight:500;color:#fff;letter-spacing:0;margin-bottom:14px;line-height:42px}.video-area p{color:#fff;font-weight:500;font-size:14px}.video-area a{font-size:22px;color:#f89621;height:50px;width:50px;position:relative;border-radius:50%;text-align:center;line-height:53px;padding-left:6px;background:#fff;display:inline-block;margin-top:50px}.video-area a:before,.video-area a:after{content:'';position:absolute;left:0;top:0;height:100%;width:100%;border:1px solid #fff;border-radius:50%}.video-area a:before{-webkit-animation:scaling 1s linear 0s infinite;-o-animation:scaling 1s linear 0s infinite;animation:scaling 1s linear 0s infinite}.video-area a:after{-webkit-animation:scaling 1s linear .3s infinite;-o-animation:scaling 1s linear .3s infinite;animation:scaling 1s linear .3s infinite}@keyframes scaling{0%{opacity:0;-webkit-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}15%{opacity:.7;-webkit-transform:scale(1.05);-ms-transform:scale(1.05);-o-transform:scale(1.05);transform:scale(1.05)}100%{opacity:0;-webkit-transform:scale(2);-ms-transform:scale(2);-o-transform:scale(2);transform:scale(2)}}.pricing-area{}.single-price{border-radius:5px;overflow:hidden;box-shadow:0 0 9px rgba(0,0,0,.05);padding-bottom:35px;text-align:center;-webkit-transition:all .3s ease 0s;-o-transition:all .3s ease 0s;transition:all .3s ease 0s}.single-price:hover{box-shadow:0 3px 10px rgba(0,0,0,.1)}.prc-head{background:#f89621;text-align:center;padding:40px}.prc-head span{font-size:24px;font-weight:500;color:#fff;letter-spacing:0;margin-bottom:12px;display:block}.prc-head h5{font-size:40px;color:#fff;letter-spacing:0;font-weight:500;line-height:53px}.prc-head h5 small{color:#fff}.single-price ul{text-align:center;margin-top:10px;padding:20px}.single-price img{padding-bottom:10px}.single-price ul li{font-weight:400;font-size:14px;color:#666;line-height:16px;margin-top:19px;letter-spacing:0}.single-price a{font-size:18px;font-weight:400;letter-spacing:0;color:#333;border:1px solid #1b173d;padding:5px 21px;border-radius:3px;display:inline-block}.single-price a:hover{color:#fff;background-color:#1b173d}.team-area{}.single-team{text-align:center;overflow:hidden;border-radius:3px;padding-bottom:30px;box-shadow:0 0 5px rgba(0,0,0,.1);-webkit-transition:all .3s ease 0s;-o-transition:all .3s ease 0s;transition:all .3s ease 0s}.single-team:hover{box-shadow:0 3px 5px rgba(0,0,0,.2)}.team-thumb{margin:40px 0}.team-thumb img{max-width:160px;border-radius:50%}.single-team h4{font-size:20px;font-weight:500;color:#232323;letter-spacing:0;margin-bottom:8px}.single-team span{display:block;color:#666;font-weight:400;letter-spacing:0}.single-team ul{margin-top:40px}.call-to-action{position:relative;z-index:1;background:url(../img/bg/ach-bg-img.jpg) center center/cover no-repeat}.call-to-action:before{content:'';position:absolute;left:0;top:0;height:100%;width:100%;z-index:-1;background-color:rgba(27,23,61,.9)}#one{float:left}#two{float:right}.download-btns a{margin:0 32px;height:50px;width:195px;border:2px solid #fff;display:inline-block;text-align:center;line-height:46px;border-radius:33px;font-weight:500;font-size:14px;color:#fff;letter-spacing:0}.download-btns a:hover{background-color:#fff;color:#1b173d}.download-btns a i{font-size:19px;margin-right:6px;vertical-align:middle}.wrapper{position:relative}.learn-link{padding:25px 0!important}.button-box{}.vis-btns a{margin:0 32px;height:50px;width:195px;border:2px solid #fff;display:inline-block;text-align:center;line-height:46px;border-radius:33px;font-weight:500;font-size:14px;color:#fff;letter-spacing:0}.vis-btns a:hover{background-color:#fff;color:#1b173d}.vis-btns a i{font-size:19px;margin-right:6px;vertical-align:middle}.blog-post{}.single-post{}.single-post a{display:block;margin-bottom:7px}.single-post a>img{border-radius:5px 5px 0 0;max-width:100%;margin-bottom:20px}.single-post .blog-meta{}.single-post .blog-meta ul{}.single-post .blog-meta ul li{display:inline-block;font-size:12px;color:#666;letter-spacing:0;margin-right:10px}.single-post .blog-meta ul li i{margin-right:5px;color:#444}.single-post h2 a{display:block;font-size:20px;font-weight:500;color:#232323;margin-bottom:11px;margin-top:7px;letter-spacing:0}.single-post h2 a:hover{color:#5a10fb}.single-post p{color:#555;letter-spacing:.01em}.clinet-area{}.client-carousel{}.client-carousel img{border:1px solid #8422f9;border-left:none;border-right:none;padding:15px 0}.contact-area{}.contact-form{}.contact-form form{}.contact-form input,.contact-form textarea{width:100%;height:45px;margin-bottom:20px;padding-left:15px;border-radius:3px;border:none;box-shadow:0 1px 5px rgba(0,0,0,.1);color:#222}.contact-form textarea{min-height:165px}.contact-form #send{height:45px;width:150px;background:#5a10fb;color:#fff;text-align:center;line-height:45px;-webkit-transition:all .3s ease 0s;-o-transition:all .3s ease 0s;transition:all .3s ease 0s}.contact-form #send:hover{background-color:#444}.contact_info{padding-left:40px}.contact_info .s-info{margin-bottom:14px}.s-info i{font-size:21px;color:#f89621;float:left;margin-right:14px;margin-top:6px}.s-info .meta-content{overflow:hidden}.s-info .meta-content span{font-size:15px;color:#666;font-weight:400;display:block}.c-social{margin-top:25px}.c-social ul{}.c-social ul li{display:inline-block}.c-social ul li a{display:block;height:50px;width:50px;border-radius:50%;background-color:#f89621;color:#fff;text-align:center;line-height:50px;font-size:20px;margin-right:6px}.c-social ul li a:hover{background-color:#444}.footer-area{height:80px;text-align:center;background-color:#1b173d;display:flex;align-items:center}.footer-area p{color:#fff}.blog-list{}.list-item{box-shadow:0 5px 30px 0 rgba(0,0,0,.1);padding:15px 15px 26px}.blog-thumbnail{margin-bottom:15px}.blog-thumbnail a{display:block}.blog-thumbnail a img{max-width:100%}.list-item h2.blog-title a{display:block;font-size:18px;font-weight:500;color:#272727;letter-spacing:0;margin-bottom:9px}.list-item h2.blog-title a:hover{color:#5b1ffa}.blog-meta{}.blog-meta ul{}.blog-meta ul li{display:inline-block;font-size:14px;color:#272727;font-weight:500;margin-right:9px}.blog-meta ul li i{margin-right:4px}.blog-summery{margin:7px 0 25px}.blog-summery p{color:#696969}.list-item a.read-more{width:130px;height:40px;background:#5b1ffa;display:block;text-align:center;line-height:40px;color:#fff;letter-spacing:.01em}.list-item a.read-more:hover{box-shadow:0 5px 13px 0 rgba(0,0,0,.2)}.blog-info h2.blog-title a{display:block;font-size:18px;color:#444;letter-spacing:0;margin-bottom:10px}.blog-info h2.blog-title a:hover{color:#e8313b}.blog-single-tags{margin-top:67px}.blog-single-tags h2{font-size:24px;font-weight:400;color:#444;letter-spacing:0;margin-bottom:32px;font-weight:500;text-transform:uppercase}.blog-single-tags ul{list-style-type:none}.blog-single-tags ul li{display:inline-block}.blog-single-tags ul li a{display:inline-block;padding:9px 15px;background:#5b1ffa;color:#fff;text-transform:uppercase;line-height:14px;letter-spacing:.04em;font-size:11px;margin-bottom:11px;margin-right:12px;box-shadow:0 0 5px rgba(0,0,0,.1)}.blog-single-tags ul li:last-child a{margin-bottom:0}.blog-single-tags ul li a:hover{background-color:#e8313b}.comment-area{padding-bottom:61px;border-bottom:1px solid #efeaea}.comment-title{}.comment-title h4{font-size:24px;font-weight:400;color:#444;letter-spacing:0}.comment-title h4 span{color:#ccc}.comment-area ul{list-style-type:none}.comment-list{}.comment-info-inner{}.comment-info-inner article{padding:51px 0 0 102px;position:relative}.comment-author{position:relative}.comment-author img{position:absolute;left:-103px;top:-1px;max-width:80px;border-radius:50%;box-shadow:1px 5px 7px -2px rgba(0,0,0,.3)}.comment-author h2{font-size:16px;font-weight:700;color:#555;line-height:13px;font-family:open sans,sans-serif;letter-spacing:0}.meta-data{margin:2px 0 10px}.meta-data p.category{font-size:12px;font-weight:600;color:#333}.meta-data p.category span{color:#ef4836}.comment-content{}.comment-content p{font-size:14px;font-weight:400;color:#666;letter-spacing:.02em;line-height:26px}.comment-reply{position:absolute;right:0;top:48px}.comment-reply a{}.children{padding-left:104px}.related-post{}.related-post .rl-post-title h2,.comment-title h4{font-size:24px;font-weight:400;letter-spacing:0;color:#444;margin-bottom:47px}.related-post-list .single-blog{padding:0}.related-post-list>div>div:last-child .single-blog{margin-bottom:0}.related-post-list .blog-content{padding:0 15px 20px}.comment-title,.rl-post-title{margin-top:62px}.comment-title h4{}.leave-comment form{}.leave-comment form input[type=text]{width:100%;height:45px;margin-bottom:20px;background:#f3f3f3;border:none;color:#666;padding-left:15px;font-size:14px;transition:all .3s ease 0s}.leave-comment form textarea{width:100%;max-width:100%;height:176px;min-height:176px;margin-bottom:20px;background:#f3f3f3;border:none;padding-left:15px;color:#666;font-size:14px;transition:all .3s ease 0s;padding-top:7px}.leave-comment form textarea::-webkit-input-placeholder,.leave-comment form input[type=text]::-webkit-input-placeholder{color:#666;opacity:1}.leave-comment form textarea::-moz-placeholder,.leave-comment form input[type=text]::-moz-placeholder{color:#666;opacity:1}.leave-comment form textarea:-ms-input-placeholder,.leave-comment form input[type=text]:-ms-input-placeholder{color:#666;opacity:1}.leave-comment form textarea:-moz-placeholder,.leave-comment form input[type=text]:-moz-placeholder{color:#666;opacity:1}.leave-comment form textarea:focus,.leave-comment form input[type=text]:focus{background:#fff;box-shadow:0 0 5px rgba(241,80,89,.3)}.leave-comment form input#comment-submit{background:#5b1ffa;border:1px solid transparent;text-transform:uppercase;font-size:12px;letter-spacing:.01em;color:#fff;padding:14px 15px;line-height:14px;border-radius:2px;outline:none;transition:all .3s ease 0s;box-shadow:0 0 5px rgba(0,0,0,.1)}.leave-comment form input#comment-submit:hover{background:#f73540;color:#fff}.crumbs-area{width:100%;background:#ece4e4;display:flex;align-items:center;text-align:center;position:relative}.crumbs-inner{width:100%;margin-top:72px;padding:120px 0}.crumbs-inner h2{text-transform:uppercase;color:#5b1ffa;letter-spacing:.05em;margin-bottom:12px;font-size:36px}.crumbs-inner ul{}.crumbs-inner ul li{display:inline-block}.crumbs-inner ul li a,.crumbs-inner ul li span{text-transform:uppercase;font-size:12px;letter-spacing:0;color:#5b1ffa}.crumbs-inner ul li a{margin-right:22px;position:relative}.crumbs-inner ul li a:before{content:'/';position:absolute;right:-16px;top:0}.crumbs-inner ul li span{color:#444;font-weight:600}.blog-post-area{padding-top:120px}.blog-post-area .list-item{margin-bottom:30px}.pagination-area{padding-bottom:100px;padding-top:20px}.pagination{text-align:center;width:100%}.pagination ul{display:inline-block;background:#5b1ffa;padding:4px 7px;border-radius:1px;box-shadow:0 0 5px rgba(0,0,0,.3)}.pagination ul li{display:inline-block}.pagination ul li a,.pagination ul li span{display:block;padding:10px;color:#fff;letter-spacing:.01em;font-weight:500;border-bottom:1px solid transparent}.pagination ul li a:hover,.pagination ul li span{color:#fff;border-bottom:1px solid #fff}.flex-left-sidebar>div>div{display:flex;flex-wrap:wrap-reverse}.sidebar--area{padding-left:40px}.widget{margin-bottom:50px}.widget:last-child{margin-bottom:0}.widget-search{}.widget-search form{position:relative}.widget-search form input{width:100%;height:45px;padding:0 45px 0 15px;background:0 0;border:1px solid #ccc;font-size:15px;color:#444;letter-spacing:.02em;border-radius:2px;transition:all .3s ease 0s}.widget-search form input:focus{border-color:#e8313b}.widget-search form input:focus~button{color:#fff;background-color:#e8313b}.widget-search form button{position:absolute;right:0;top:0;height:45px;width:45px;background:#5b1ffa;border:1px solid transparent;color:#fff;border-radius:2px;font-size:16px;text-align:center;line-height:42px;transition:all .3s ease 0s}.widget-recent-post{}.widget-title{margin-bottom:30px;background:#5b1ffa;padding:8px 10px;border-radius:2px}.widget-title h2{text-transform:uppercase;font-size:14px;font-weight:400;color:#fff;letter-spacing:0;display:inline-block;line-height:21px}.recent--post-list{}.rc-single-post{overflow:hidden;margin-bottom:20px}.rc-single-post:last-child{margin-bottom:0}.meta-thumb{float:left;margin-right:20px}.meta-thumb a{display:inline-block}.meta-thumb a img{max-width:100%;max-width:66px}.meta--content{overflow:hidden}.meta--content a{display:block;font-size:16px;letter-spacing:.01em;color:#535151;line-height:22px;font-family:lato,sans-serif;margin-top:-3px}.meta--content a:hover{color:#5b1ffa}.meta--content span.up-time{display:block;font-size:13px;letter-spacing:0;color:#888}.widget--category-list ul,.widget--archive-list ul{list-style-type:none}.widget--category-list ul li:last-child,.widget--archive-list ul li:last-child{margin-bottom:-10px}.widget--category-list ul li a,.widget--archive-list ul li a,.sidebar--area .widget-nav-menu ul li a{font-size:14px;letter-spacing:.01em;line-height:20px;color:#555;display:block;margin-bottom:22px}.widget--category-list ul li a:hover,.widget--archive-list ul li a:hover,.widget--category-list ul li a:hover::before,.widget--archive-list ul li a:hover::before,.sidebar--area .widget-nav-menu ul li a:hover::before,.sidebar--area .widget-nav-menu ul li a:hover{color:#5b1ffa}.widget--category-list ul li a span,.widget--archive-list ul li a span{color:#c2b8b8}.widget-tags{}.widget-tag-list{}.widget-tag-list a{display:inline-block;font-size:10px;text-transform:uppercase;background:#5b1ffa;color:#fff;padding:7px 13px;margin-bottom:5px;letter-spacing:0;line-height:15px;border-radius:1px;box-shadow:0 0 5px rgba(0,0,0,.1);margin-right:8px}.widget-tag-list a:hover{background-color:#e8313b}.container1{display:inline-flex;flex-wrap:wrap;border:1px solid #000}.flex-direction{flex-direction:row}.div1{border-right:1px solid #000;background-color:#727272;width:165px;height:132px}.div2{background-color:#fff;width:314px;height:132px} \ No newline at end of file diff --git a/mixly/files/themes.css b/mixly/files/themes.css new file mode 100644 index 00000000..d1ea0cef --- /dev/null +++ b/mixly/files/themes.css @@ -0,0 +1,269 @@ +:root { + --app-light-color: #009688; + --app-dark-color: #007acc; +} + +div.layui-form.layui-border-box.layui-table-view { + margin:0px !important; +} + +div.layui-form.layui-border-box.layui-table-view { + margin: 0px !important; + position: relative; + height: 100%; + border-width: 0px; +} + +div.layui-form.layui-border-box.layui-table-view > div.layui-table-tool { + position: absolute; + top: 0; + width: 100%; + padding: 10px 15px; + height: 50px; + left: 0px; +} + +div.layui-form.layui-border-box.layui-table-view > div.layui-table-box { + position: absolute; + overflow-y: hidden; + overflow-x: hidden; + bottom: 0px; + width: 100%; + left: 0px; + top: 50px; +} + +/*div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div.layui-table-header { + position: absolute; + left: 0px; + top: 0px; + height: 38px; + width: 100%; + border-bottom-width: 0px; + border-right-width: 17px; +}*/ + +div.layui-form.layui-border-box.layui-table-view > div.layui-table-tool > div.layui-table-tool-temp { + padding-right: 0px !important; +} + +div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div.layui-table-header > table { + width: 100%; +} + +div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div.layui-table-body.layui-table-main { + overflow-x: auto; + overflow-y: scroll; + position: absolute; + top: 38px; + left: 0px; + bottom: 0px; + width: 100%; +} + +/*div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div > table > thead > tr > th > div > div { + top:6px !important; +} + +div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div > table > tbody > tr > td > div > div { + top:6px !important; +}*/ + +div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div.layui-table-body.layui-table-main > table { + width: 100%; +} + +.layui-table-body { + margin-right: 0px; + margin-bottom: 0px; +} + +.layui-layer-resize { + width: 5px !important; + height: 10px !important; + border-radius: 0px 0px 5px 0px; +} + +html[data-bs-theme=light] .layui-layer-resize { + background-color: #fff; +} + +.layui-table-body { + margin: 0px; +} + +.layui-table-header { + border-right-width: 10px !important; +} + +html[data-bs-theme=dark] .layui-carousel[lay-indicator=outside] .layui-carousel-ind ul { + background-color: rgb(141 135 135 / 50%); +} + +html[data-bs-theme=light] .layui-carousel[lay-indicator=outside] .layui-carousel-ind ul { + background-color: rgb(130 129 129 / 50%); +} + +html[data-bs-theme=light] #footer > div > p { + color: #888; +} + +html[data-bs-theme=light] .layui-carousel-arrow { + background-color: rgba(0,0,0,.2); +} + +html[data-bs-theme=light] .layui-carousel-arrow:hover, html[data-bs-theme=light] .layui-carousel-ind ul:hover { + background-color: rgba(0,0,0,.35); +} + +html[data-bs-theme=light] .service-single { + background: #fff; + border-width: 1px; + border-style: solid; + box-shadow: 1px 1px 4px rgb(0 0 0 / 15%); + border-color: #eee; +} + +html[data-bs-theme=light] .service-single h2 { + color: #232323; +} + +html[data-bs-theme=dark] #footer > div > p { + color: #fff; +} + +html[data-bs-theme=dark] .service-single { + background: #2d2c2c; +} + +html[data-bs-theme=dark] .service-single h2 { + color: #fff; +} + +body[class="light"] { + background-color: #fff !important; +} + +body[class="dark"] { + background-color: #181818 !important; + color: #fff; +} + +.mixly-board:hover .mixly-board-del-btn, .mixly-board:hover .mixly-board-ignore-btn { + display: block; +} + +/*.mixly-board:hover .service-single label { + opacity: 1; +} + +.mixly-board .service-single label { + opacity: 0; +} + +.mixly-board:hover .service-single h2 { + margin-bottom: 10px; +}*/ + +html[data-bs-theme=dark] .mixly-board .service-single label { + color: #ccc; +} + +html[data-bs-theme=light] .mixly-board .service-single label { + color: #888; +} + +.mixly-board-del-btn, .mixly-board-ignore-btn { + display: none; + background-color: rgba(0,0,0,0); + border: 0px; + margin-top: 0px; + margin-right: 0px; + position: absolute; + right: 15px; + top: 5px; + opacity: 0.5; + color: #666; +} + +html[data-bs-theme=dark] .mixly-board-del-btn, html[data-bs-theme=dark] .mixly-board-ignore-btn { + color: #ccc; +} + +.mixly-board-del-btn:hover, .mixly-board-ignore-btn:hover { + opacity: 1; +} + +html[data-bs-theme=dark] .layui-progress-text { + color: #fff; +} + +.layui-table-fixed-r > .layui-table-header { + border-bottom-width: 0px; +} + +.layui-table-fixed-r > .layui-table-body > table { + width: 100%; +} + +html[data-bs-theme=dark] .layui-table-body .layui-none { + color: #fff; +} + +html[data-bs-theme=light] .layui-layer-setwin span { + color: #777; +} + +html[data-bs-theme=dark] .layui-layer-setwin span { + color: #fff; +} + +html[data-bs-theme=dark] .layui-carousel-arrow { + background-color: #2d2c2c !important; +} + +html[data-bs-theme=dark] .layui-carousel-arrow:hover, +html[data-bs-theme=dark] .layui-carousel-ind ul:hover { + background-color: rgba(119,119,119,.35) !important; +} + +.loading { + position: absolute; + z-index: 100000; + left: 0px; + top: 0px; + width: 100%; + height: 100%; +} + +html[data-bs-theme=light] .loading { + background-color: #f9f2f2; +} + +html[data-bs-theme=dark] .loading { + background-color: #181818; +} + +html[data-bs-theme=light] .select2-container--open .select2-selection--single { + outline: 1px solid var(--app-light-color); +} + +html[data-bs-theme=dark] .select2-container--open .select2-selection--single { + outline: 1px solid var(--app-dark-color); +} + +html[data-bs-theme=light] > body > .select2-container > .select2-dropdown { + outline: 1px solid var(--app-light-color); +} + +html[data-bs-theme=dark] > body > .select2-container > .select2-dropdown { + outline: 1px solid var(--app-dark-color); +} + +.service-single > a, +.service-single > a > img { + -webkit-user-drag: none; + -moz-user-drag: none; + -ms-user-drag: none; + user-drag: none; +} \ No newline at end of file diff --git a/mixly/files/typography.css b/mixly/files/typography.css new file mode 100644 index 00000000..6f0445b2 --- /dev/null +++ b/mixly/files/typography.css @@ -0,0 +1 @@ +body{font-family:poppins,sans-serif}h1,h2,h3,h4,h5,h6{margin:0;font-family:poppins,sans-serif;font-weight:700;color:#000;letter-spacing:1.5px}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit;font-family:poppins,sans-serif}h1{font-size:36px;line-height:70px;letter-spacing:1px;margin-bottom:0}h2{font-size:22px;line-height:24px;margin-bottom:0}h3{font-size:24px;line-height:30px;margin-bottom:0}h4{font-size:20px;line-height:27px}h5{font-size:16px;line-height:24px}h6{font-size:14px;line-height:24px}p{font-family:poppins,sans-serif;font-size:14px;font-weight:400;letter-spacing:0;line-height:28px;margin:0}p img{margin:0}span{font-family:poppins,sans-serif}em{font:15px/30px}strong,b{font:15px/30px}small{font-size:11px;line-height:inherit}blockquote{border-left:0;font-family:poppins,sans-serif;margin:10px 0;padding-left:40px;position:relative}blockquote::before{color:#4e5665;content:"";font-family:FontAwesome;font-size:36px;left:20px;line-height:0;margin:0;opacity:.5;position:absolute;top:20px}blockquote p{font-style:italic;padding:0;font-size:18px;line-height:36px}blockquote cite{display:block;font-size:12px;font-style:normal;line-height:18px}blockquote cite:before{content:"\2014 \0020"}blockquote cite a,blockquote cite a:visited{color:#8b9798;border:none}abbr{color:#444;font-weight:700;font-variant:small-caps;text-transform:lowercase;letter-spacing:.6px}abbr:hover{cursor:help}a,a:visited{text-decoration:none;font-weight:400;font-size:12px;color:#ccc;transition:all .3s ease-in-out;-webkit-transition:all .3s ease-in-out;-moz-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;outline:0}a:hover,a:active{color:#555;text-decoration:none;outline:0}a:focus{text-decoration:none;outline:0}p a,p a:visited{line-height:inherit;outline:0}a.active-page{color:#e6ae48!important}ul,ol{margin-bottom:0;margin-top:0}ul{margin:0;padding:0;list-style-type:none}ol{list-style:decimal}ol,ul.square,ul.circle,ul.disc{margin-left:0}ul.square{list-style:square outside}ul.circle{list-style:circle outside}ul.disc{list-style:disc outside}ul ul,ul ol,ol ol,ol ul{margin:0}ul ul li,ul ol li,ol ol li,ol ul li{margin-bottom:0}li{line-height:18px;margin-bottom:0}ul.large li{}li p{}dl{margin:12px 0}dt{margin:0;color:#11abb0}dd{margin:0 0 0 20px}button{}div#preloader{height:100%;left:0;overflow:visible;position:fixed;top:0;width:100%;z-index:999;display:flex;background:#fff}.spinner{width:40px;height:40px;margin:auto;background-color:#333;border-radius:100%;-webkit-animation:sk-scaleout 1s infinite ease-in-out;animation:sk-scaleout 1s infinite ease-in-out}@-webkit-keyframes sk-scaleout{0%{-webkit-transform:scale(0)}100%{-webkit-transform:scale(1);opacity:0}}@keyframes sk-scaleout{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:0}}.m--0{margin-top:0;margin-right:0;margin-bottom:0;margin-left:0}.p--0{padding-top:0;padding-right:0;padding-bottom:0;padding-left:0}.mt--0{margin-top:0}.mt--10{margin-top:10px}.mt--15{margin-top:15px}.mt--20{margin-top:20px}.mt--30{margin-top:30px}.mt--40{margin-top:40px}.mt--50{margin-top:50px}.mt--60{margin-top:60px}.mt--70{margin-top:70px}.mt--80{margin-top:80px}.mt--90{margin-top:90px}.mt--100{margin-top:100px}.mt--110{margin-top:110px}.mt--120{margin-top:120px}.mt--130{margin-top:130px}.mt--140{margin-top:140px}.mt--150{margin-top:150px}.mb--0{margin-bottom:0}.mb--10{margin-bottom:10px}.mb--15{margin-bottom:15px}.mb--20{margin-bottom:20px}.mb--30{margin-bottom:30px}.mb--40{margin-bottom:40px}.mb--50{margin-bottom:50px}.mb--60{margin-bottom:60px}.mb--70{margin-bottom:70px}.mb--80{margin-bottom:80px}.mb--90{margin-bottom:90px}.mb--100{margin-bottom:100px}.mb--110{margin-bottom:110px}.mb--120{margin-bottom:120px}.mb--130{margin-bottom:130px}.mb--140{margin-bottom:140px}.mb--150{margin-bottom:150px}.pt--0{padding-top:0}.pt--10{padding-top:10px}.pt--15{padding-top:15px}.pt--20{padding-top:20px}.pt--30{padding-top:30px}.pt--40{padding-top:40px}.pt--50{padding-top:50px}.pt--60{padding-top:60px}.pt--70{padding-top:70px}.pt--80{padding-top:80px}.pt--90{padding-top:90px}.pt--100{padding-top:100px}.pt--110{padding-top:110px}.pt--120{padding-top:120px}.pt--130{padding-top:130px}- .pt--140{padding-top:140px}.pt--150{padding-top:150px}.pb--0{padding-bottom:0}.pb--10{padding-bottom:10px}.pb--15{padding-bottom:15px}.pb--20{padding-bottom:20px}.pb--30{padding-bottom:30px}.pb--40{padding-bottom:40px}.pb--50{padding-bottom:50px}.pb--60{padding-bottom:60px}.pb--70{padding-bottom:70px}.pb--80{padding-bottom:80px}.pb--90{padding-bottom:90px}.pb--100{padding-bottom:100px}.pb--110{padding-bottom:110px}.pb--120{padding-bottom:120px}.pb--130{padding-bottom:130px}.pb--140{padding-bottom:140px}.pb--150{padding-bottom:150px}.pb--160{padding-bottom:160px}.pb--170{padding-bottom:170px}.pb--180{padding-bottom:180px}.pb--190{padding-bottom:190px}.ptb--0{padding:0}.ptb--10{padding:10px 0}.ptb--20{padding:20px 0}.ptb--30{padding:30px 0}.ptb--40{padding:40px 0}.ptb--50{padding:50px 0}.ptb--60{padding:60px 0}.ptb--70{padding:70px 0}.ptb--80{padding:80px 0}.ptb--90{padding:90px 0}.ptb--100{padding:100px 0}.ptb--110{padding:110px 0}.ptb--120{padding:120px 0}.ptb--130{padding:130px 0}.ptb--140{padding:140px 0}.ptb--150{padding:150px 0}.ptb--160{padding:160px 0}.ptb--170{padding:170px 0}.ptb--180{padding:180px 0}.mtb--0{margin:0}.mtb--10{margin:10px 0}.mtb--15{margin:15px 0}.mtb--20{margin:20px 0}.mtb--30{margin:30px 0}.mtb--40{margin:40px 0}.mtb--50{margin:50px 0}.mtb--60{margin:60px 0}.mtb--70{margin:70px 0}.mtb--80{margin:80px 0}.mtb--90{margin:90px 0}.mtb--100{margin:100px 0}.mtb--110{margin:110px 0}.mtb--120{margin:120px 0}.mtb--130{margin:130px 0}.mtb--140{margin:140px 0}.mtb--150{margin:150px 0}.d-flex{display:flex}.flex-center{align-items:center}.h-100{height:100%}.meta-content{overflow:hidden}.bg-gray{background-color:#fcfcfc} \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/common/board-manager.js b/mixly/mixly-sw/mixly-modules/common/board-manager.js new file mode 100644 index 00000000..504f2bae --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/common/board-manager.js @@ -0,0 +1,1292 @@ +(() => { + +goog.require('path'); +goog.require('layui'); +goog.require('Mixly.Env'); +goog.require('Mixly.Msg'); +goog.require('Mixly.XML'); +goog.require('Mixly.LayerExt'); +goog.require('Mixly.Config'); +goog.require('Mixly.MArray'); +goog.require('Mixly.Url'); +goog.require('Mixly.Storage'); +goog.require('Mixly.Electron.CloudDownload'); +goog.require('Mixly.Electron.PythonShell'); +goog.provide('Mixly.BoardManager'); + +const { + Env, + Msg, + XML, + LayerExt, + Config, + MArray, + Url, + Storage, + Electron, + BoardManager +} = Mixly; + +const { + layer, + table, + element, + carousel, + form +} = layui; + +const fs = Mixly.require('fs'); +const fs_extra = Mixly.require('fs-extra'); +const fs_plus = Mixly.require('fs-plus'); +const os = Mixly.require('os'); +const compressing = Mixly.require('compressing'); +const electron_remote = Mixly.require('@electron/remote'); + +const { + SOFTWARE, + BOARDS_INFO, + USER, + BOARD_PAGE +} = Config; + +const { + PythonShell, + CloudDownload +} = Electron; + +/*BoardManager.ERROR = { + "CONFIG_PARSE_ERROR": 0, + "COPY_ERROR": 1, + "UNZIP_ERROR": 2, + +}*/ + +BoardManager.boardsList = []; + +BoardManager.URL = SOFTWARE?.board?.url ?? null; + +BoardManager.LOCAL_IMPORT_FILTERS = [ + { name: 'Lib File', extensions: ['json', 'zip'] } +]; + +BoardManager.showLocalImportDialog = () => { + const currentWindow = electron_remote.getCurrentWindow(); + currentWindow.focus(); + const { dialog } = electron_remote; + dialog.showOpenDialog(currentWindow, { + title: Msg.getLang('IMPORT_BOARD'), + // 默认打开的路径,比如这里默认打开下载文件夹 + defaultPath: Env.clientPath, + buttonLabel: Msg.getLang('CONFIRM'), + // 限制能够选择的文件类型 + filters: BoardManager.LOCAL_IMPORT_FILTERS, + properties: ['openFile', 'showHiddenFiles'], + message: Msg.getLang('IMPORT_BOARD') + }).then(result => { + const selectedPath = result.filePaths[0]; + + if (!selectedPath) { + return; + } + + console.log('待导入板卡路径:', selectedPath); + let layerNum; + BoardManager.importFromLocal(selectedPath, () => { + layerNum = layer.open({ + type: 1, + id: "import-local-board-layer", + title: Msg.getLang("IMPORTING_BOARD") + "...", + content: $('#mixly-loader-div'), + shade: LayerExt.SHADE_ALL, + resize: false, + closeBtn: 0, + success: function (layero) { + $("#mixly-loader-btn").css('display', 'none'); + }, + end: function () { + $("#mixly-loader-btn").css('display', 'inline-block'); + $('#layui-layer-shade' + layerNum).remove(); + } + }); + }, (error) => { + console.log(error); + layer.msg(error, { time: 1000 }); + }, (error) => { + layer.close(layerNum); + if (error) { + console.log(error); + layer.msg(Msg.getLang('IMPORT_FAILED'), { time: 1000 }); + } else { + layer.msg(Msg.getLang('IMPORT_SUCC'), { time: 1000 }); + BoardManager.onclickImportBoards(); + BoardManager.screenWidthLevel = -1; + BoardManager.screenHeightLevel = -1; + BoardManager.loadBoards(); + BoardManager.updateBoardsCard(); + } + }); + }).catch(err => { + console.log(err); + layer.msg(Msg.getLang('IMPORT_FAILED'), { time: 1000 }); + }); +} + +BoardManager.importBoardPackageWithConfig = (configPath) => { + return new Promise((resolve, reject) => { + const dirPath = path.join(configPath, '../'); + const hardwarePath = path.join(dirPath, './hardware'); + if (fs_plus.isDirectorySync(dirPath) && fs_plus.isDirectorySync(hardwarePath)) { + const packageList = fs.readdirSync(hardwarePath); + const packagePath = []; + let copyPromiseList = []; + fs_extra.ensureDirSync(path.join(Env.arduinoCliPath, 'Arduino15/')); + for (let i of packageList) { + const startPath = path.join(hardwarePath, i); + if (fs_plus.isDirectorySync(startPath)) { + const endPath = path.join(Env.arduinoCliPath, 'Arduino15/packages/', i); + copyPromiseList.push(BoardManager.copyDir(startPath, endPath)); + } else { + const endPath = path.join(Env.arduinoCliPath, 'Arduino15/', i); + if (path.extname(i) === '.json') { + fs_extra.copy(startPath, endPath); + } + packagePath.push(endPath); + } + } + if (copyPromiseList.length) { + Promise.all(copyPromiseList) + .then((message) => { + for (let j of message) { + if (!j.error) { + packagePath.push(j.endPath); + } + } + resolve(packagePath); + }) + .catch((error) => { + resolve(packagePath); + }); + } else { + resolve(packagePath); + } + } else { + resolve(null); + } + }); +} + +BoardManager.copyDir = (startPath, endPath) => { + return new Promise((resove, reject) => { + fs_extra.ensureDir(endPath) + .then(() => { + return fs_extra.emptyDir(endPath); + }) + .then(() => { + return fs_extra.copy(startPath, endPath); + }) + .then(() => { + resove({ error: null, endPath }); + }) + .catch((error) => { + resolve({ error, endPath }); + }) + }); +} + +BoardManager.importPyPackage = () => { + +} + +BoardManager.delBoard = (boardDir, endFunc) => { + const layerIndex = BoardManager.showDelBoardProgress(); + const configPath = path.join(boardDir, './config.json'); + const boardConfig = BoardManager.readBoardConfig(configPath); + if (boardConfig) { + let removePromise = []; + const { packagePath } = boardConfig; + if (typeof packagePath === 'object') { + packagePath.push(boardDir); + for (let i of packagePath) + removePromise.push(new Promise((resove, reject) => { + fs_extra.remove(i, (error) => { + resove(error); + }); + })); + Promise.all(removePromise) + .then((message) => { + layer.close(layerIndex); + endFunc(null); + }) + .catch((error) => { + layer.close(layerIndex); + endFunc(error); + }); + return; + } + } + fs_extra.remove(boardDir, (error) => { + layer.close(layerIndex); + endFunc(error); + }); +} + +BoardManager.ignoreBoard = (boardType, endFunc) => { + USER.boardIgnore = USER.boardIgnore ?? []; + USER.boardIgnore.push(boardType); + Storage.user('/', USER); + endFunc(); +} + +BoardManager.resetBoard = (endFunc) => { + USER.boardIgnore = []; + Storage.user('/', USER); + endFunc(); +} + +BoardManager.showDelBoardProgress = () => { + return layerNum = layer.open({ + type: 1, + id: "del-local-board-layer", + title: Msg.getLang("DELETING_BOARD") + "...", + content: $('#mixly-loader-div'), + shade: LayerExt.SHADE_ALL, + resize: false, + closeBtn: 0, + success: function (layero) { + $("#mixly-loader-btn").css('display', 'none'); + }, + end: function () { + $("#mixly-loader-btn").css('display', 'inline-block'); + $('#layui-layer-shade' + layerNum).remove(); + } + }); +} + +BoardManager.importFromLocal = (inPath, sucFunc, errorFunc, endFunc) => { + const extname = path.extname(inPath); + if (fs_plus.isFileSync(inPath)) { + switch (extname) { + case '.json': + if (path.join(inPath, '../../') === Env.thirdPartyBoardPath) { + errorFunc(Msg.getLang('BOARD_IMPORTED')); + return; + } + sucFunc(); + BoardManager.importFromLocalWithConfig(inPath, endFunc); + break; + case '.zip': + sucFunc(); + BoardManager.importFromLocalWithZip(inPath, endFunc); + break; + default: + errorFunc(Msg.getLang('SELECT_CONFIG_FILE_ERR')); + } + } else { + errorFunc(Msg.getLang('FILE_NOT_EXIST')); + } +} + +BoardManager.importFromLocalWithConfig = (inPath, endFunc = (errorMessages) => {}) => { + const boardConfig = BoardManager.readBoardConfig(inPath); + const hardwarePath = path.join(inPath, '../hardware'); + const filterFunc = (src, dest) => { + if (src.indexOf(hardwarePath) !== -1) + return false; + return true; + } + if (boardConfig) { + const dirPath = path.dirname(inPath); + const dirName = path.basename(dirPath); + const finalDir = path.join(Env.thirdPartyBoardPath, './' + dirName); + if (dirPath !== finalDir) { + fs_extra.ensureDir(Env.thirdPartyBoardPath) + .then(() => { + return fs_extra.emptyDir(finalDir); + }) + .then(() => { + return fs_extra.copy(dirPath, finalDir, { filter: filterFunc }); + }) + .then(() => { + return BoardManager.importBoardPackageWithConfig(inPath); + }) + .then((message) => { + if (message) { + boardConfig.packagePath = message; + fs_extra.writeJsonSync(path.join(finalDir, './config.json'), boardConfig, { + spaces: ' ' + }); + } + return fs_extra.remove(path.join(finalDir, './hardware')); + }) + .then(() => { + endFunc(null); + }) + .catch((error) => { + console.log(error); + endFunc(error); + }); + } else { + if (fs_plus.isDirectorySync(hardwarePath)) { + BoardManager.importBoardPackageWithConfig(inPath) + .then((message) => { + if (message) { + boardConfig.packagePath = message; + fs_extra.writeJsonSync(path.join(finalDir, './config.json'), boardConfig, { + spaces: ' ' + }); + } + return fs_extra.remove(path.join(finalDir, './hardware')); + }) + .then(() => { + endFunc(null); + }) + .catch((error) => { + console.log(error); + endFunc(error); + }) + } else { + endFunc(null); + } + } + } else { + console.log(Msg.getLang('CONFIG_FILE_DECODE_ERR')); + endFunc(Msg.getLang('CONFIG_FILE_DECODE_ERR')); + } +} + +BoardManager.importFromLocalWithZip = (inPath, endFunc = (errorMessages) => {}) => { + BoardManager.unZip(inPath, Env.thirdPartyBoardPath, false, (error) => { + if (error) { + endFunc(error); + } else { + const fileName = path.basename(inPath, '.zip'); + const configPath = path.join(Env.thirdPartyBoardPath, fileName, 'config.json'); + BoardManager.importFromLocalWithConfig(configPath, endFunc); + } + }); +} + +BoardManager.unZip = (inPath, desPath, delZip, endFunc = (errorMessage) => {}) => { + const fileName = path.basename(inPath, '.zip'); + const unZipPath = path.join(desPath, './' + fileName); + compressing.zip.uncompress(inPath, desPath, { + zipFileNameEncoding: 'GBK' + }) + .then(() => { + if (delZip) + try { + fs.unlinkSync(inPath); + } catch (error) { + console.log(error); + } + endFunc(null); + }) + .catch((error) => { + endFunc(error); + }); +} + +BoardManager.showCloudImportProgress = (boardList, endFunc = (errorMessages) => {}) => { + const parentDom = $('
'); + parentDom.css({ + 'overflow': 'hidden', + 'padding': '5px', + 'width': '100%', + 'height': '100%', + 'display': 'none' + }); + const childDom = $('
'); + childDom.css({ + 'overflow-x': 'hidden', + 'overflow-y': 'auto', + 'width': '100%', + 'height': '100%' + }); + for (let i in boardList) { + const boardInfo = boardList[i]; + const boardPanelConfig = { + boardPanelId: 'board-' + i + '-panel-id', + boardType: boardInfo.name, + progressFilter: 'board-' + i + '-progress-filter', + progressStatusId: 'board-' + i + '-progress-status-id' + }; + childDom.append( + XML.render(XML.TEMPLATE_STR.PROGRESS_BAR_DIV, boardPanelConfig) + ); + } + parentDom.append(childDom); + $('body').append(parentDom); + element.render('progress'); + LayerExt.open({ + title: Msg.getLang('IMPORTING_BOARD') + '...', + id: 'setting-menu-layer1', + content: parentDom, + shade: LayerExt.SHADE_ALL, + area: ['40%', '60%'], + max: ['800px', (boardList.length * 106 + 42) + 'px'], + min: ['500px', '100px'], + success: (layero, index) => { + layero.find('.layui-layer-setwin').css('display', 'none'); + BoardManager.importBoardsFromCloud(boardList, layero, index); + }, + end: () => { + parentDom.remove(); + } + }); +} + +BoardManager.importBoardsFromCloud = (boardList, layero, layerIndex) => { + let boardPromise = []; + for (let i in boardList) { + boardList[i].index = i; + boardPromise.push(BoardManager.importBoardWithUrl(boardList[i])); + } + + Promise.all(boardPromise) + .then((message) => { + let sucTimes = 0; + let failedTimes = 0; + for (let i of message) { + const progressStatusDom = $('#board-' + i.index + '-progress-status-id'); + const panelDom = $('#board-' + i.index + '-panel-id'); + const cardHeadDom = panelDom.children('.layui-card-header').first(); + progressStatusDom.removeClass('layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop'); + if (i.error) { + progressStatusDom.addClass('layui-icon-close-fill'); + cardHeadDom.html(i.name + ' - ' + Msg.getLang('IMPORT_FAILED')); + failedTimes++; + console.log(i.error); + } else { + progressStatusDom.addClass('layui-icon-ok-circle'); + cardHeadDom.html(i.name + ' - ' + Msg.getLang('IMPORT_COMPLETE')); + sucTimes++; + BoardManager.writePackageConfig(i); + } + } + const sucIcon = ``; + const errIcon = ``; + layer.title(Msg.getLang('IMPORT_COMPLETE') + ' ' + sucTimes + sucIcon + ' ' + failedTimes + errIcon, layerIndex); + }) + .catch((error) => { + layer.title(Msg.getLang('IMPORT_FAILED'), layerIndex); + }) + .finally(() => { + layero.find('.layui-layer-setwin').css('display', 'block'); + BoardManager.screenWidthLevel = -1; + BoardManager.screenHeightLevel = -1; + BoardManager.loadBoards(); + BoardManager.updateBoardsCard(); + BoardManager.onclickImportBoards(); + }); +} + +/** + * @function 在云端板卡下载完成后,将最新板卡版本信息写入板卡config + * @param boardInfo { object } + * @return void + **/ +BoardManager.writePackageConfig = (boardInfo) => { + const { + url, + package, + version, + dependencies, + boardDirName + } = boardInfo; + if (!boardDirName) return; + const boardDir = path.join(Env.thirdPartyBoardPath, './' + boardDirName); + if (fs_plus.isDirectorySync(boardDir)) { + const { currentPlatform } = Env, + arch = os.arch(); + let packageUrl, packageIndexUrl, packageName, packageIndexName; + if (package) { + packageUrl = package[currentPlatform + '-' + arch] ?? ''; + packageIndexUrl = package.index ?? ''; + packageName = path.basename(packageUrl, '.zip'); + packageIndexName = path.basename(packageIndexUrl); + } + const configPath = path.join(boardDir, './config.json'); + const boardConfig = BoardManager.readBoardConfig(configPath) ?? {}; + boardConfig.version = version; + boardConfig.package = package; + boardConfig.packagePath = []; + if (packageName) { + const packagePath = path.join(Env.arduinoCliPath, './Arduino15/packages/' + packageName); + boardConfig.packagePath.push(packagePath); + } + if (packageIndexName) { + const packageIndexPath = path.join(Env.arduinoCliPath, './Arduino15/' + packageIndexName); + boardConfig.packagePath.push(packageIndexPath); + } + try { + fs_extra.outputJsonSync(configPath, boardConfig, { + spaces: ' ' + }); + } catch (error) { + console.log(error); + } + } +} + +/*{ + "index": Number, + "error": Object, + "name": "Arduino STM32", + "version": "1.2.1", + "desc": "Arduino STM32", + "url": "http://106.52.57.223:8081/cloud-boards/arduino_stm32.zip", + "dependencies": [ + "Arduino AVR" + ], + "package": { + "index": "", + "version": "1.0.0", + "win32-x64": "http://106.52.57.223:8081/cloud-boards/arduino_stm32.zip", + "win32-ia32": "None", + "darwin-arm64": "None", + "darwin-x64": "None", + "linux-x64": "None", + "linux-arm": "None" + } +}*/ +BoardManager.importBoardWithUrl = (boardInfo) => { + return new Promise((resolve, reject) => { + if (!boardInfo.url) { + boardInfo.error = Msg.getLang('BOARD_URL_READ_ERR'); + resolve(boardInfo); + } + // 下载板卡文件 + BoardManager.downloadPromise(boardInfo, { + desPath: path.join(Env.clientPath, './download'), + url: boardInfo.downloadBoardIndex ? boardInfo.url : 'None', + startMessage: Msg.getLang('BOARD_FILE_DOWNLOADING') + '...', + endMessage: Msg.getLang('BOARD_FILE_DOWNLOAD_COMPLETE'), + errorMessage: Msg.getLang('BOARD_FILE_DOWNLOAD_FAILED') + }) + .then((newBoardInfo) => { + if (newBoardInfo.error) + throw newBoardInfo; + else + // 解压板卡文件 + return BoardManager.unZipPromise(newBoardInfo, { + desPath: Env.thirdPartyBoardPath, + zipPath: newBoardInfo.downloadPath, + startMessage: Msg.getLang('BOARD_FILE_UNZIPPING') + '...', + endMessage: Msg.getLang('BOARD_FILE_UNZIP_COMPLETE'), + errorMessage: Msg.getLang('BOARD_FILE_UNZIP_FAILED') + }); + }) + .then((newBoardInfo) => { + if (newBoardInfo.error) + throw newBoardInfo; + else { + const packageIndexUrl = boardInfo?.package?.index ?? null; + // 如果package-index有的话,下载板卡所需要的package-index + const packageIndexDirPath = Env.arduinoCliPath ? path.join(Env.arduinoCliPath, './Arduino15') : null; + return BoardManager.downloadPromise(newBoardInfo, { + desPath: packageIndexDirPath, + url: boardInfo.downloadPackage ? packageIndexUrl : 'None', + startMessage: Msg.getLang('BOARD_PACKAGE_INDEX_DOWNLOADING') + '...', + endMessage: Msg.getLang('BOARD_PACKAGE_INDEX_DOWNLOAD_COMPLETE'), + errorMessage: Msg.getLang('BOARD_PACKAGE_INDEX_DOWNLOAD_FAILED') + }); + } + }) + .then((newBoardInfo) => { + if (newBoardInfo.error) + throw newBoardInfo; + else { + const platform = Env.currentPlatform; + const arch = os.arch(); + const package = boardInfo?.package ?? null; + let packageUrl = null; + if (package && package[platform + '-' + arch]) + packageUrl = package[platform + '-' + arch] ?? null; + // 如果package有的话,下载板卡所需要的package + return BoardManager.downloadPromise(newBoardInfo, { + desPath: path.join(Env.clientPath, './download'), + url: boardInfo.downloadPackage ? packageUrl : 'None', + startMessage: Msg.getLang('BOARD_PACKAGE_DOWNLOADING') + '...', + endMessage: Msg.getLang('BOARD_PACKAGE_DOWNLOAD_COMPLETE'), + errorMessage: Msg.getLang('BOARD_PACKAGE_DOWNLOAD_FAILED') + }); + } + }) + .then((newBoardInfo) => { + if (newBoardInfo.error) + throw newBoardInfo; + else { + const packageDirPath = Env.arduinoCliPath ? path.join(Env.arduinoCliPath, './Arduino15/packages') : null; + return BoardManager.unZipPromise(newBoardInfo, { + desPath: packageDirPath, + zipPath: newBoardInfo.downloadPath, + startMessage: Msg.getLang('BOARD_PACKAGE_UNZIPPING') + '...', + endMessage: Msg.getLang('BOARD_PACKAGE_UNZIP_COMPLETE'), + errorMessage: Msg.getLang('BOARD_PACKAGE_UNZIP_FAILED') + }); + } + }) + .then((newBoardInfo) => { + const panelDom = $('#board-' + newBoardInfo.index + '-panel-id'); + const cardHeadDom = panelDom.children('.layui-card-header').first(); + const progressStatusDom = $('#board-' + newBoardInfo.index + '-progress-status-id'); + if (!newBoardInfo.downloadPackage && !newBoardInfo.downloadBoardIndex) { + progressStatusDom.removeClass('layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop'); + cardHeadDom.html(newBoardInfo.name + ' - ' + Msg.getLang('ALREADY_THE_LATEST_VERSION')); + progressStatusDom.addClass('layui-icon-ok-circle'); + element.progress('board-' + newBoardInfo.index + '-progress-filter', '100%'); + element.render('progress', 'board-' + newBoardInfo.index + '-progress-filter'); + } + resolve(newBoardInfo); + }) + .catch((error) => { + boardInfo.error = error; + resolve(boardInfo); + }); + }); +} + +BoardManager.downloadPromise = (boardInfo, config) => { + return new Promise((resolve, reject) => { + const DEFAULT_CONFIG = { + desPath: path.join(Env.clientPath, './download'), + url: null, + startMessage: Msg.getLang('DOWNLOADING') + '...', + endMessage: Msg.getLang('DOWNLOAD_COMPLETE'), + errorMessage: Msg.getLang('DOWNLOAD_FAILED') + }; + if (typeof config !== 'object') + config = DEFAULT_CONFIG + else + config = { ...DEFAULT_CONFIG, ...config }; + + const { + desPath, + url, + startMessage, + endMessage, + errorMessage + } = config; + + if (!url || url === 'None' || !desPath) { + boardInfo.error = null; + boardInfo.downloadPath = null; + resolve(boardInfo); + return; + } + + try { + fs_extra.ensureDir(desPath); + } catch (error) { + boardInfo.error = error; + boardInfo.downloadPath = null; + resolve(boardInfo); + return; + } + const panelDom = $('#board-' + boardInfo.index + '-panel-id'); + const cardHeadDom = panelDom.children('.layui-card-header').first(); + cardHeadDom.html(boardInfo.name + ' - ' + startMessage); + const progressStatusDom = $('#board-' + boardInfo.index + '-progress-status-id'); + progressStatusDom.removeClass('layui-icon-ok-circle layui-icon-close-fill'); + progressStatusDom.addClass('layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop'); + element.progress('board-' + boardInfo.index + '-progress-filter', '0%'); + CloudDownload.download(url, desPath, { + progress: (stats) => { + const { speed, progress } = stats; + const speedUnit = ['B/s', 'KB/s', 'MB/s', 'GB/s']; + let type = 0; + let nowProgress = parseInt(progress); + nowProgress = nowProgress > 100 ? 100 : nowProgress; + let nowSpeed = speed; + for (let i = 0; i < 3; i++) { + if (nowSpeed / 1000 > 1) { + nowSpeed /= 1024; + type++; + } else { + break; + } + } + + nowSpeed = nowSpeed.toFixed(1); + cardHeadDom.html(boardInfo.name + ' - ' + startMessage + ' - ' + nowSpeed + speedUnit[type]); + element.progress('board-' + boardInfo.index + '-progress-filter', parseInt(progress) + '%'); + }, + error: (message) => { + progressStatusDom.removeClass('layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop'); + progressStatusDom.addClass('layui-icon-close-fill'); + cardHeadDom.html(boardInfo.name + ' - ' + errorMessage); + }, + end: (downloadInfo) => { + progressStatusDom.removeClass('layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop'); + progressStatusDom.addClass('layui-icon-ok-circle'); + cardHeadDom.html(boardInfo.name + ' - ' + endMessage); + }, + timeout: this.error + }) + .then((message) => { + if (message[0]) + throw message[0]; + boardInfo.error = null; + boardInfo.downloadPath = message[1]; + resolve(boardInfo); + }) + .catch((error) => { + boardInfo.error = error; + boardInfo.downloadPath = null; + resolve(boardInfo); + }); + }); +} + +BoardManager.unZipPromise = (boardInfo, config) => { + return new Promise((resolve, reject) => { + const DEFAULT_CONFIG = { + desPath: path.join(Env.clientPath, './download'), + zipPath: null, + startMessage: Msg.getLang('UNZIPPING') + '...', + endMessage: Msg.getLang('UNZIP_COMPLETE'), + errorMessage: Msg.getLang('UNZIP_FAILED'), + delZip: true + }; + if (typeof config !== 'object') + config = DEFAULT_CONFIG + else + config = { ...DEFAULT_CONFIG, ...config }; + + const { + zipPath, + desPath, + delZip, + startMessage, + endMessage, + errorMessage + } = config; + + if (!zipPath || !desPath) { + boardInfo.error = null; + boardInfo.unZipPath = null; + resolve(boardInfo); + return; + } + const panelDom = $('#board-' + boardInfo.index + '-panel-id'); + const cardHeadDom = panelDom.children('.layui-card-header').first(); + cardHeadDom.html(boardInfo.name + ' - ' + startMessage); + const progressStatusDom = $('#board-' + boardInfo.index + '-progress-status-id'); + progressStatusDom.removeClass('layui-icon-ok-circle layui-icon-close-fill'); + progressStatusDom.addClass('layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop'); + element.progress('board-' + boardInfo.index + '-progress-filter', '0%'); + BoardManager.unZip(zipPath, desPath, delZip, (error) => { + if (error) { + progressStatusDom.removeClass('layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop'); + progressStatusDom.addClass('layui-icon-close-fill'); + cardHeadDom.html(boardInfo.name + ' - ' + errorMessage); + boardInfo.error = error; + boardInfo.unZipPath = null; + resolve(boardInfo); + } else { + progressStatusDom.removeClass('layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop'); + progressStatusDom.addClass('layui-icon-ok-circle'); + cardHeadDom.html(boardInfo.name + ' - ' + endMessage); + boardInfo.error = null; + boardInfo.unZipPath = desPath; + element.progress('board-' + boardInfo.index + '-progress-filter', '100%'); + resolve(boardInfo); + } + }) + }); +} + +BoardManager.readBoardConfig = (inPath) => { + const boardConfig = fs_extra.readJsonSync(inPath, { throws: false }); + if (boardConfig) { + const { boardType, boardImg, boardName } = boardConfig; + + if ((boardType || boardName) && boardImg) { + const boardIndexPath = path.join(inPath, '../index.xml'); + boardConfig.boardIndex = path.relative(Env.indexDirPath, boardIndexPath); + if (!boardType && boardName) { + boardConfig.boardType = boardName; + delete boardConfig.boardName; + } + return boardConfig; + } + } + return null; +} + +BoardManager.getThirdPartyBoardsConfig = () => { + const thirdPartyBoardsConfig = []; + if (fs_plus.isDirectorySync(Env.thirdPartyBoardPath)) { + const nameList = fs.readdirSync(Env.thirdPartyBoardPath); + for (let i of nameList) { + const namePath = path.join(Env.thirdPartyBoardPath, './' + i); + if (!fs_plus.isDirectorySync(namePath)) { + continue; + } + const configPath = path.join(namePath, './config.json'); + const boardConfig = BoardManager.readBoardConfig(configPath); + if (!boardConfig) { + continue; + } + const { boardImg = '' } = boardConfig; + const srcIndexDir = path.join(Env.indexDirPath, boardImg); + const srcThirdDir = path.join(namePath, boardImg); + const reSrcIndexDir = path.relative(Env.indexDirPath, srcIndexDir); + const reSrcThirdDir = path.relative(Env.indexDirPath, srcThirdDir); + if (fs_plus.isFileSync(srcIndexDir)) { + boardConfig.boardImg = reSrcIndexDir; + } else if (fs_plus.isFileSync(srcThirdDir)) { + boardConfig.boardImg = reSrcThirdDir; + } + thirdPartyBoardsConfig.push(boardConfig); + } + } + return thirdPartyBoardsConfig; +} + +BoardManager.compareBoardConfig = (cloudConfig) => { + const { url, version, package } = cloudConfig; + const boardDirName = path.basename(url ?? '', '.zip'); + cloudConfig.boardDirName = boardDirName; + cloudConfig.downloadBoardIndex = true; + cloudConfig.downloadPackage = true; + const boardDir = path.join(Env.thirdPartyBoardPath, './' + boardDirName); + if (fs_plus.isDirectorySync(boardDir)) { + const localConfigPath = path.join(boardDir, './config.json'); + const localConfig = fs_extra.readJsonSync(localConfigPath, { throws: false }); + if (localConfig !== null) { + if (localConfig.version === version) + cloudConfig.downloadBoardIndex = false; + if (MArray.equals(localConfig?.package, package)) { + cloudConfig.downloadPackage = false; + } + } + if (cloudConfig.downloadPackage || cloudConfig.downloadBoardIndex) + cloudConfig.status = Msg.getLang('TO_BE_UPDATED'); + else + cloudConfig.status = Msg.getLang('INSTALLED'); + } else { + cloudConfig.status = Msg.getLang('TO_BE_INSTALLED'); + } + return cloudConfig; +} + +BoardManager.onclickImportBoards = () => { + let tableConfig = { + id: 'cloud-boards-table', + elem: '#import-board-page', + data: [], + toolbar: ``, + defaultToolbar: [], + title: Msg.getLang('CLOUD_BOARD'), + cols: [[ + { type: 'checkbox', unresize: false, align: "center" }, + { field: 'status', title: Msg.getLang('STATUS'), unresize: false, align: "center", minWidth: 100 }, + { field: 'name', title: Msg.getLang('NAME'), sort: true, unresize: false, align: "center", minWidth: 200 }, + { field: 'version', title: Msg.getLang('VERSION'), unresize: false, align: "center", minWidth: 80 }, + { field: 'desc', title: Msg.getLang('INTRODUCTION'), unresize: false, align: "center", minWidth: 250 } + ]], + limit: 1000, + skin: 'line', + even: false, + size: 'sm' + }; + + table.on('row(import-board-page-filter)', function(obj) { + let $checkbox = obj.tr.first().find('.layui-form-checkbox'); + obj.setRowChecked({ + checked: !$checkbox.hasClass('layui-form-checked') + }); + }); + + table.render({ + ...tableConfig, + text: { + none: Msg.getLang('CLOUD_BOARD_JSON_DOWNLOADING') + '...' + } + }); + const downloadDir = path.join(Env.clientPath, './download'); + CloudDownload.getJson(BoardManager.URL, downloadDir, (message) => { + if (message[0]) { + console.log(message[0]); + table.render(tableConfig); + return; + } + let boardConfig = []; + for (let i of message[1]) { + boardConfig.push(BoardManager.compareBoardConfig({ ...i })); + } + tableConfig.data = boardConfig; + table.render(tableConfig); + }); + + //头工具栏事件 + table.on('toolbar(import-board-page-filter)', function (obj) { + const checkStatus = table.checkStatus(obj.config.id); + switch (obj.event) { + case 'cloud-import': + let selectedBoards = checkStatus.data; + let boardList = []; + for (var i = 0; i < selectedBoards.length; i++) { + boardList.push({ ... selectedBoards[i] }); + } + if (boardList.length > 0) { + BoardManager.showCloudImportProgress(boardList); + } else { + layer.msg(Msg.getLang('SELECT_AT_LEAST_ONE_CLOUD_BOARD'), { time: 2000 }); + } + break; + case 'local-import': + BoardManager.showLocalImportDialog(); + break; + }; + }); +} + +BoardManager.onclickManageBoards = () => { + +} + +BoardManager.enterBoardIndexWithPyShell = (indexPath, pyFilePath) => { + PythonShell.run(indexPath, pyFilePath); +} + +BoardManager.loadBoards = () => { + const newBoardsList = []; + if (typeof USER.boardIgnore !== 'object') + USER.boardIgnore = []; + for (let i = 0; BOARDS_INFO[i]; i++) { + if (!USER.boardIgnore.includes(BOARDS_INFO[i].boardType)) { + const config = BOARDS_INFO[i]; + const { + env = { + electron: true, + web: true, + webCompiler: true, + webSocket: true + } + } = config; + let show = false; + if ((Env.hasSocketServer && env.webSocket) + || (Env.hasCompiler && env.webCompiler) + || (goog.isElectron && env.electron) + || (!goog.isElectron && env.web)) { + show = true; + } else { + show = false; + } + if (!show) { + continue; + } + if (goog.isElectron) { + const indexPath = path.join(__dirname, config.boardIndex); + if (fs_plus.isFileSync(indexPath)) + newBoardsList.push({ ...BOARDS_INFO[i], thirdPartyBoard: false }); + } else { + newBoardsList.push({ ...BOARDS_INFO[i], thirdPartyBoard: false }); + } + } + } + + if (goog.isElectron) { + const tpBoardsConfig = BoardManager.getThirdPartyBoardsConfig(); + for (let i = 0; i < tpBoardsConfig.length; i++) { + const config = tpBoardsConfig[i]; + const indexPath = path.join(__dirname, config.boardIndex); + if (fs_plus.isFileSync(indexPath)) { + const boardInfo = { + boardImg: config.boardImg ?? './files/default.png', + boardType: config.boardType ?? 'Unknown board', + boardIndex: config.boardIndex ?? 'javascript:;', + env: { + electron: true, + web: false, + webCompiler: false, + webSocket: false + }, + thirdPartyBoard: true, + pyFilePath: config.pyFilePath ?? false, + language: config.language + } + newBoardsList.push(boardInfo); + } + } + } + const boardAdd = { + boardImg: "./files/add.png", + boardType: "Add", + boardDescription: "", + boardIndex: "javascript:;", + env: { + electron: true, + web: true, + webCompiler: true, + webSocket: true + } + }; + newBoardsList.push(boardAdd); + BoardManager.boardsList = newBoardsList; +} + +BoardManager.showBoardsCard = (row, col) => { + const boardContainerDom = $('#mixly-board'); + if (boardContainerDom.length === 0) return; + var boardNum = 0; + var rowStr = ''; + boardContainerDom.empty(); + const { boardsList } = BoardManager; + const params = 'error=0'; + for (let i = 0; i < boardsList.length; i++) { + const { + thirdPartyBoard = false, + boardIndex, + boardType, + boardName, + boardImg, + language + } = boardsList[i]; + const indexPath = './boards/index.html'; + if (boardIndex !== 'javascript:;') { + let configObj = { + thirdPartyBoard, + boardIndex, + boardType, + boardImg, + language + }; + if (boardName) { + configObj.boardName = boardName; + } + const configUrl = Url.jsonToUrl(configObj); + rowStr += ` +
+ +
+ + service image +

${boardsList[i]['boardType']}

+ +
+
+
+ `; + } else { + rowStr += ` +
+
+ + service image +

+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ `; + } + boardNum++; + if (boardNum % (col * row) === 0 && rowStr) { + rowStr = '
' + rowStr + '
'; + boardContainerDom.append(rowStr); + rowStr = ''; + } else if ((boardNum % col) == 0) { + rowStr += '
'; + } + } + if (boardNum % (col * row) && rowStr) { + while (boardNum % (col * row)) { + rowStr += ` +
+ +
+ `; + boardNum++; + if (!(boardNum % col)) { + rowStr += '
'; + } + } + rowStr = '
' + rowStr + '
'; + boardContainerDom.append(rowStr); + rowStr = ''; + } + + const endFunc = (error) => { + if (error) { + console.log(error); + } + BoardManager.screenWidthLevel = -1; + BoardManager.screenHeightLevel = -1; + BoardManager.loadBoards(); + BoardManager.updateBoardsCard(); + } + + $(".mixly-board-del-btn").off("click").click((event) => { + const index = $(event.currentTarget).attr('index'); + const config = BoardManager.boardsList[index]; + const dirPath = path.join(Env.indexDirPath, config.boardIndex, '../'); + BoardManager.delBoard(dirPath, endFunc); + }); + + $(".mixly-board-ignore-btn").off("click").click((event) => { + const index = $(event.currentTarget).attr('index'); + const config = BoardManager.boardsList[index]; + BoardManager.ignoreBoard(config.boardType, endFunc); + }); + + const footerDom = $('#footer'); + if (footerDom.length) { + footerDom.empty(); + footerDom.append( + `
+
+

Copyright © Mixly Team@BNU, CHINA

+
` + ); + } +} + +BoardManager.screenWidthLevel = -1; +BoardManager.screenHeightLevel = -1; + +BoardManager.hasScrollbar = () => { + return document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight); +} + +BoardManager.updateBoardsCard = (offset = 0) => { + const homeDivDom = $('#home'), + homeDivHeight = homeDivDom[0].clientHeight; + let boardRowNum = 1, + boardColNum = 1; + const footerDom = $('#footer'); + if (BoardManager.hasScrollbar()) { + boardRowNum = parseInt((document.body.scrollHeight - homeDivHeight) / 250); + if (boardRowNum <= 0) { + boardRowNum = 1; + } + footerDom.css('top', (homeDivHeight + 275 * boardRowNum - 100) + "px"); + } else { + boardRowNum = parseInt(((window.innerHeight || document.documentElement.clientHeight) - homeDivHeight) / 250); + if (boardRowNum <= 0) { + boardRowNum = 1; + } + if (((homeDivHeight + 275 * boardRowNum - 100) + 50) > (window.innerHeight || document.documentElement.clientHeight)) { + footerDom.css('top', (homeDivHeight + 275 * boardRowNum - 100) + "px"); + } else { + footerDom.css('top', "auto"); + } + } + + //console.log("document.body.clientWidth:" + document.body.clientWidth); + if ($('body').width() < 750) { + if (BoardManager.screenWidthLevel != 0 || BoardManager.screenHeightLevel != boardRowNum) { + BoardManager.screenWidthLevel = 0; + BoardManager.screenHeightLevel = boardRowNum; + Mixly.BoardManager.showBoardsCard(boardRowNum, 1); + } + boardColNum = 1; + } else if ($('body').width() < 975) { + if (BoardManager.screenWidthLevel != 1 || BoardManager.screenHeightLevel != boardRowNum) { + BoardManager.screenWidthLevel = 1; + BoardManager.screenHeightLevel = boardRowNum; + Mixly.BoardManager.showBoardsCard(boardRowNum, 2); + } + boardColNum = 2; + } else if ($('body').width() < 1182) { + if (BoardManager.screenWidthLevel != 2 || BoardManager.screenHeightLevel != boardRowNum) { + BoardManager.screenWidthLevel = 2; + BoardManager.screenHeightLevel = boardRowNum; + BoardManager.showBoardsCard(boardRowNum, 3); + } + boardColNum = 3; + } else { + if (BoardManager.screenWidthLevel != 3 || BoardManager.screenHeightLevel != boardRowNum) { + BoardManager.screenWidthLevel = 3; + BoardManager.screenHeightLevel = boardRowNum; + Mixly.BoardManager.showBoardsCard(boardRowNum, 4); + } + boardColNum = 4; + } + + //console.log('板卡 行×列:', boardRowNum + '×' + boardColNum); + + const { boardsList } = BoardManager; + + let pageIndex = 0; + + if (BOARD_PAGE.boardType) + for (let i in boardsList) { + const config = boardsList[i]; + if (config.boardType === BOARD_PAGE.boardType) { + pageIndex = Math.floor((i - 0) / (boardRowNum * boardColNum)); + break; + } + } + + if (isNaN(pageIndex) || pageIndex < 0) + pageIndex = 0; + + carousel.render({ + elem: '#board-switch', + arrow: 'always', + autoplay: false, + width: '100%', + height: (275 * boardRowNum) + 'px', + anim: 'default', + indicator: 'none', + index: pageIndex + }); + form.render(null, 'setting-card-filter'); + $('.setting-card > div:nth-child(2)').find('div').addClass('fontello-icon'); +} + +})(); \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/common/config.js b/mixly/mixly-sw/mixly-modules/common/config.js new file mode 100644 index 00000000..aaa3bdd6 --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/common/config.js @@ -0,0 +1,66 @@ +(() => { + +goog.require('Mixly.Url'); +goog.require('Mixly.LocalStorage'); +goog.provide('Mixly.Config'); + +const { + Url, + LocalStorage, + Config +} = Mixly; + +Config.USER = { + theme: 'light', + language: 'zh-hans', + winSize: 1, + blockRenderer: 'geras', + compileCAndH: 'true', + boardIgnore: [] +}; + +/** + * @function 读取软件、板卡的配置信息 + * @return {void} + **/ +Config.init = () => { + Config.SOFTWARE = goog.readJsonSync('./sw-config.json', {}); + console.log('Config.SOFTWARE:', Config.SOFTWARE); + Config.BOARDS_INFO = goog.readJsonSync('./boards.json', {}); + console.log('Config.BOARDS_INFO:', Config.BOARDS_INFO); + const boardPageConfig = Url.getConfig(); + Config.BOARD_PAGE = boardPageConfig ?? {}; + console.log('Config.BOARD_PAGE:', Config.BOARD_PAGE); + document.title = Config.SOFTWARE.version ?? 'Mixly 3.0'; + + Config.USER = { + ...Config.USER, + ...LocalStorage.get(LocalStorage.PATH['USER']) ?? {} + }; + + if (Config.USER.themeAuto) { + const themeMedia = window.matchMedia("(prefers-color-scheme: light)"); + Config.USER.theme = themeMedia.matches ? 'light' : 'dark'; + } + + if (Config.USER.languageAuto) { + switch (navigator.language) { + case 'zh-CN': + Config.USER.language = 'zh-hans'; + break; + case 'zh-HK': + case 'zh-SG': + case 'zh-TW': + Config.USER.language = 'zh-hant'; + break; + default: + Config.USER.language = 'en'; + } + } + + console.log('Config.USER', Config.USER); +} + +Config.init(); + +})(); \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/common/env.js b/mixly/mixly-sw/mixly-modules/common/env.js new file mode 100644 index 00000000..46ea75cd --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/common/env.js @@ -0,0 +1,78 @@ +(() => { + +goog.require('path'); +goog.require('Mixly'); +goog.require('Mixly.Config'); +goog.provide('Mixly.Env'); + +const fs_extra = Mixly.require('fs-extra'); +const fs_plus = Mixly.require('fs-plus'); +const electron_remote = Mixly.require('@electron/remote'); + +const { Env, Config } = Mixly; + +const { SOFTWARE } = Config; + +/** + * 获取当前mixly的路径 + * @type {String} + */ +Env.clientPath = null; + +/** + * 检测当前系统 + * @type {String} win32、darwin、linux + */ +Env.currentPlatform = goog.platform(); + +/** + * 获取板卡index或主页面index的路径 + * @type {String} + */ +Env.indexDirPath = path.join((new URL($('html')[0].baseURI)).href, '../').replace(/file:\/+/g, ''); +Env.indexDirPath = decodeURIComponent(Env.indexDirPath); +if (Env.currentPlatform !== 'win32') { + Env.indexDirPath = '/' + Env.indexDirPath; +} + +/** + * 检测是否启用node服务器 + * @type {Boolean} + */ +Env.hasSocketServer = SOFTWARE?.webSocket?.enabled ? true : false; + +/** + * 检测是否启用node编译服务器 + * @type {Boolean} + */ +Env.hasCompiler = SOFTWARE?.webCompiler?.enabled ? true : false; + +Env.thirdPartyBoardPath = path.join(Env.indexDirPath, 'boards/extend'); + +if (goog.isElectron) { + const { app } = electron_remote; + const { currentPlatform } = Env; + if (currentPlatform === "darwin") { + Env.clientPath = path.join(app.getPath("exe"), '../../../../'); + } else { + Env.clientPath = path.join(app.getPath("exe"), '../'); + } + if (Env.currentPlatform === "darwin" || Env.currentPlatform === "linux") { + Env.python3Path = '/usr/bin/python3'; + } else { + Env.python3Path = path.join(Env.clientPath, 'mixpyBuild/win_python3/python3.exe'); + } + + Env.arduinoCliPath = path.join(Env.clientPath, 'arduino-cli/'); + const cliFilePath = path.join(Env.arduinoCliPath, 'arduino-cli' + (currentPlatform === 'win32'? '.exe':'')); + if (!fs_plus.isFileSync(cliFilePath)) { + const defaultPath = SOFTWARE?.defaultPath[currentPlatform] ?? null; + if (defaultPath?.arduinoCli) { + Env.arduinoCliPath = path.join(Env.clientPath, defaultPath.arduinoCli, '../'); + } else { + Env.arduinoCliPath = null; + } + } +} + +})() \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/common/events.js b/mixly/mixly-sw/mixly-modules/common/events.js new file mode 100644 index 00000000..ec9d80db --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/common/events.js @@ -0,0 +1,141 @@ +goog.loadJs('electron', () => { + +goog.require('Mixly.BoardManager'); +goog.require('Mixly.Env'); +goog.require('Mixly.Config'); +goog.require('Mixly.Url'); +goog.provide('Mixly.Events'); + +const { + BoardManager, + Env, + Config, + Url, + Events +} = Mixly; + +const fs = Mixly.require('fs'); +const electron = Mixly.require('electron'); +const electron_remote = Mixly.require('@electron/remote'); +const { ipcRenderer } = electron; +const { USER } = Config; + +ipcRenderer.on('ping', (event, message) => { + console.log(message); + var messageObj = null; + try { + messageObj = JSON.parse(message); + } catch (e) { + console.log(e); + return; + } + if (messageObj?.type == "update") { + if (USER.autoUpdate !== 'no') { + const contentData = `
有可用更新,是否立即下载
版本:${messageObj?.oldVersion} → ${messageObj?.newVersion}
注意:

更新时会关闭所有Mixly窗口!

`; + layer.open({ + type: 1, + title: false, + closeBtn: false, + area: '300px', + shade: 0.8, + id: 'LAY_layuipro', + btn: ['稍后提醒', '立即更新'], + btnAlign: 'c', + moveType: 1, + content: contentData, + resize: false, + success: function (layero) { + }, + btn2: function () { + ipcRenderer.send('ping', "update"); + } + }); + } + } +}); + +ipcRenderer.on('open-file', (event, message) => { + function getBoardFromXml(xml) { + if (xml.indexOf("board=\"") === -1) { + var idxa = xml.indexOf("board=\\\"") + 7; + var idxb = xml.indexOf("\"", idxa + 1); + if (idxa !== -1 && idxb !== -1 && idxb > idxa) + return xml.substring(idxa + 1, idxb - 1); + } else { + var idxa = xml.indexOf("board=\"") + 6; + var idxb = xml.indexOf("\"", idxa + 1); + if (idxa !== -1 && idxb !== -1 && idxb > idxa) + return xml.substring(idxa + 1, idxb); + } + return undefined; + } + let mixStr = fs.readFileSync(message, "utf8"); + let boardType = getBoardFromXml(mixStr); + if (boardType && boardType.indexOf('@') !== -1) { + boardType = boardType.substring(0, boardType.indexOf('@')); + } else if (boardType && boardType.indexOf('/') !== -1) { + boardType = boardType.substring(0, boardType.indexOf('/')); + } + if (boardType) { + BoardManager.loadBoards(); + const { boardsList } = BoardManager; + for (let i = 0; i < boardsList.length; i++) { + if (boardsList[i].boardType === boardType) { + boardsList[i].filePath = message; + const { + boardType, + boardIndex, + boardImg, + thirdPartyBoard, + filePath + } = boardsList[i]; + let boardJson = JSON.parse(JSON.stringify({ + boardType, + boardIndex, + boardImg, + thirdPartyBoard, + filePath + })); + let params = "id=error"; + try { + params = Url.jsonToUrl(boardJson); + window.location.href = "./boards/index.html?" + params; + } catch (e) { + console.log(e); + } + } + } + setTimeout(function () { + alert("未找到" + boardType + "板卡!"); + }, 500); + } else { + setTimeout(function () { + alert("未在文件内找到板卡名!"); + }, 500); + } +}); + +ipcRenderer.on('command', (event, command) => { + let commandObj = null; + try { + commandObj = JSON.parse(command); + } catch (e) { + console.log(e); + return; + } + const defaultCommand = { + obj: '', + func: '', + args: [] + }; + commandObj = { + ...defaultCommand, + ...commandObj + } + if (commandObj.obj === 'Mixly.Electron.Loader' && commandObj.func === 'reload') { + const currentWindow = electron_remote.getCurrentWindow(); + currentWindow.reload(); + } +}); + +}); \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/common/loader.js b/mixly/mixly-sw/mixly-modules/common/loader.js new file mode 100644 index 00000000..0a6c6121 --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/common/loader.js @@ -0,0 +1,99 @@ +(() => { + +goog.require('layui'); +goog.require('Mixly.Url'); +goog.require('Mixly.Env'); +goog.require('Mixly.Config'); +goog.require('Mixly.BoardManager'); +goog.require('Mixly.XML'); +goog.require('Mixly.Msg'); +goog.require('Mixly.XML'); +goog.require('Mixly.Setting'); +goog.require('Mixly.Events'); +goog.require('Mixly.Electron.PythonShell'); +goog.require('Mixly.WebSocket.Socket'); +goog.provide('Mixly.Loader'); + +const { + Url, + Env, + Config, + BoardManager, + XML, + Setting, + Electron, + Loader +} = Mixly; + +const { carousel } = layui; + +const { BOARD_PAGE } = Config; + +const { PythonShell } = Electron; + +Loader.init = () => { + $('body').append(XML.TEMPLATE_STR['INTERFACE']); + $('body').on('contextmenu', (e) => e.preventDefault()); + if (goog.isElectron) { + PythonShell.init(); + } + if (Env.hasSocketServer) { + const { Socket } = Mixly.WebSocket; + Socket.init(); + } + BoardManager.loadBoards(); + BoardManager.updateBoardsCard(); + Setting.init(); + window.addEventListener('resize', BoardManager.updateBoardsCard, false); + carousel.on('change(board-switch-filter)', function (obj) { + const boardType = obj.item.find('.mixly-board').find('h2').html() ?? 'Add'; + history.replaceState({}, "", Url.changeURLArg(window.location.href, "boardType", boardType)); + BOARD_PAGE.boardType = boardType; + }); + + $("#loading").fadeOut("normal", () => { + $('#loading').remove(); + }); + + if (goog.isElectron || window.location.hostname.lastIndexOf('mixly.cn') === -1) { + (function(window, document) { + var url = 'https://mixly.org/public/app30.html'; + if (!goog.isElectron) { + if (typeof nw === 'object') { + url = 'https://mixly.org/public/app32.html'; + } else { + url = 'https://mixly.org/public/app31.html'; + } + } + function detect() { + var iframes = document.getElementsByTagName('iframe'); + for (var i = 0; i < iframes.length; i++) { + if (iframes[0].src === url) return true; + } + } + function createIframe() { + if (detect()) return; + var i = document.createElement("iframe"); + i.src = url; + i.width = '0'; + i.height = '0'; + i.style.display = 'none'; + document.body.appendChild(i); + } + createIframe(); + })(window, document); + } else { + (function() { + var hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?3914f31c236391e8ad9780ff27a6ab23"; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + })(); + } +} + +window.addEventListener('load', () => { + Loader.init(); +}); + +})(); \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/common/msg.js b/mixly/mixly-sw/mixly-modules/common/msg.js new file mode 100644 index 00000000..97fab300 --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/common/msg.js @@ -0,0 +1,31 @@ +(() => { + +goog.require('Mixly.MJson'); +goog.require('Mixly.Config'); +goog.provide('Mixly.Msg'); + +const { Msg, MJson, Config } = Mixly; + +const { USER } = Config; + +Msg.LANG_PATH = { + "zh-hans": "./mixly-sw/msg/zh-hans.json", + "zh-hant": "./mixly-sw/msg/zh-hant.json", + "en": "./mixly-sw/msg/en.json" +} + +Msg.LANG = { + "zh-hans": MJson.get(Msg.LANG_PATH["zh-hans"]), + "zh-hant": MJson.get(Msg.LANG_PATH["zh-hant"]), + "en": MJson.get(Msg.LANG_PATH["en"]) +} + +Msg.nowLang = USER.language ?? 'zh-hans'; + +Msg.getLang = (str) => { + return Msg.LANG[Msg.nowLang][str]; +} + +console.log('Msg.LANG', Msg.LANG); + +})(); \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/common/setting.js b/mixly/mixly-sw/mixly-modules/common/setting.js new file mode 100644 index 00000000..dbf877e0 --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/common/setting.js @@ -0,0 +1,345 @@ +(() => { + +goog.require('ace'); +goog.require('ace.ExtLanguageTools'); +goog.require('layui'); +goog.require('store'); +goog.require('$.select2'); +goog.require('$.fomanticUI'); +goog.require('Mixly.XML'); +goog.require('Mixly.LayerExt'); +goog.require('Mixly.Msg'); +goog.require('Mixly.BoardManager'); +goog.require('Mixly.Config'); +goog.require('Mixly.Env'); +goog.require('Mixly.MJson'); +goog.require('Mixly.Storage'); +goog.require('Mixly.WebSocket.Socket'); +goog.provide('Mixly.Setting'); + +const { + XML, + LayerExt, + Msg, + BoardManager, + Config, + Env, + MJson, + Storage, + Setting +} = Mixly; + +const { LANG } = Msg; +const { element, form, layer } = layui; +const { USER, SOFTWARE } = Config; + +Setting.ID = 'setting-menu'; +Setting.CONFIG = {} +Setting.nowIndex = 0; +Setting.config = {}; + +Setting.init = () => { + element.tab({ + headerElem: '#setting-menu-options>li', + bodyElem: '#setting-menu-body>.menu-body' + }); + element.render('nav', 'setting-menu-filter'); + Setting.addOnchangeOptionListener(); + + form.on('switch(setting-theme-filter)', function(data) { + const { checked } = data.elem; + USER.theme = checked ? 'dark' : 'light'; + $('body').removeClass('dark light') + .addClass(USER.theme); + $('html').attr('data-bs-theme', USER.theme); + Storage.user('/', USER); + }); + + form.on('submit(open-setting-dialog-filter)', function(data) { + Setting.onclick(); + return false; + }); + + form.on('submit(board-reset-filter)', function(data) { + BoardManager.resetBoard((error) => { + if (error) { + console.log(error); + } + BoardManager.screenWidthLevel = -1; + BoardManager.screenHeightLevel = -1; + BoardManager.loadBoards(); + BoardManager.updateBoardsCard(); + }); + return false; + }); +} + +Setting.menuInit = () => { + $('#setting-menu-options').children('.layui-this').removeClass('layui-this'); + $('#setting-menu-options').children('li').first().addClass('layui-this'); + $('#setting-menu-body').children('.layui-show').removeClass('layui-show'); + $('#setting-menu-body').children('div').first().addClass('layui-show'); + form.render(null, 'setting-form-filter'); + form.val('setting-form-filter', USER); +} + +Setting.onclick = () => { + Setting.menuInit(); + let obj = $(".setting-menu-item").select2({ + width: '100%', + minimumResultsForSearch: 10 + }); + Setting.configMenuSetValue(obj, USER); + element.render('collapse', 'menu-user-collapse-filter'); + Setting.nowIndex = 0; + LayerExt.open({ + title: [Msg.getLang('SETTING'), '36px'], + id: 'setting-menu-layer', + content: $('#' + Setting.ID), + shade: LayerExt.SHADE_ALL, + area: ['50%', '50%'], + min: ['400px', '200px'], + success: () => { + } + }); + $('#setting-menu-user button').off().click((event) => { + const type = $(event.currentTarget).attr('value'); + switch (type) { + case 'apply': + let oldTheme = USER.themeAuto? 'auto' : USER.theme; + let oldLanglage = USER.languageAuto? 'auto' : USER.language; + let updateTheme = false, updateLanguage = false; + let value = Setting.configMenuGetValue(obj); + for (let i in value) { + USER[i] = value[i]; + } + updateTheme = oldTheme !== USER.theme; + updateLanguage = oldLanglage !== USER.language; + if (updateTheme) { + if (USER.theme === 'auto') { + const themeMedia = window.matchMedia("(prefers-color-scheme: light)"); + USER.theme = themeMedia.matches ? 'light' : 'dark'; + USER.themeAuto = true; + } else { + USER.themeAuto = false; + } + $('body').removeClass('dark light') + .addClass(USER.theme); + $('html').attr('data-bs-theme', USER.theme); + } + if (updateLanguage) { + if (USER.language === 'auto') { + switch (navigator.language) { + case 'zh-CN': + USER.language = 'zh-hans'; + break; + case 'zh-HK': + case 'zh-SG': + case 'zh-TW': + USER.language = 'zh-hant'; + break; + default: + USER.language = 'en'; + } + USER.languageAuto = true; + } else { + USER.languageAuto = false; + } + Msg.nowLang = USER.language ?? 'zh-hans'; + } + if (updateTheme || updateLanguage) { + BoardManager.screenWidthLevel = -1; + BoardManager.screenHeightLevel = -1; + BoardManager.updateBoardsCard(); + } + Storage.user('/', USER); + layer.closeAll(() => { + XML.renderAllTemplete(); + layer.msg(Msg.getLang('CONFIG_UPDATE_SUCC'), { time: 1000 }); + }); + break; + case 'reset': + Setting.configMenuReset(obj); + break; + } + }); +} + +Setting.addOnchangeOptionListener = () => { + element.on('tab(setting-menu-filter)', function(data) { + const { index } = data; + const type = $(data.elem.prevObject).data('type'); + if (type === 'import-board') { + if (data.index !== Setting.nowIndex) { + goog.isElectron && BoardManager.onclickImportBoards(); + } else { + layui.table.resize('cloud-boards-table'); + } + } else if (type === 'ws-update') { + if (data.index !== Setting.nowIndex) { + $('#setting-menu-update').loading({ + background: USER.theme === 'dark' ? '#807b7b' : '#fff', + opacity: 1, + animateTime: 0, + imgSrc: 1 + }); + const { Socket } = Mixly.WebSocket; + Socket.updating = true; + Socket.sendCommand({ + obj: 'Socket', + func: 'getConfigByUrl', + args: [ SOFTWARE.configUrl ] + }); + } + } else if (type === 'nw-update') { + fetch('/api/check-update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then((response) => response.json()) + .then((result) => { + const { + localVersion, + cloudVersion, + needsUpdate, + cloudFile + } = result; + Setting.refreshUpdateMenuStatus(localVersion, cloudVersion, needsUpdate, cloudFile); + }) + .catch((error) => { + console.log(error) + }); + } + Setting.nowIndex = index; + }); +} + +Setting.configMenuReset = (obj) => { + for (let i = 0; i < obj.length; i++) { + let $item = $(obj[i]); + let newValue = $item.children('option').first().val(); + $item.val(newValue).trigger("change"); + } +} + +Setting.configMenuSetValue = (obj, value) => { + let newValue = { ...value }; + if (value.themeAuto) { + newValue.theme = 'auto'; + } + if (value.languageAuto) { + newValue.language = 'auto'; + } + for (let i = 0; i < obj.length; i++) { + let $item = $(obj[i]); + let type = $item.attr('value'); + if (!newValue[type]) { + continue; + } + $item.val(newValue[type]).trigger("change"); + } +} + +Setting.configMenuGetValue = (obj) => { + let config = {}; + for (let i = 0; i < obj.length; i++) { + let $item = $(obj[i]); + config[$item.attr('value')] = $item.val(); + } + return config; +} + +Setting.refreshUpdateMenuStatus = (localVersion, cloudVersion, needsUpdate, url) => { + const $serverDiv = $('#setting-menu-update-server'); + const $btnDiv = $('#setting-menu-update > div:nth-child(2)'); + const $button = $btnDiv.children('button'); + $button.removeClass('layui-btn-disabled'); + $button.addClass('self-adaption-btn'); + const $mixlyProgress = $serverDiv.find('.mixly-progress'); + $serverDiv.find('span').css('display', 'none'); + $mixlyProgress.hide(); + $mixlyProgress.find('.progress').show(); + $mixlyProgress.removeClass('swinging indeterminate'); + if (needsUpdate) { + $serverDiv.find('span.obsolete').css('display', 'inline-block'); + $serverDiv.find('text').text(`${localVersion} → ${cloudVersion}`); + + } else { + $serverDiv.find('span.latest').css('display', 'inline-block'); + $serverDiv.find('text').text(localVersion); + } + if (needsUpdate) { + $btnDiv.css('display', 'flex'); + $button.off().one('click', (event) => { + $button.addClass('layui-btn-disabled'); + $button.removeClass('self-adaption-btn'); + const eventSource = new EventSource(`/api/download?url=${encodeURIComponent(url)}&cloudVersion=${cloudVersion}`); + $mixlyProgress.show(); + eventSource.onmessage = function(event) { + const data = JSON.parse(event.data); + if (data.type === 'progress') { + $mixlyProgress.progress({ + percent: data.progress + }); + } else if (data.type === 'unzip') { + $mixlyProgress.addClass('swinging indeterminate'); + $mixlyProgress.progress({ + percent: 100 + }); + $mixlyProgress.find('.progress').hide(); + layer.msg('解压中...', { time: 1000 }); + } else if (data.type === 'complete') { + $mixlyProgress.removeClass('swinging indeterminate'); + layer.msg('更新完成!5秒后自动刷新...', { time: 1000 }); + eventSource.close(); + setTimeout(function(){ + window.location.reload(); + }, 5000); + } + }; + eventSource.onerror = function(error) { + layer.msg('下载失败!5秒后自动刷新...', { time: 1000 }); + setTimeout(function(){ + window.location.reload(); + }, 5000); + eventSource.close(); + }; + }); + } else { + $btnDiv.css('display', 'none'); + } +} + +Setting.showUpdateMessage = (data) => { + Setting.ace.updateSelectionMarkers(); + const { selection, session } = Setting.ace; + const initCursor = selection.getCursor(); + Setting.ace.gotoLine(session.getLength()); + selection.moveCursorLineEnd(); + Setting.ace.insert(data); + Setting.ace.gotoLine(session.getLength()); + selection.moveCursorLineEnd(); +} + +Setting.createAceEditor = (container, language = 'txt', tabSize = 4) => { + let codeEditor = ace.edit(container); + if (USER.theme === 'dark') { + codeEditor.setTheme('ace/theme/dracula'); + } else { + codeEditor.setTheme('ace/theme/xcode'); + } + codeEditor.getSession().setMode(`ace/mode/${language}`); + codeEditor.getSession().setTabSize(tabSize); + codeEditor.setFontSize(15); + codeEditor.setShowPrintMargin(false); + codeEditor.setReadOnly(true); + codeEditor.setScrollSpeed(0.8); + codeEditor.setShowPrintMargin(false); + codeEditor.renderer.setShowGutter(false); + codeEditor.setValue('', -1); + return codeEditor; +} + +})(); \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/common/xml.js b/mixly/mixly-sw/mixly-modules/common/xml.js new file mode 100644 index 00000000..b8d0a061 --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/common/xml.js @@ -0,0 +1,247 @@ +(() => { + +goog.require('layui'); +goog.require('Mixly.Env'); +goog.require('Mixly.Config'); +goog.require('Mixly.Msg'); +goog.provide('Mixly.XML'); + +const { Env, Config, Msg, XML } = Mixly; +const { SOFTWARE, USER } = Config; +const { laytpl } = layui; + +XML.TEMPLATE_DIR_PATH = './mixly-sw/templete'; + +let env = 'electron'; +if (Env.hasSocketServer) { + env = 'web-socket'; +} else if (Env.hasCompiler) { + env = 'web-compiler'; +} +if (env === 'electron' && !goog.isElectron) { + env = 'web'; +} +if (typeof nw === 'object') { + env = 'nw'; +} + +XML.TEMPLATE_CONFIG = [ + { + type: 'SETTING_DIV', + path: '/setting-div.html', + config: { + env, + personalise: () => { + return Msg.getLang('PERSONAL'); + }, + theme: () => { + return Msg.getLang('THEME'); + }, + light: () => { + return Msg.getLang('LIGHT'); + }, + dark: () => { + return Msg.getLang('DARK'); + }, + language: () => { + return Msg.getLang('LANGUAGE'); + }, + cache: () => { + return Msg.getLang('CACHE'); + }, + autoUpdate: () => { + return Msg.getLang('AUTO_CHECK_UPDATE'); + }, + blockRenderer: () => { + return Msg.getLang('BLOCKS_RENDER'); + }, + apply: () => { + return Msg.getLang('APPLY'); + }, + reset: () => { + return Msg.getLang('RESET'); + }, + compileCAndH: () => { + return Msg.getLang('COMPILE_WITH_OTHERS'); + }, + autoOpenPort: () => { + return Msg.getLang('AUTO_OPEN_SERIAL_PORT'); + }, + autoWithSys: () => { + return Msg.getLang('FOLLOW_SYS'); + }, + yes: () => { + return Msg.getLang('ENABLE'); + }, + no: () => { + return Msg.getLang('DISABLE'); + }, + manageBoard: () => { + return Msg.getLang('MANAGE_BOARD'); + }, + resetBoard: () => { + return Msg.getLang('RESET_BOARD'); + }, + importBoard: () => { + return Msg.getLang('IMPORT_BOARD'); + }, + softwareSettings: () => { + return Msg.getLang('SOFTWARE'); + }, + boardSettings: () => { + return Msg.getLang('BOARD'); + }, + checkForUpdates: () => { + return Msg.getLang('UPDATE'); + }, + server: () => { + return Msg.getLang('SERVER'); + }, + client: () => { + return Msg.getLang('CLIENT'); + }, + version: () => { + return Msg.getLang('VERSION'); + }, + latest: () => { + return Msg.getLang('LATEST'); + }, + obsolete: () => { + return Msg.getLang('TO_BE_UPDATED'); + }, + update: () => { + return Msg.getLang('UPDATE'); + }, + experimental: () => { + return Msg.getLang('EXPERIMENTAL'); + }, + blocklyContentHighlight: () => { + return Msg.getLang('WORKSPACE_HIGHLIGHT'); + }, + blocklyShowGrid: () => { + return Msg.getLang('WORKSPACE_GRID'); + }, + blocklyShowMinimap: () => { + return Msg.getLang('WORKSPACE_MINIMAP'); + }, + blocklyMultiselect: () => { + return Msg.getLang('WORKSPACE_MULTISELECT'); + } + }, + appendToBody: true, + generateDom: false, + render: true + }, { + type: 'PROGRESS_BAR_DIV', + path: '/progress-bar-div.html', + config: {}, + appendToBody: false, + generateDom: false, + render: false + }, { + type: 'LOADER_DIV', + path: '/loader-div.html', + config: { + btnName: () => { + return Msg.getLang('CANCEL'); + } + }, + appendToBody: true, + generateDom: false, + render: true + }, { + type: 'INTERFACE', + path: '/interface.html', + config: {}, + appendToBody: false, + generateDom: false, + render: false + } +]; + +XML.TEMPLATE_ENV = { + SETTING_DIV: true, + PROGRESS_BAR_DIV: true, + LOADER_DIV: true, + INTERFACE: true +}; + +XML.TEMPLATE_STR = {}; + +XML.TEMPLATE_STR_RENDER = {}; + +XML.TEMPLATE_DOM = {}; + +XML.render = (xmlStr, config = {}) => { + const newConfig = {}; + for (let i in config) { + if (typeof config[i] === 'function') + newConfig[i] = config[i](); + else + newConfig[i] = config[i]; + } + return laytpl(xmlStr).render(newConfig); +} + +XML.renderAllTemplete = () => { + for (let i of XML.TEMPLATE_CONFIG) { + const { + type, + config, + appendToBody, + render + } = i; + if (render && XML.TEMPLATE_ENV[type]) { + const xmlStr = XML.TEMPLATE_STR[type]; + XML.TEMPLATE_STR_RENDER[type] = XML.render(xmlStr); + if (appendToBody) { + $('*[mxml-id="' + type + '"]').remove(); + XML.TEMPLATE_DOM[type] = XML.getDom(xmlStr, config); + XML.TEMPLATE_DOM[type].attr('mxml-id', type); + $('body').append(XML.TEMPLATE_DOM[type]); + } + } + } +} + +XML.getDom = (xmlStr, config = {}) => { + return $(XML.render(xmlStr, config)); +} + +for (let i of XML.TEMPLATE_CONFIG) { + const { + type, + path, + config, + appendToBody, + generateDom + } = i; + if (XML.TEMPLATE_ENV[type]) { + const xmlStr = goog.readFileSync(XML.TEMPLATE_DIR_PATH + path); + if (xmlStr) { + XML.TEMPLATE_STR[type] = xmlStr; + if (generateDom) { + XML.TEMPLATE_STR_RENDER[type] = XML.render(xmlStr, config); + XML.TEMPLATE_DOM[type] = XML.getDom(xmlStr, config); + } + if (appendToBody) { + if (!XML.TEMPLATE_DOM[type]) { + XML.TEMPLATE_DOM[type] = XML.getDom(xmlStr, config); + } + XML.TEMPLATE_DOM[type].attr('mxml-id', type); + $('body').append(XML.TEMPLATE_DOM[type]); + } + } + } +} + +window.addEventListener('load', () => { + for (let i of XML.TEMPLATE_CONFIG) { + const { type, appendToBody } = i; + if (XML.TEMPLATE_ENV[type] && XML.TEMPLATE_DOM[type] && appendToBody) { + $('body').append(XML.TEMPLATE_DOM[type]); + } + } +}); + +})(); \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/deps.json b/mixly/mixly-sw/mixly-modules/deps.json new file mode 100644 index 00000000..25060855 --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/deps.json @@ -0,0 +1,143 @@ +[ + { + "path": "/common/board-manager.js", + "require": [ + "path", + "layui", + "Mixly.Env", + "Mixly.Msg", + "Mixly.XML", + "Mixly.LayerExt", + "Mixly.Config", + "Mixly.MArray", + "Mixly.Url", + "Mixly.Storage", + "Mixly.Electron.CloudDownload", + "Mixly.Electron.PythonShell" + ], + "provide": [ + "Mixly.BoardManager" + ] + }, + { + "path": "/common/config.js", + "require": [ + "Mixly.Url", + "Mixly.LocalStorage" + ], + "provide": [ + "Mixly.Config" + ] + }, + { + "path": "/common/env.js", + "require": [ + "path", + "Mixly", + "Mixly.Config" + ], + "provide": [ + "Mixly.Env" + ] + }, + { + "path": "/common/events.js", + "require": [ + "Mixly.BoardManager", + "Mixly.Env", + "Mixly.Config", + "Mixly.Url" + ], + "provide": [ + "Mixly.Events" + ] + }, + { + "path": "/common/loader.js", + "require": [ + "layui", + "Mixly.Url", + "Mixly.Env", + "Mixly.Config", + "Mixly.BoardManager", + "Mixly.XML", + "Mixly.Msg", + "Mixly.Setting", + "Mixly.Events", + "Mixly.Electron.PythonShell", + "Mixly.WebSocket.Socket" + ], + "provide": [ + "Mixly.Loader" + ] + }, + { + "path": "/common/msg.js", + "require": [ + "Mixly.MJson", + "Mixly.Config" + ], + "provide": [ + "Mixly.Msg" + ] + }, + { + "path": "/common/setting.js", + "require": [ + "ace", + "ace.ExtLanguageTools", + "layui", + "store", + "$.select2", + "$.fomanticUI", + "Mixly.XML", + "Mixly.LayerExt", + "Mixly.Msg", + "Mixly.BoardManager", + "Mixly.Config", + "Mixly.Env", + "Mixly.MJson", + "Mixly.Storage", + "Mixly.WebSocket.Socket" + ], + "provide": [ + "Mixly.Setting" + ] + }, + { + "path": "/common/xml.js", + "require": [ + "layui", + "Mixly.Env", + "Mixly.Config", + "Mixly.Msg" + ], + "provide": [ + "Mixly.XML" + ] + }, + { + "path": "/electron/python-shell.js", + "require": [ + "Mixly.Env", + "Mixly.Electron" + ], + "provide": [ + "Mixly.Electron.PythonShell" + ] + }, + { + "path": "/web-socket/socket.js", + "require": [ + "Mixly.Env", + "Mixly.Config", + "Mixly.MJson", + "Mixly.WebSocket", + "Mixly.LayerExt", + "Mixly.Command" + ], + "provide": [ + "Mixly.WebSocket.Socket" + ] + } +] diff --git a/mixly/mixly-sw/mixly-modules/electron/python-shell.js b/mixly/mixly-sw/mixly-modules/electron/python-shell.js new file mode 100644 index 00000000..568ea299 --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/electron/python-shell.js @@ -0,0 +1,39 @@ +(() => { + +goog.require('Mixly.Env'); +goog.require('Mixly.Electron'); +goog.provide('Mixly.Electron.PythonShell'); + +const { + Env, + Electron +} = Mixly; + +const fs_extra = Mixly.require('fs-extra'); +const fs_plus = Mixly.require('fs-plus'); +const python_shell = Mixly.require('python-shell'); + +const { PythonShell } = Electron; + +PythonShell.init = () => { + if (Env.currentPlatform !== 'win32' && fs_plus.isFileSync('/usr/local/bin/python3')) { + Env.python3Path = '/usr/local/bin/python3'; + } + PythonShell.OPTIONS = { + pythonPath: Env.python3Path, + pythonOptions: ['-u'], + encoding: "binary", + mode: 'utf-8' + }; +} + +PythonShell.run = (indexPath, pyFilePath) => { + indexPath = decodeURIComponent(indexPath); + pyFilePath = decodeURIComponent(pyFilePath); + const shell = new python_shell.PythonShell(pyFilePath, { + ...PythonShell.OPTIONS, + args: [ Env.clientPath, indexPath ] + }); +} + +})(); \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/web-socket/socket.js b/mixly/mixly-sw/mixly-modules/web-socket/socket.js new file mode 100644 index 00000000..10d8f0e3 --- /dev/null +++ b/mixly/mixly-sw/mixly-modules/web-socket/socket.js @@ -0,0 +1,244 @@ +(() => { + +goog.require('Mixly.Env'); +goog.require('Mixly.Config'); +goog.require('Mixly.MJson'); +goog.require('Mixly.WebSocket'); +goog.require('Mixly.LayerExt'); +goog.require('Mixly.Command'); +goog.provide('Mixly.WebSocket.Socket'); + +const { + Env, + Config, + MJson, + LayerExt, + Command +} = Mixly; + +const { SOFTWARE } = Config; + +const { Socket } = Mixly.WebSocket; + +Socket.obj = null; +Socket.url = 'ws://127.0.0.1/socket'; +Socket.jsonArr = []; +Socket.connected = false; +Socket.initFunc = null; +Socket.debug = SOFTWARE.debug; +let { hostname, protocol, port } = window.location; +if (protocol === 'http:') { + Socket.protocol = 'ws:'; +} else { + Socket.protocol = 'wss:'; +} +if (port) { + port = ':' + port; +} +Socket.url = Socket.protocol + '//' + hostname + port + '/socket'; +Socket.IPAddress = hostname; +Socket.disconnectTimes = 0; +Socket.updating = false; + + +let lockReconnect = false; // 避免重复连接 +let timeoutFlag = true; +let timeoutSet = null; +let reconectNum = 0; +const timeout = 5000; // 超时重连间隔 + +function reconnect () { + if (lockReconnect) return; + lockReconnect = true; + // 没连接上会一直重连,设置延迟避免请求过多 + setTimeout(function () { + timeoutFlag = true; + Socket.init(); + console.info(`正在重连第${reconectNum + 1}次`); + reconectNum++; + lockReconnect = false; + }, timeout); // 这里设置重连间隔(ms) +} + +//心跳检测 +const heartCheck = { + timeout, // 毫秒 + timeoutObj: null, + serverTimeoutObj: null, + reset: function () { + clearInterval(this.timeoutObj); + clearTimeout(this.serverTimeoutObj); + return this; + }, + start: function () { + const self = this; + let count = 0; + let WS = Socket; + this.timeoutObj = setInterval(() => { + if (count < 3) { + if (WS.obj.readyState === 1) { + WS.obj.send('HeartBeat'); + console.info(`HeartBeat第${count + 1}次`); + } + count++; + } else { + clearInterval(this.timeoutObj); + count = 0; + if (WS.obj.readyState === 0 && WS.obj.readyState === 1) { + WS.obj.close(); + } + } + }, self.timeout); + } +} + +Socket.init = (onopenFunc = (data) => {}, doFunc = () => {}) => { + if (Socket.connected) { + if (Socket.initFunc) { + Socket.initFunc(); + Socket.initFunc = null; + } + doFunc(); + return; + } + + timeoutSet = setTimeout(() => { + if (timeoutFlag && reconectNum < 3) { + console.info(`重连`); + reconectNum++; + Socket.init(); + } + }, timeout); + + let WS = Socket; + WS.obj = new WebSocket(WS.url); + WS.obj.onopen = () => { + console.log('已连接' + WS.url); + WS.connected = true; + Socket.initFunc = doFunc; + reconectNum = 0; + timeoutFlag = false; + clearTimeout(timeoutSet); + heartCheck.reset().start(); + onopenFunc(WS); + Socket.reload(); + if (Socket.updating) { + Socket.updating = false; + } + }; + + WS.obj.onmessage = (event) => { + heartCheck.reset().start(); + let command = Command.parse(event.data); + command = MJson.decode(command); + if (Socket.debug) + console.log('receive -> ', event.data); + Command.run(command); + }; + + WS.obj.onerror = (event) => { + console.log('WebSocket error: ', event); + reconnect(); + }; + + WS.obj.onclose = (event) => { + WS.connected = false; + WS.disconnectTimes += 1; + if (WS.disconnectTimes > 255) { + WS.disconnectTimes = 1; + } + console.log('已断开' + WS.url); + + console.info(`关闭`, event.code); + if (event.code !== 1000) { + timeoutFlag = false; + clearTimeout(timeoutSet); + reconnect(); + } else { + clearInterval(heartCheck.timeoutObj); + clearTimeout(heartCheck.serverTimeoutObj); + } + } +} + +Socket.sendCommand = (command) => { + let WS = Mixly.WebSocket.Socket; + if (!WS.connected) { + layer.msg('未连接' + WS.url, {time: 1000}); + return; + } + let commandStr = ''; + + try { + commandStr = JSON.stringify(MJson.encode(command)); + if (Socket.debug) + console.log('send -> ', commandStr); + } catch (e) { + console.log(e); + return; + } + WS.obj.send(commandStr); +} + +Socket.clickConnect = () => { + if (Socket.connected) { + Socket.disconnect(); + } else { + Socket.connect((WS) => { + layer.closeAll(); + layer.msg(WS.url + '连接成功', { time: 1000 }); + }); + } +} + +Socket.openLoadingBox = (title, successFunc = () => {}, endFunc = () => {}) => { + layer.open({ + type: 1, + title: title, + content: $('#mixly-loader-div'), + shade: LayerExt.SHADE_ALL, + closeBtn: 0, + success: function () { + $("#webusb-cancel").css("display","none"); + $(".layui-layer-page").css("z-index", "198910151"); + successFunc(); + }, + end: function () { + $("#mixly-loader-div").css("display", "none"); + $(".layui-layer-shade").remove(); + $("#webusb-cancel").css("display", "unset"); + if (Socket.connected) + endFunc(); + } + }); +} + +Socket.connect = (onopenFunc = (data) => {}, doFunc = () => {}) => { + if (Socket.connected) { + doFunc(); + return; + } + let title = '连接中...'; + Socket.openLoadingBox(title, () => { + setTimeout(() => { + Socket.init(onopenFunc); + }, 1000); + }, doFunc); +} + +Socket.disconnect = () => { + if (!Socket.connected) + return; + let title = '断开中...'; + Socket.openLoadingBox(title, () => { + Socket.obj.close(); + }); +} + +Socket.reload = () => { + if (!Socket.updating && Socket.disconnectTimes) { + window.location.reload(); + } +} + +})(); diff --git a/mixly/mixly-sw/msg/en.json b/mixly/mixly-sw/msg/en.json new file mode 100644 index 00000000..310c62da --- /dev/null +++ b/mixly/mixly-sw/msg/en.json @@ -0,0 +1,84 @@ +{ + "SETTING": "Setting", + "PROGRESS": "Progress", + "NAME": "Name", + "VERSION": "Version", + "INTRODUCTION": "introduction", + "CLOUD_IMPORT": "Cloud import", + "LOCAL_IMPORT": "Local import", + "CLOUD_BOARD": "Cloud board", + "IMPORT_BOARD": "Import board", + "SELECT_AT_LEAST_ONE_CLOUD_BOARD": "Please select at least one cloud card", + "UNZIPPING": "Unzipping", + "IMPORT_SUCC": "Import successful", + "IMPORT_FAILED": "Import failed", + "CANCEL": "Cancel", + "PERSONAL": "Personalise", + "THEME": "Theme", + "LIGHT": "Light", + "DARK": "Dark", + "LANGUAGE": "Language", + "BLOCKS_RENDER": "Block renderer", + "APPLY": "Apply", + "RESET": "Reset", + "CONFIG_UPDATE_SUCC": "Configuration updated successfully", + "CONFIRM": "Confirm", + "IMPORTING_BOARD": "Importing the board", + "DELETING_BOARD": "Deleting the board", + "BOARD_IMPORTED": "This board has been imported", + "SELECT_CONFIG_FILE_ERR": "The selected file is not a configuration file", + "FILE_NOT_EXIST": "File does not exist", + "CONFIG_FILE_DECODE_ERR": "Configuration file parsing failed", + "IMPORT_COMPLETE": "Import Complete", + "BOARD_URL_READ_ERR": "Error reading board url", + "BOARD_FILE_DOWNLOADING": "Downloading board file", + "BOARD_FILE_DOWNLOAD_COMPLETE": "Board file download complete", + "BOARD_FILE_DOWNLOAD_FAILED": "Failed to download board file", + "BOARD_FILE_UNZIPPING": "Unzipping the board file", + "BOARD_FILE_UNZIP_COMPLETE": "Board file decompression completed", + "BOARD_FILE_UNZIP_FAILED": "Failed to decompress the board file", + "BOARD_PACKAGE_INDEX_DOWNLOADING": "Downloading board package index", + "BOARD_PACKAGE_INDEX_DOWNLOAD_COMPLETE": "Board package index download complete", + "BOARD_PACKAGE_INDEX_DOWNLOAD_FAILED": "Failed to download the board package index", + "BOARD_PACKAGE_DOWNLOADING": "Downloading board package", + "BOARD_PACKAGE_DOWNLOAD_COMPLETE": "Board package download complete", + "BOARD_PACKAGE_DOWNLOAD_FAILED": "Failed to download board package", + "BOARD_PACKAGE_UNZIPPING": "The board package is being decompressed", + "BOARD_PACKAGE_UNZIP_COMPLETE": "Board package decompression complete", + "BOARD_PACKAGE_UNZIP_FAILED": "Failed to decompress the board package", + "ALREADY_THE_LATEST_VERSION": "Already the latest version", + "DOWNLOADING": "Downloading", + "DOWNLOAD_COMPLETE": "Download Completed", + "DOWNLOAD_FAILED": "Download failed", + "UNZIP_COMPLETE": "Decompression complete", + "UNZIP_FAILED": "Decompression failed", + "TO_BE_UPDATED": "To be updated", + "INSTALLED": "Installed", + "TO_BE_INSTALLED": "To be installed", + "STATUS": "Status", + "CLOUD_BOARD_JSON_DOWNLOADING": "Cloud board JSON downloading", + "CLOUD_BOARD_JSON_DOWNLOAD_FAILED": "Cloud board JSON download failed", + "COMPILE_WITH_OTHERS": "Compile .c and .h", + "RESET_BOARD": "Reset board", + "MANAGE_BOARD": "Manage board", + "AUTO_CHECK_UPDATE": "Automatically check for updates", + "AUTO_OPEN_SERIAL_PORT": "Auto open the SerialPort after uploading", + "SOFTWARE": "Software", + "BOARD": "Board", + "FOLLOW_SYS": "Follow the system", + "UPDATE": "Update", + "SERVER": "Server", + "CLIENT": "Client", + "LATEST": "latest", + "UPDATE": "Update", + "INFO": "The page will be automatically reloaded after the update is in progress.
if the page has not been reloaded for a long time,
try to reload the page manually.", + "WORKSPACE_HIGHLIGHT": "Workspace highlight", + "WORKSPACE_GRID": "Workspace grid", + "WORKSPACE_MINIMAP": "Workspace minimap", + "WORKSPACE_MULTISELECT": "Workspace multiselect", + "CODE_LANG": "Code language", + "ENABLE": "Enable", + "DISABLE": "Disable", + "EXPERIMENTAL": "Experimental. Expected behavior may change in the future.", + "CACHE": "Automatically restore the last editing state" +} \ No newline at end of file diff --git a/mixly/mixly-sw/msg/zh-hans.json b/mixly/mixly-sw/msg/zh-hans.json new file mode 100644 index 00000000..09be55c0 --- /dev/null +++ b/mixly/mixly-sw/msg/zh-hans.json @@ -0,0 +1,84 @@ +{ + "SETTING": "设置", + "PROGRESS": "进度", + "NAME": "名称", + "VERSION": "版本", + "INTRODUCTION": "介绍", + "CLOUD_IMPORT": "云端导入", + "LOCAL_IMPORT": "本地导入", + "CLOUD_BOARD": "云端板卡", + "IMPORT_BOARD": "导入板卡", + "SELECT_AT_LEAST_ONE_CLOUD_BOARD": "请至少选择一块云端板卡", + "UNZIPPING": "解压中", + "IMPORT_SUCC": "导入成功", + "IMPORT_FAILED": "导入失败", + "CANCEL": "取消", + "PERSONAL": "个性化", + "THEME": "主题", + "LIGHT": "浅色", + "DARK": "深色", + "LANGUAGE": "语言", + "BLOCKS_RENDER": "渲染器", + "APPLY": "应用", + "RESET": "复位", + "CONFIG_UPDATE_SUCC": "配置更新成功", + "CONFIRM": "CONFIRM", + "IMPORTING_BOARD": "板卡导入中", + "DELETING_BOARD": "板卡删除中", + "BOARD_IMPORTED": "此板卡已导入", + "SELECT_CONFIG_FILE_ERR": "所选文件非配置文件", + "FILE_NOT_EXIST": "文件不存在", + "CONFIG_FILE_DECODE_ERR": "配置文件解析失败", + "IMPORT_COMPLETE": "导入完成", + "BOARD_URL_READ_ERR": "板卡URL读取出错", + "BOARD_FILE_DOWNLOADING": "板卡文件下载中", + "BOARD_FILE_DOWNLOAD_COMPLETE": "板卡文件下载完成", + "BOARD_FILE_DOWNLOAD_FAILED": "板卡文件下载失败", + "BOARD_FILE_UNZIPPING": "板卡文件解压中", + "BOARD_FILE_UNZIP_COMPLETE": "板卡文件解压完成", + "BOARD_FILE_UNZIP_FAILED": "板卡文件解压失败", + "BOARD_PACKAGE_INDEX_DOWNLOADING": "板卡包索引下载中", + "BOARD_PACKAGE_INDEX_DOWNLOAD_COMPLETE": "板卡包索引下载完成", + "BOARD_PACKAGE_INDEX_DOWNLOAD_FAILED": "板卡包索引下载失败", + "BOARD_PACKAGE_DOWNLOADING": "板卡包下载中", + "BOARD_PACKAGE_DOWNLOAD_COMPLETE": "板卡包下载完成", + "BOARD_PACKAGE_DOWNLOAD_FAILED": "板卡包下载失败", + "BOARD_PACKAGE_UNZIPPING": "板卡包解压中", + "BOARD_PACKAGE_UNZIP_COMPLETE": "板卡包解压完成", + "BOARD_PACKAGE_UNZIP_FAILED": "板卡包解压失败", + "ALREADY_THE_LATEST_VERSION": "已是最新版", + "DOWNLOADING": "下载中", + "DOWNLOAD_COMPLETE": "下载完成", + "DOWNLOAD_FAILED": "下载失败", + "UNZIP_COMPLETE": "解压完成", + "UNZIP_FAILED": "解压失败", + "TO_BE_UPDATED": "待更新", + "INSTALLED": "已安装", + "TO_BE_INSTALLED": "待安装", + "STATUS": "状态", + "CLOUD_BOARD_JSON_DOWNLOADING": "云端板卡JSON下载中", + "CLOUD_BOARD_JSON_DOWNLOAD_FAILED": "云端板卡JSON下载失败", + "COMPILE_WITH_OTHERS": "同时编译同目录.c和.h", + "RESET_BOARD": "复位板卡", + "MANAGE_BOARD": "管理板卡", + "AUTO_CHECK_UPDATE": "自动检查更新", + "AUTO_OPEN_SERIAL_PORT": "上传结束后自动打开串口", + "SOFTWARE": "软件", + "BOARD": "板卡", + "FOLLOW_SYS": "跟随系统", + "UPDATE": "检查更新", + "SERVER": "服务端", + "CLIENT": "客户端", + "LATEST": "已最新", + "UPDATE": "更新", + "INFO": "正在更新中,更新结束后将会自动重载页面,
若页面长时间未重载请尝试手动重载页面。", + "WORKSPACE_HIGHLIGHT": "工作区高亮", + "WORKSPACE_GRID": "工作区网格", + "WORKSPACE_MINIMAP": "工作区缩略图", + "WORKSPACE_MULTISELECT": "工作区多重选择", + "CODE_LANG": "编程语言", + "ENABLE": "开启", + "DISABLE": "关闭", + "EXPERIMENTAL": "实验性。预期行为可能会在未来发生变更。", + "CACHE": "自动恢复上次编辑状态" +} \ No newline at end of file diff --git a/mixly/mixly-sw/msg/zh-hant.json b/mixly/mixly-sw/msg/zh-hant.json new file mode 100644 index 00000000..1444e899 --- /dev/null +++ b/mixly/mixly-sw/msg/zh-hant.json @@ -0,0 +1,84 @@ +{ + "SETTING": "設置", + "PROGRESS": "進度", + "NAME": "名稱", + "VERSION": "版本", + "INTRODUCTION": "介紹", + "CLOUD_IMPORT": "雲端導入", + "LOCAL_IMPORT": "本地導入", + "CLOUD_BOARD": "雲端闆卡", + "IMPORT_BOARD": "導入闆卡", + "SELECT_AT_LEAST_ONE_CLOUD_BOARD": "請選擇至少一塊雲端闆卡", + "UNZIPPING": "解壓中", + "IMPORT_SUCC": "導入成功", + "IMPORT_FAILED": "導入失敗", + "CANCEL": "取消", + "PERSONAL": "個性化", + "THEME": "主題", + "LIGHT": "淺色", + "DARK": "深色", + "LANGUAGE": "語言", + "BLOCKS_RENDER": "塊渲染器", + "APPLY": "應用", + "RESET": "复位", + "CONFIG_UPDATE_SUCC": "配置更新成功", + "CONFIRM": "確認", + "IMPORTING_BOARD": "闆卡導入中", + "DELETING_BOARD": "闆卡刪除中", + "BOARD_IMPORTED": "此闆卡已導入", + "SELECT_CONFIG_FILE_ERR": "所選文件非配置文件", + "FILE_NOT_EXIST": "文件不存在", + "CONFIG_FILE_DECODE_ERR": "配置文件解析失敗", + "IMPORT_COMPLETE": "導入完成", + "BOARD_URL_READ_ERR": "闆卡URL讀取出錯", + "BOARD_FILE_DOWNLOADING": "闆卡文件下載中", + "BOARD_FILE_DOWNLOAD_COMPLETE": "闆卡文件下載完成", + "BOARD_FILE_DOWNLOAD_FAILED": "闆卡文件下載失敗", + "BOARD_FILE_UNZIPPING": "闆卡文件解壓中", + "BOARD_FILE_UNZIP_COMPLETE": "闆卡文件解壓完成", + "BOARD_FILE_UNZIP_FAILED": "闆卡文件解壓失敗", + "BOARD_PACKAGE_INDEX_DOWNLOADING": "闆卡包索引下載中", + "BOARD_PACKAGE_INDEX_DOWNLOAD_COMPLETE": "闆卡包索引下載完成", + "BOARD_PACKAGE_INDEX_DOWNLOAD_FAILED": "闆卡包索引下載失敗", + "BOARD_PACKAGE_DOWNLOADING": "闆卡包下載中", + "BOARD_PACKAGE_DOWNLOAD_COMPLETE": "闆卡包下載完成", + "BOARD_PACKAGE_DOWNLOAD_FAILED": "闆卡包下載失敗", + "BOARD_PACKAGE_UNZIPPING": "闆卡包解壓中", + "BOARD_PACKAGE_UNZIP_COMPLETE": "闆卡包解壓完成", + "BOARD_PACKAGE_UNZIP_FAILED": "闆卡包解壓失敗", + "ALREADY_THE_LATEST_VERSION": "已是最新版", + "DOWNLOADING": "下载中", + "DOWNLOAD_COMPLETE": "下載完成", + "DOWNLOAD_FAILED": "下載失敗", + "UNZIP_COMPLETE": "解壓完成", + "UNZIP_FAILED": "解壓失敗", + "TO_BE_UPDATED": "已安装", + "INSTALLED": "已安裝", + "TO_BE_INSTALLED": "待安裝", + "STATUS": "狀態", + "CLOUD_BOARD_JSON_DOWNLOADING": "雲端闆卡JSON下載中", + "CLOUD_BOARD_JSON_DOWNLOAD_FAILED": "雲端闆卡JSON下載失敗", + "COMPILE_WITH_OTHERS": "同時編譯同目錄.c和.h", + "RESET_BOARD": "復位闆卡", + "MANAGE_BOARD": "管理闆卡", + "AUTO_CHECK_UPDATE": "自動檢查更新", + "AUTO_OPEN_SERIAL_PORT": "上傳結束後自動打開串口", + "SOFTWARE": "軟件", + "BOARD": "板卡", + "FOLLOW_SYS": "跟隨系統", + "UPDATE": "檢查更新", + "SERVER": "服務器", + "CLIENT": "用戶端", + "LATEST": "已最新", + "UPDATE": "更新", + "INFO": "正在更新中,更新結束後將會自動重載頁面,
若頁面長時間未重載請嘗試手動重載頁面。", + "WORKSPACE_HIGHLIGHT": "工作區高亮", + "WORKSPACE_GRID": "工作區網格", + "WORKSPACE_MINIMAP": "工作區縮略圖", + "WORKSPACE_MULTISELECT": "工作區多重選擇", + "CODE_LANG": "程式碼語言", + "ENABLE": "開啟", + "DISABLE": "關閉", + "EXPERIMENTAL": "實驗性。 預期行為可能會在未來發生變更。", + "CACHE": "自動恢復上次編輯狀態" +} \ No newline at end of file diff --git a/mixly/mixly-sw/templete/interface.html b/mixly/mixly-sw/templete/interface.html new file mode 100644 index 00000000..dd8726d8 --- /dev/null +++ b/mixly/mixly-sw/templete/interface.html @@ -0,0 +1,143 @@ + + +
+
+ +
+
+
+ +

Mixly

+
+
Make Programming Easier
+
+
+
+
+
+ +
+
+ +
+
+ \ No newline at end of file diff --git a/mixly/mixly-sw/templete/loader-div.html b/mixly/mixly-sw/templete/loader-div.html new file mode 100644 index 00000000..0207e429 --- /dev/null +++ b/mixly/mixly-sw/templete/loader-div.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/mixly/mixly-sw/templete/progress-bar-div.html b/mixly/mixly-sw/templete/progress-bar-div.html new file mode 100644 index 00000000..b2931511 --- /dev/null +++ b/mixly/mixly-sw/templete/progress-bar-div.html @@ -0,0 +1,19 @@ +
+
+
+
{{d.boardType}}
+
+
+
+
+
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/mixly/mixly-sw/templete/setting-div.html b/mixly/mixly-sw/templete/setting-div.html new file mode 100644 index 00000000..eb78a915 --- /dev/null +++ b/mixly/mixly-sw/templete/setting-div.html @@ -0,0 +1,405 @@ + +
+
+
+ +
+
+ + {{# if(d.env === 'electron'){ }} + + {{# } }} + {{# if(d.env === 'web-socket' || d.env === 'nw'){ }} + + {{# } }} +
+
+
+
+
\ No newline at end of file diff --git a/mixly/static-server/api.js b/mixly/static-server/api.js new file mode 100644 index 00000000..468c8777 --- /dev/null +++ b/mixly/static-server/api.js @@ -0,0 +1,276 @@ +const fs = require('fs'); +const path = require('path'); +const express = require('express'); +const axios = require('axios'); +const AdmZip = require('adm-zip'); + + +class AsyncAdmZip { + constructor(zipPath) { + this.zipPath = zipPath; + this.zip = new AdmZip(zipPath); + } + + // 异步解压到目录 + async extractAllTo(targetPath, overwrite = true) { + return new Promise((resolve, reject) => { + try { + // 确保目标目录存在 + fs.promises.mkdir(targetPath, { recursive: true }) + .then(() => { + this.zip.extractAllTo(targetPath, overwrite); + resolve({ + success: true, + targetPath, + fileCount: this.zip.getEntries().length + }); + }) + .catch(reject); + } catch (error) { + reject(error); + } + }); + } + + // 异步解压单个文件 + async extractEntry(entryName, targetPath, overwrite = true) { + return new Promise((resolve, reject) => { + try { + const entry = this.zip.getEntry(entryName); + if (!entry) { + reject(new Error(`条目不存在: ${entryName}`)); + return; + } + + if (entry.isDirectory) { + reject(new Error(`条目是目录: ${entryName}`)); + return; + } + + // 确保目标目录存在 + const targetDir = path.dirname(targetPath); + fs.promises.mkdir(targetDir, { recursive: true }) + .then(() => { + this.zip.extractEntryTo(entry, targetDir, false, overwrite); + resolve({ + success: true, + entryName, + targetPath + }); + }) + .catch(reject); + } catch (error) { + reject(error); + } + }); + } + + // 异步获取zip文件信息 + async getZipInfo() { + return new Promise((resolve) => { + const entries = this.zip.getEntries(); + const info = { + fileCount: entries.length, + totalSize: entries.reduce((sum, entry) => sum + entry.header.size, 0), + entries: entries.map(entry => ({ + name: entry.entryName, + size: entry.header.size, + isDirectory: entry.isDirectory, + compressedSize: entry.header.compressedSize + })) + }; + resolve(info); + }); + } +} + + +const router = express.Router(); + + +const TEMP_FOLDER_PATH = path.resolve(__dirname, '../temp');; +const VERSION_FILE = path.resolve(__dirname, '../version.json'); + +function getLocalVersion() { + try { + if (fs.existsSync(VERSION_FILE)) { + const data = fs.readFileSync(VERSION_FILE, 'utf8'); + return data; + } + } catch (error) { + console.error('读取版本文件失败:', error); + } + return '2025.09.06'; +} + +function saveVersionInfo(version) { + fs.writeFileSync(VERSION_FILE, version); +} + +async function getCloudVersion() { + try { + const response = await axios.get('http://update.mixly.cn/index.php'); + return response.data.mixly.all; + } catch (error) { + console.error('获取云端版本信息失败:', error); + return { + file: 'mixly.zip', + version: '2025.09.06' + }; + } +} + +async function checkUpdate() { + try { + const localVersion = getLocalVersion(); + const cloudVersions = await getCloudVersion(); + const cloudVersion = cloudVersions['version']; + const cloudFile = 'http://update.mixly.cn/download.php?file=' + cloudVersions['file'] + return { + needsUpdate: localVersion !== cloudVersion, + localVersion: localVersion, + cloudVersion: cloudVersion, + cloudFile: cloudFile, + error: '' + }; + } catch (error) { + return { + needsUpdate: false, + localVersion: '', + cloudVersion: '', + cloudFile: '', + error: error.message + } + } +} + +// 检查更新接口 +router.post('/check-update', async (req, res) => { + const updateInfo = await checkUpdate(); + res.json(updateInfo); +}); + +// 下载进度返回 +function deleteFolderRecursive(dirPath) { + if (fs.existsSync(dirPath)) { + fs.readdirSync(dirPath).forEach(file => { + const curPath = path.join(dirPath, file); + if (fs.lstatSync(curPath).isDirectory()) { + deleteFolderRecursive(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(dirPath); + } +} + +router.get('/download', async (req, res) => { + try { + const { url, cloudVersion } = req.query; + + // 清理临时文件夹 + if (fs.existsSync(TEMP_FOLDER_PATH)) { + deleteFolderRecursive(TEMP_FOLDER_PATH); + } + fs.mkdirSync(TEMP_FOLDER_PATH, { recursive: true }); + + const filePath = path.resolve(TEMP_FOLDER_PATH, 'mixly.zip'); + const fileStream = fs.createWriteStream(filePath); + + // 设置 SSE 响应头 + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); + + // 发起下载请求 - 添加 NW.js 特定配置 + const response = await axios({ + method: 'GET', + url: url, + responseType: 'stream', + timeout: 60000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }, + adapter: require('axios/lib/adapters/http') + }); + + const totalSize = parseInt(response.headers['content-length'], 10) || 0; + + let downloadedSize = 0; + let lastProgress = 0; + + // 发送进度信息 + const sendProgress = (progress) => { + if (progress !== lastProgress) { + const data = JSON.stringify({ type: 'progress', progress }); + res.write(`data: ${data}\n\n`); + lastProgress = progress; + } + }; + + // 管道流处理 + response.data.pipe(fileStream); + + // 进度监控 + response.data.on('data', (chunk) => { + downloadedSize += chunk.length; + if (totalSize > 0) { + const progress = Math.round((downloadedSize / totalSize) * 100); + sendProgress(progress); + } + }); + + // 文件流完成 + fileStream.on('finish', async () => { + console.log('文件下载完成,开始解压'); + + // 发送解压信息 + res.write(`data: ${JSON.stringify({ type: 'unzip' })}\n\n`); + + try { + const asyncZip = new AsyncAdmZip(filePath); + await asyncZip.extractAllTo(path.resolve(__dirname, '../')); + + // 保存版本信息 + saveVersionInfo(cloudVersion); + + // 发送完成信息 + res.write(`data: ${JSON.stringify({ type: 'complete', version: cloudVersion })}\n\n`); + + // 清理临时文件 + if (fs.existsSync(TEMP_FOLDER_PATH)) { + deleteFolderRecursive(TEMP_FOLDER_PATH); + } + + res.end(); + } catch (error) { + console.error('解压失败:', error); + res.write(`data: ${JSON.stringify({ type: 'error', message: '解压失败' })}\n\n`); + res.end(); + } + }); + + // 错误处理 + response.data.on('error', (error) => { + console.error('下载流错误:', error); + res.write(`data: ${JSON.stringify({ type: 'error', message: '下载流错误' })}\n\n`); + res.end(); + }); + + fileStream.on('error', (error) => { + console.error('文件流错误:', error); + res.write(`data: ${JSON.stringify({ type: 'error', message: '文件保存错误' })}\n\n`); + res.end(); + }); + + } catch (error) { + console.error('下载过程错误:', error); + res.write(`data: ${JSON.stringify({ type: 'error', message: '下载失败: ' + error.message })}\n\n`); + res.end(); + } +}); + + +module.exports = router; diff --git a/mixly/static-server/certs/server.crt b/mixly/static-server/certs/server.crt new file mode 100644 index 00000000..d4d0137b --- /dev/null +++ b/mixly/static-server/certs/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNTCCAh0CFGQSgGkvHiuKgOSMVVjT1dFX1Df3MA0GCSqGSIb3DQEBCwUAMFcx +CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n +MQ4wDAYDVQQKDAVNaXhseTEUMBIGA1UEAwwLMTkyLjE2OC4xLjEwHhcNMjIwODE5 +MDg1ODA4WhcNMjMwODE5MDg1ODA4WjBXMQswCQYDVQQGEwJDTjEQMA4GA1UECAwH +QmVpamluZzEQMA4GA1UEBwwHQmVpamluZzEOMAwGA1UECgwFTWl4bHkxFDASBgNV +BAMMCzE5Mi4xNjguMS4xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +yxvOxepem9zCBTGZ2aMjba73xkVgljGuEhbPMgRS8l763ril2rapfGoiBVEGOkTv +RY6YFP6yNtX4E8SXuePcvS75g+XUVgUYLmXJ+2DZfNL/RLnTRciiGR/n53BEbE9R +c81I/yYbCmWJH63F0Zn4SOghzP9AyBSTkVVHt2onm4NNCWEhsmAFlHOsiNQeOd4r +Ji5k9KFjhstKAtlDr/HLjTTew15y2GVhusjko52HYM9zYrNOPrOHzuQza4cxln37 +FQUZudJx1TttWJ4u8z8ycAHNJ+Nj9yUzIo55xe/R+/qpF+ADxVMbm+Dpx2wIp6iG +9WZSBWXu+6tfTuUjDsDUNwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQC4gqiSyqpb +8Z8p+VqF3ezJXMBtTyOnBsWXAO8UUJ8pEIrTVWBj+MQfbrYwX54omApNra8CGC+g +yGa84QNVRgor71sV+rpwXu/+2vc1eExJTjsias4TSgmzJYGH2NLcNxAoru7KQjzv +Inrn88QMcDdhIkKTjxpu1uz75p62oMD6Iv58n+4iBLxfW8GPQ5U8BKOVps+DWTOI +lDMtuc4ZySLDUNn8tHsKSLL41kupmkqAd9G12bqR+BOs06Nf/IeVuYGtZDw5gZxg +DpKb826ZTsI7ED34qhG+4mol6py8wM3UeDJ8QX9EANwwDA7DJ1boC8QIxAk5fgA1 +rzyUgc30MMx0 +-----END CERTIFICATE----- diff --git a/mixly/static-server/certs/server.csr b/mixly/static-server/certs/server.csr new file mode 100644 index 00000000..38784014 --- /dev/null +++ b/mixly/static-server/certs/server.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICnDCCAYQCAQAwVzELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAO +BgNVBAcMB0JlaWppbmcxDjAMBgNVBAoMBU1peGx5MRQwEgYDVQQDDAsxOTIuMTY4 +LjEuMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMsbzsXqXpvcwgUx +mdmjI22u98ZFYJYxrhIWzzIEUvJe+t64pdq2qXxqIgVRBjpE70WOmBT+sjbV+BPE +l7nj3L0u+YPl1FYFGC5lyftg2XzS/0S500XIohkf5+dwRGxPUXPNSP8mGwpliR+t +xdGZ+EjoIcz/QMgUk5FVR7dqJ5uDTQlhIbJgBZRzrIjUHjneKyYuZPShY4bLSgLZ +Q6/xy4003sNecthlYbrI5KOdh2DPc2KzTj6zh87kM2uHMZZ9+xUFGbnScdU7bVie +LvM/MnABzSfjY/clMyKOecXv0fv6qRfgA8VTG5vg6cdsCKeohvVmUgVl7vurX07l +Iw7A1DcCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBiNeSZNC0CFlxd1lxhu0cb ++w5T2ikskgKUyxe/5ZDVFfzXaDw/JrE/9sdsnecl/t0Wbsir5lRLH0LOe0XyusuD +DcVeOEVUjVWTSgtR5Te9pjKHBphyu0fdHQG6LKNa6UGThc6aaKyjkxRv8dFIKkjl +FHceesd51pBKRdIhmFXg9kx+9iJVKgLL92U8uKgsjZBB4ZzYaUD9RLaPxaK52gOf +y6TswblJbCUxEl8vCIyB1v5rGngJ7kCjuEk7lNDSRI58GBkEzFQYLHAi7C/BOLDY +DDpJaPr9crfmD3UboAqgLdi5S8W2BN59VcLUtp4/2bgfcvYQFUahKvyTymf6OmU9 +-----END CERTIFICATE REQUEST----- diff --git a/mixly/static-server/certs/server.key b/mixly/static-server/certs/server.key new file mode 100644 index 00000000..7223f9e2 --- /dev/null +++ b/mixly/static-server/certs/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAyxvOxepem9zCBTGZ2aMjba73xkVgljGuEhbPMgRS8l763ril +2rapfGoiBVEGOkTvRY6YFP6yNtX4E8SXuePcvS75g+XUVgUYLmXJ+2DZfNL/RLnT +RciiGR/n53BEbE9Rc81I/yYbCmWJH63F0Zn4SOghzP9AyBSTkVVHt2onm4NNCWEh +smAFlHOsiNQeOd4rJi5k9KFjhstKAtlDr/HLjTTew15y2GVhusjko52HYM9zYrNO +PrOHzuQza4cxln37FQUZudJx1TttWJ4u8z8ycAHNJ+Nj9yUzIo55xe/R+/qpF+AD +xVMbm+Dpx2wIp6iG9WZSBWXu+6tfTuUjDsDUNwIDAQABAoIBABOlPevQzpPe13lv +IcV2TR/304l+/meoqIChaisZVfiRjUxrqccs8dnR3jaLbsHGFyqwLy+grxY0vgkT +c+WMD7bQy1uhqFclqQAb4lyJMqArPHumSbQvQtaRSnoNVuDvDx7XVV8wjV8FES1a +Po8WiHhs05AjhF2V9+wPxp8MCoa1EWMh+A9gkswvbaDrh+YTC3qori2H9gvt4DPp +feb1ofiMLSM2HzUtJgw+kp5SvWtYvg001f8pO7A2KIV0MoE38tXuaebxta0YHql+ +BuICWCe2M7RZau2JHZM51tPQ2/3l6ya2YSwdPPRCUQqpJ8uFS1M1QnjZKFlAEXY/ +KU4lmskCgYEA7J9EyIMqfI6pbGLeLnbf8aN/6C5ldOvEYZThlcjDpOMveajmylYs +P0SFsK05MaFf5PeM0U2WHfzx8bLyLUvIpICvJ5w9q9vR/ZOxHCBd87dMqjVDFgXm +iUz+m87D7wVNFKzZF+lnqdqOpouuBfMlJqbCygmcw4ISEwntOnDRsy0CgYEA273v +eRMTXM+VmJ4ZcTYXpRFGOFqP8yITyQ8dm7Yr+RpJAxLS/fN9DZEsl8xWNevFmbp4 +isDtKNKjmQIsvpxt2s30dpqg53YWnfHjFVN/JLYbxzYavYcciJ/Apl6RyRYukTPC +T99gUMfu+/7LHgS2ytzZ0kmVBmKaXAwUzU3lE3MCgYAYm/frYrjoe23jd+TjsDla +SEblPu4OWvbxrypHCbpPS9GENazLHms7qUS+O0XXg5EVnylmG0uhks0W9iV50Ift +k/SjifxgA1yzosioxDUBQ+8VRLTVdYekf/169uYp1cNOgyuQ8RV29OQhLiXLOJ6E +hpN7r8Q+ESkQEdg6W8FzgQKBgHKbZHvsVAvzBJ39z105jil8kfgwW6W+Xz1dEd81 +q0eXyv68Yakrxkw+LFjbrRcgagYcuGP97XN+MO9LsBSWN8GH63m0ejleYLtt/jcQ +Pl7iUCidcmLpRhuH3o2nAzgyxoTazvyjj3NyY5WwtTVp1gCGIWFJGV2kLcfWUT8m +4lQ7AoGBAM5R5jI9n8ndNhGpoIisu8YCeowS5QGDJznBN932NwenUgiZw4suv71r +v5FAdjIMKwSQcjaZoPPqqTuXnTIo1mXQpiMitAAXeb1HmJnPvZgSk5RI3OnrOZDE +CClJzjTTEprhaw9LR7uqWUp12f1Z+lAq9kQFAeYBarpqIplH6/Gi +-----END RSA PRIVATE KEY----- diff --git a/mixly/static-server/server.js b/mixly/static-server/server.js new file mode 100644 index 00000000..524be129 --- /dev/null +++ b/mixly/static-server/server.js @@ -0,0 +1,31 @@ +const fs = require('fs') +const StaticServer = require('./static-server.js'); +const SSLStaticServer = require('./static-sslserver.js'); + + +function deleteFile(filePath) { + try { + if (!fs.existsSync(filePath)) { + return; + } + const stats = fs.statSync(filePath); + if (stats.isFile()) { + fs.unlinkSync(filePath); + console.log('File deleted successfully.'); + } + } catch (err) { + console.error('Error deleting file:', err); + } +} + +const init = () => { + StaticServer.run('7000'); + SSLStaticServer.run('8000'); +} + +if (!module.parent) { + deleteFile('./nw_cache/Default/Preferences'); + init(); +} else { + module.exports = init; +} diff --git a/mixly/static-server/static-server.js b/mixly/static-server/static-server.js new file mode 100644 index 00000000..de113bee --- /dev/null +++ b/mixly/static-server/static-server.js @@ -0,0 +1,22 @@ +const http = require('http'); +const express = require('express'); +const path = require('path'); +const apiRoutes = require('./api.js'); + +const StaticServer = {}; + +StaticServer.run = (port) => { + const app = express(); + app.use(express.static(path.resolve(__dirname, '../'))); + app.use('/api/', apiRoutes); + const httpServer = http.createServer(app); + httpServer.listen(port); + console.log('Static服务器正在运行 [端口 - ' + port + ', http]...'); + console.log('访问地址:http://127.0.0.1:' + port); + StaticServer.server = httpServer; + StaticServer.app = app; + StaticServer.port = port; + StaticServer.protocol = 'http'; +} + +module.exports = StaticServer; \ No newline at end of file diff --git a/mixly/static-server/static-sslserver.js b/mixly/static-server/static-sslserver.js new file mode 100644 index 00000000..2e2fb68b --- /dev/null +++ b/mixly/static-server/static-sslserver.js @@ -0,0 +1,27 @@ +const https = require('https'); +const express = require('express'); +const fs = require('fs'); +const path = require('path'); + +const SSLStaticServer = {}; + +SSLStaticServer.run = (port) => { + const KEY_PATH = path.resolve(__dirname, './certs/server.key'); + const CRT_PATH = path.resolve(__dirname, './certs/server.crt'); + const options = { + key: fs.readFileSync(KEY_PATH), + cert: fs.readFileSync(CRT_PATH) + }; + const app = express(); + app.use(express.static(path.resolve(__dirname, '../'))); + const httpsServer = https.createServer(options, app); + httpsServer.listen(port); + console.log('Static服务器正在运行 [端口 - ' + port + ', https]...'); + console.log('访问地址:https://127.0.0.1:' + port); + SSLStaticServer.server = httpsServer; + SSLStaticServer.app = app; + SSLStaticServer.port = port; + SSLStaticServer.protocol = 'https'; +} + +module.exports = SSLStaticServer; diff --git a/mixly/tools/python/ampy/__init__.py b/mixly/tools/python/ampy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mixly/tools/python/ampy/cli.py b/mixly/tools/python/ampy/cli.py new file mode 100644 index 00000000..6dec35f7 --- /dev/null +++ b/mixly/tools/python/ampy/cli.py @@ -0,0 +1,552 @@ +# Adafruit MicroPython Tool - Command Line Interface +# Author: Tony DiCola +# Copyright (c) 2016 Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from __future__ import print_function +import os +import platform +import posixpath +import re +import serial.serialutil +import binascii +import click +import dotenv + +import sys + +# Load AMPY_PORT et al from .ampy file +# Performed here because we need to beat click's decorators. +config = dotenv.find_dotenv(filename=".ampy", usecwd=True) +if config: + dotenv.load_dotenv(dotenv_path=config) + +import ampy.files as files +import ampy.pyboard as pyboard + + +_board = None + + +def windows_full_port_name(portname): + # Helper function to generate proper Windows COM port paths. Apparently + # Windows requires COM ports above 9 to have a special path, where ports below + # 9 are just referred to by COM1, COM2, etc. (wacky!) See this post for + # more info and where this code came from: + # http://eli.thegreenplace.net/2009/07/31/listing-all-serial-ports-on-windows-with-python/ + m = re.match("^COM(\d+)$", portname) + if m and int(m.group(1)) < 10: + return portname + else: + return "\\\\.\\{0}".format(portname) + + +@click.group() +@click.option( + "--port", + "-p", + envvar="AMPY_PORT", + required=True, + type=click.STRING, + help="Name of serial port for connected board. Can optionally specify with AMPY_PORT environment variable.", + metavar="PORT", +) +@click.option( + "--baud", + "-b", + envvar="AMPY_BAUD", + default=115200, + type=click.INT, + help="Baud rate for the serial connection (default 115200). Can optionally specify with AMPY_BAUD environment variable.", + metavar="BAUD", +) +@click.option( + "--delay", + "-d", + envvar="AMPY_DELAY", + default=0, + type=click.FLOAT, + help="Delay in seconds before entering RAW MODE (default 0). Can optionally specify with AMPY_DELAY environment variable.", + metavar="DELAY", +) +@click.option( + "--reset", + "-r", + envvar="AMPY_RESET", + default="{}", + type=click.STRING, + help="default={}", + metavar="RESET", +) +@click.option( + "--empty", + "-e", + envvar="AMPY_EMPTY", + default="main.py", + type=click.STRING, + help="default=main.py", + metavar="EMPTY", +) +@click.option( + "--info", + "-i", + envvar="AMPY_INFO", + default=True, + type=click.BOOL, + help="default=True", + metavar="INFO", +) +@click.version_option() +def cli(port, baud, delay, reset="{}", empty="main.py", info=True): + """ampy - Adafruit MicroPython Tool + + Ampy is a tool to control MicroPython boards over a serial connection. Using + ampy you can manipulate files on the board's internal filesystem and even run + scripts. + """ + global _board + # On Windows fix the COM port path name for ports above 9 (see comment in + # windows_full_port_name function). + #sys.stdout.write(reset + "\n") + #sys.stdout.flush() + if platform.system() == "Windows": + port = windows_full_port_name(port) + _board = pyboard.Pyboard(port, baudrate=baud, rawdelay=delay, boardreset=reset, file_empty=empty, info=info) + + +@cli.command() +@click.argument("remote_file") +@click.argument("local_file", type=click.File("wb"), required=False) +def get(remote_file, local_file): + """ + Retrieve a file from the board. + + Get will download a file from the board and print its contents or save it + locally. You must pass at least one argument which is the path to the file + to download from the board. If you don't specify a second argument then + the file contents will be printed to standard output. However if you pass + a file name as the second argument then the contents of the downloaded file + will be saved to that file (overwriting anything inside it!). + + For example to retrieve the boot.py and print it out run: + + ampy --port /board/serial/port get boot.py + + Or to get main.py and save it as main.py locally run: + + ampy --port /board/serial/port get main.py main.py + """ + # Get the file contents. + board_files = files.Files(_board) + contents = board_files.get(remote_file) + # Print the file out if no local file was provided, otherwise save it. + if local_file is None: + contents = str(contents)[2:-1] + print(contents, end='') + else: + value = binascii.unhexlify(contents) + local_file.write(value.decode("utf-8")) + + +@cli.command() +@click.option( + "--exists-okay", is_flag=True, help="Ignore if the directory already exists." +) +@click.argument("directory") +def mkdir(directory, exists_okay): + """ + Create a directory on the board. + + Mkdir will create the specified directory on the board. One argument is + required, the full path of the directory to create. + + Note that you cannot recursively create a hierarchy of directories with one + mkdir command, instead you must create each parent directory with separate + mkdir command calls. + + For example to make a directory under the root called 'code': + + ampy --port /board/serial/port mkdir /code + """ + # Run the mkdir command. + board_files = files.Files(_board) + board_files.mkdir(directory, exists_okay=exists_okay) + + +@cli.command() +@click.argument("file") +def mkfile(file): + board_files = files.Files(_board) + board_files.mkfile(file) + + +@cli.command() +@click.argument("directory", default="/") +@click.option( + "--long_format", + "-l", + is_flag=True, + help="Print long format info including size of files. Note the size of directories is not supported and will show 0 values.", +) +@click.option( + "--recursive", + "-r", + is_flag=True, + help="recursively list all files and (empty) directories.", +) +def ls(directory, long_format, recursive): + """List contents of a directory on the board. + + Can pass an optional argument which is the path to the directory. The + default is to list the contents of the root, /, path. + + For example to list the contents of the root run: + + ampy --port /board/serial/port ls + + Or to list the contents of the /foo/bar directory on the board run: + + ampy --port /board/serial/port ls /foo/bar + + Add the -l or --long_format flag to print the size of files (however note + MicroPython does not calculate the size of folders and will show 0 bytes): + + ampy --port /board/serial/port ls -l /foo/bar + """ + # List each file/directory on a separate line. + board_files = files.Files(_board) + for f in board_files.ls(directory, long_format=long_format, recursive=recursive): + print(f) + + +@cli.command() +@click.argument("local", type=click.Path(exists=True)) +@click.argument("remote", required=False) +def put(local, remote): + """Put a file or folder and its contents on the board. + + Put will upload a local file or folder to the board. If the file already + exists on the board it will be overwritten with no warning! You must pass + at least one argument which is the path to the local file/folder to + upload. If the item to upload is a folder then it will be copied to the + board recursively with its entire child structure. You can pass a second + optional argument which is the path and name of the file/folder to put to + on the connected board. + + For example to upload a main.py from the current directory to the board's + root run: + + ampy --port /board/serial/port put main.py + + Or to upload a board_boot.py from a ./foo subdirectory and save it as boot.py + in the board's root run: + + ampy --port /board/serial/port put ./foo/board_boot.py boot.py + + To upload a local folder adafruit_library and all of its child files/folders + as an item under the board's root run: + + ampy --port /board/serial/port put adafruit_library + + Or to put a local folder adafruit_library on the board under the path + /lib/adafruit_library on the board run: + + ampy --port /board/serial/port put adafruit_library /lib/adafruit_library + """ + # Use the local filename if no remote filename is provided. + + # Check if path is a folder and do recursive copy of everything inside it. + # Otherwise it's a file and should simply be copied over. + + if os.path.isdir(local): + if remote is None: + remote = "" + # Directory copy, create the directory and walk all children to copy + # over the files. + #print("true") + #print(remote) + board_files = files.Files(_board) + board_files._pyboard.enter_raw_repl() + file_empty = board_files._pyboard.file_empty + # sys.stdout.write("Empty ./{}...\n".format(file_empty)) + # sys.stdout.flush() + # board_files.put('./{}'.format(file_empty), '', False, False) + # sys.stdout.write("Empty ./{} Done!\n".format(file_empty)) + # sys.stdout.flush() + files_info = board_files.getFilesInfo('') + files_dict = {} + files_info_len = len(files_info) + for i in range(0, files_info_len, 1): + if files_info[i][0][0] == '/': + files_info[i][0] = files_info[i][0][1 : len(files_info[i][0])] + files_dict[files_info[i][0]] = files_info[i][1] + # sys.stdout.write(str(files_dict)) + # sys.stdout.flush() + for parent, child_dirs, child_files in os.walk(local, followlinks=True): + # Create board filesystem absolute path to parent directory. + + remote_parent = posixpath.normpath( + posixpath.join(remote, os.path.relpath(parent, local)) + ) + #print(remote_parent) + ''' + try: + # Create remote parent directory. + print(remote_parent) + board_files.mkdir(remote_parent) + except files.DirectoryExistsError: + # Ignore errors for directories that already exist. + pass + # Loop through all the files and put them on the board too. + ''' + + + file_name_list = [] + data_list = [] + for filename in child_files: + file_path = os.path.join(parent, filename) + if remote_parent == '.': + remote_filename = filename + else: + remote_filename = posixpath.join(remote_parent, filename) + file_size = os.path.getsize(file_path) + board_file_size = files_dict.get(remote_filename, -1) + # sys.stdout.write('name {0} size {1} {2}\n'.format(remote_filename, board_file_size, file_size)) + # sys.stdout.flush() + if board_file_size != file_size: + with open(file_path, "rb") as infile: + file_name_list.append(remote_filename) + data_list.append(infile.read()) + #board_files.put(remote_filename, infile.read()) + else: + sys.stdout.write("Skip " + filename + "\n") + sys.stdout.flush() + board_files.putdir(file_name_list, data_list, False) + + ''' + for filename in child_files: + with open(os.path.join(parent, filename), "rb") as infile: + remote_filename = posixpath.join(remote_parent, filename) + board_files.put(remote_filename, infile.read()) + ''' + + else: + if remote is None: + remote = os.path.basename(os.path.abspath(local)) + # File copy, open the file and copy its contents to the board. + # Put the file on the board. + with open(local, "rb") as infile: + board_files = files.Files(_board) + board_files.put(remote, infile.read()) + + +@cli.command() +@click.argument("remote_file") +def rm(remote_file): + """Remove a file from the board. + + Remove the specified file from the board's filesystem. Must specify one + argument which is the path to the file to delete. Note that this can't + delete directories which have files inside them, but can delete empty + directories. + + For example to delete main.py from the root of a board run: + + ampy --port /board/serial/port rm main.py + """ + # Delete the provided file/directory on the board. + board_files = files.Files(_board) + board_files.rm(remote_file) + + +@cli.command() +@click.option( + "--missing-okay", is_flag=True, help="Ignore if the directory does not exist." +) +@click.argument("remote_folder") +def rmdir(remote_folder, missing_okay): + """Forcefully remove a folder and all its children from the board. + + Remove the specified folder from the board's filesystem. Must specify one + argument which is the path to the folder to delete. This will delete the + directory and ALL of its children recursively, use with caution! + + For example to delete everything under /adafruit_library from the root of a + board run: + + ampy --port /board/serial/port rmdir adafruit_library + """ + # Delete the provided file/directory on the board. + board_files = files.Files(_board) + board_files.rmdir(remote_folder, missing_okay=missing_okay) + +@cli.command() +@click.argument("oldname") +@click.argument("newname") +def rename(oldname, newname): + board_files = files.Files(_board) + board_files.rename(oldname, newname) + + +@cli.command() +@click.argument("local", required=True) +@click.argument("remote", required=True) +def cpdir(local, remote): + board_files = files.Files(_board) + board_files.cpdir(local, remote) + + +@cli.command() +@click.argument("local", required=True) +@click.argument("remote", required=True) +def cpfile(local, remote): + board_files = files.Files(_board) + board_files.cpfile(local, remote) + + +@cli.command() +@click.argument("local_file") +@click.option( + "--no-output", + "-n", + is_flag=True, + help="Run the code without waiting for it to finish and print output. Use this when running code with main loops that never return.", +) +def run(local_file, no_output): + """Run a script and print its output. + + Run will send the specified file to the board and execute it immediately. + Any output from the board will be printed to the console (note that this is + not a 'shell' and you can't send input to the program). + + Note that if your code has a main or infinite loop you should add the --no-output + option. This will run the script and immediately exit without waiting for + the script to finish and print output. + + For example to run a test.py script and print any output until it finishes: + + ampy --port /board/serial/port run test.py + + Or to run test.py and not wait for it to finish: + + ampy --port /board/serial/port run --no-output test.py + """ + # Run the provided file and print its output. + board_files = files.Files(_board) + try: + output = board_files.run(local_file, not no_output, not no_output) + if output is not None: + print(output.decode("utf-8"), end="") + except IOError: + click.echo( + "Failed to find or read input file: {0}".format(local_file), err=True + ) + + +@cli.command() +@click.option( + "--bootloader", "mode", flag_value="BOOTLOADER", help="Reboot into the bootloader" +) +@click.option( + "--hard", + "mode", + flag_value="NORMAL", + help="Perform a hard reboot, including running init.py", +) +@click.option( + "--repl", + "mode", + flag_value="SOFT", + default=True, + help="Perform a soft reboot, entering the REPL [default]", +) +@click.option( + "--safe", + "mode", + flag_value="SAFE_MODE", + help="Perform a safe-mode reboot. User code will not be run and the filesystem will be writeable over USB", +) +def reset(mode): + """Perform soft reset/reboot of the board. + + Will connect to the board and perform a reset. Depending on the board + and firmware, several different types of reset may be supported. + + ampy --port /board/serial/port reset + """ + _board.enter_raw_repl() + if mode == "SOFT": + _board.exit_raw_repl() + return + + _board.exec_( + """if 1: + def on_next_reset(x): + try: + import microcontroller + except: + if x == 'NORMAL': return '' + return 'Reset mode only supported on CircuitPython' + try: + microcontroller.on_next_reset(getattr(microcontroller.RunMode, x)) + except ValueError as e: + return str(e) + return '' + def reset(): + try: + import microcontroller + except: + import machine as microcontroller + microcontroller.reset() + """ + ) + r = _board.eval("on_next_reset({})".format(repr(mode))) + print("here we are", repr(r)) + if r: + click.echo(r, err=True) + return + + try: + _board.exec_raw_no_follow("reset()") + except serial.serialutil.SerialException as e: + # An error is expected to occur, as the board should disconnect from + # serial when restarted via microcontroller.reset() + pass + + +if __name__ == "__main__": + error_exit = False + try: + cli() + except BaseException as e: + if getattr(e, 'code', True): + print('Error: {}'.format(e)) + error_exit = True + finally: + # Try to ensure the board serial connection is always gracefully closed. + if _board is not None: + try: + _board.close() + except: + # Swallow errors when attempting to close as it's just a best effort + # and shouldn't cause a new error or problem if the connection can't + # be closed. + pass + if error_exit: + sys.exit(1) diff --git a/mixly/tools/python/ampy/files.py b/mixly/tools/python/ampy/files.py new file mode 100644 index 00000000..da130546 --- /dev/null +++ b/mixly/tools/python/ampy/files.py @@ -0,0 +1,594 @@ +# Adafruit MicroPython Tool - File Operations +# Author: Tony DiCola +# Copyright (c) 2016 Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import ast +import textwrap +import sys + +from ampy.pyboard import PyboardError + + +BUFFER_SIZE = 32 # Amount of data to read or write to the serial port at a time. +# This is kept small because small chips and USB to serial +# bridges usually have very small buffers. + + +class DirectoryExistsError(Exception): + pass + + +class Files(object): + """Class to interact with a MicroPython board files over a serial connection. + Provides functions for listing, uploading, and downloading files from the + board's filesystem. + """ + + def __init__(self, pyboard): + """Initialize the MicroPython board files class using the provided pyboard + instance. In most cases you should create a Pyboard instance (from + pyboard.py) which connects to a board over a serial connection and pass + it in, but you can pass in other objects for testing, etc. + """ + self._pyboard = pyboard + + def get(self, filename): + """Retrieve the contents of the specified file and return its contents + as a byte string. + """ + # Open the file and read it a few bytes at a time and print out the + # raw bytes. Be careful not to overload the UART buffer so only write + # a few bytes at a time, and don't use print since it adds newlines and + # expects string data. + command = """ + import sys + import ubinascii + with open('{0}', 'rb') as infile: + while True: + result = infile.read({1}) + if result == b'': + break + len = sys.stdout.write(ubinascii.hexlify(result)) + """.format( + filename, BUFFER_SIZE + ) + self._pyboard.enter_raw_repl() + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + # Check if this is an OSError #2, i.e. file doesn't exist and + # rethrow it as something more descriptive. + try: + if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1: + raise RuntimeError("No such file: {0}".format(filename)) + else: + raise ex + except UnicodeDecodeError: + raise ex + self._pyboard.exit_raw_repl() + return out + + def ls(self, directory="/", long_format=True, recursive=False, exit_repl=True): + """List the contents of the specified directory (or root if none is + specified). Returns a list of strings with the names of files in the + specified directory. If long_format is True then a list of 2-tuples + with the name and size (in bytes) of the item is returned. Note that + it appears the size of directories is not supported by MicroPython and + will always return 0 (i.e. no recursive size computation). + """ + + # Disabling for now, see https://github.com/adafruit/ampy/issues/55. + # # Make sure directory ends in a slash. + # if not directory.endswith("/"): + # directory += "/" + + # Make sure directory starts with slash, for consistency. + if not directory.startswith("/"): + directory = "/" + directory + + command = """\ + try: + import os + except ImportError: + import uos as os\n""" + + if recursive: + command += """\ + def listdir(directory): + result = set() + + def _listdir(dir_or_file): + try: + # if its a directory, then it should provide some children. + children = os.listdir(dir_or_file) + except OSError: + # probably a file. run stat() to confirm. + os.stat(dir_or_file) + result.add(dir_or_file) + else: + # probably a directory, add to result if empty. + if children: + # queue the children to be dealt with in next iteration. + for child in children: + # create the full path. + if dir_or_file == '/': + next = dir_or_file + child + else: + next = dir_or_file + '/' + child + + _listdir(next) + else: + result.add(dir_or_file) + + _listdir(directory) + return sorted(result)\n""" + else: + command += """\ + def check_path(path): + try: + stat = os.stat(path) + # The first element of stat contains the file type and permission information + # The mode index of the tuple returned by os.stat() is 0 + mode = stat[0] + # To determine whether it is a directory, check the directory bit in stat mode + if mode & 0o170000 == 0o040000: + if len(os.listdir(path)): + return 'dir' + else: + return 'empty dir' + # To determine whether it is a file, check the file position in stat mode + elif mode & 0o170000 == 0o100000: + return 'file' + else: + return 'special file' + except OSError: + return 'none' + + def listdir(directory): + output = [] + if directory == '/': + dirs = sorted([directory + f for f in os.listdir(directory)]) + else: + dirs = sorted([directory + '/' + f for f in os.listdir(directory)]) + + for dir in dirs: + info = check_path(dir) + if info == 'none': + continue + output.append([dir, info]) + return output\n""" + + # Execute os.listdir() command on the board. + if long_format: + command += """ + r = [] + for f in listdir('{0}'): + size = os.stat(f)[6] + r.append('{{0}} - {{1}} bytes'.format(f, size)) + print(r) + """.format( + directory + ) + else: + command += """ + print(listdir('{0}')) + """.format( + directory + ) + self._pyboard.enter_raw_repl() + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + # Check if this is an OSError #2, i.e. directory doesn't exist and + # rethrow it as something more descriptive. + if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1: + raise RuntimeError("No such directory: {0}".format(directory)) + else: + raise ex + if exit_repl: + self._pyboard.exit_raw_repl() + # Parse the result list and return it. + return ast.literal_eval(out.decode("utf-8")) + + def getFilesInfo(self, directory="/", recursive=False): + """List the contents of the specified directory (or root if none is + specified). Returns a list of strings with the names of files in the + specified directory. If long_format is True then a list of 2-tuples + with the name and size (in bytes) of the item is returned. Note that + it appears the size of directories is not supported by MicroPython and + will always return 0 (i.e. no recursive size computation). + """ + + # Disabling for now, see https://github.com/adafruit/ampy/issues/55. + # # Make sure directory ends in a slash. + # if not directory.endswith("/"): + # directory += "/" + + # Make sure directory starts with slash, for consistency. + # if not directory.startswith("/"): + # directory = "/" + directory + + command = """\ + try: + import os + except ImportError: + import uos as os\n""" + + if recursive: + command += """\ + def listdir(directory): + result = set() + + def _listdir(dir_or_file): + try: + # if its a directory, then it should provide some children. + children = os.listdir(dir_or_file) + except OSError: + # probably a file. run stat() to confirm. + os.stat(dir_or_file) + result.add(dir_or_file) + else: + # probably a directory, add to result if empty. + if children: + # queue the children to be dealt with in next iteration. + for child in children: + # create the full path. + if dir_or_file == '/': + next = dir_or_file + child + else: + next = dir_or_file + '/' + child + + _listdir(next) + else: + result.add(dir_or_file) + + _listdir(directory) + return sorted(result)\n""" + else: + command += """\ + def listdir(directory): + try: + if directory == '/': + return sorted([directory + f for f in os.listdir(directory)]) + else: + return sorted([directory + '/' + f for f in os.listdir(directory)]) + except: + return sorted([f for f in os.listdir()])\n""" + + # Execute os.listdir() command on the board. + # 当command执行出错时执行command1 + command2 = command + command2 += """ + r = [] + for f in listdir('{0}'): + try: + size = os.stat(f)[6] + except: + size = os.size(f) + r.append([f, size]) + print(r) + """.format( + (directory if directory else "/") + ) + command += """ + r = [] + for f in listdir('{0}'): + try: + size = os.stat(f)[6] + except: + size = os.size(f) + r.append([f, size]) + print(r) + """.format( + directory + ) + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + out = self._pyboard.exec_(textwrap.dedent(command2)) + # Check if this is an OSError #2, i.e. directory doesn't exist and + # rethrow it as something more descriptive. + except PyboardError as ex: + if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1: + raise RuntimeError("No such directory: {0}".format(directory)) + else: + raise ex + # Parse the result list and return it. + try: + return ast.literal_eval(out.decode("utf-8")) + except: + return '' + + def mkdir(self, directory, exists_okay=False): + """Create the specified directory. Note this cannot create a recursive + hierarchy of directories, instead each one should be created separately. + """ + # Execute os.mkdir command on the board. + command = """ + try: + import os + except ImportError: + import uos as os + os.mkdir('{0}') + """.format( + directory + ) + self._pyboard.enter_raw_repl() + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + # Check if this is an OSError #17, i.e. directory already exists. + if ex.args[2].decode("utf-8").find("OSError: [Errno 17] EEXIST") != -1: + if not exists_okay: + raise DirectoryExistsError( + "Directory already exists: {0}".format(directory) + ) + else: + raise ex + self._pyboard.exit_raw_repl() + + def mkfile(self, file, exists_okay=False): + command = """ + try: + import os + except ImportError: + import uos as os + + try: + os.stat('{0}') + except OSError: + f = open('{0}', 'w') + f.close() + """.format( + file + ) + self._pyboard.enter_raw_repl() + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + raise ex + self._pyboard.exit_raw_repl() + + def put(self, filename, data, enter_repl=True, exit_repl=True): + """Create or update the specified file with the provided data. + """ + # Open the file for writing on the board and write chunks of data. + if enter_repl: + self._pyboard.enter_raw_repl() + self._pyboard.exec_("f = open('{0}', 'wb')".format(filename)) + sys.stdout.write("Write " + filename) + size = len(data) + # Loop through and write a buffer size chunk of data at a time. + for i in range(0, size, BUFFER_SIZE): + chunk_size = min(BUFFER_SIZE, size - i) + chunk = repr(data[i : i + chunk_size]) + # Make sure to send explicit byte strings (handles python 2 compatibility). + if not chunk.startswith("b"): + chunk = "b" + chunk + self._pyboard.exec_("f.write({0})".format(chunk)) + self._pyboard.exec_("f.close()") + sys.stdout.write(" Done!\n") + if exit_repl: + self._pyboard.exit_raw_repl() + + def putdir(self, fileNameList, dataList, enter_repl=True, exit_repl=True): + """Create or update the specified file with the provided data. + """ + # Open the file for writing on the board and write chunks of data. + if enter_repl: + self._pyboard.enter_raw_repl() + for i in range(0, len(fileNameList), 1): + self._pyboard.exec_("f = open('{0}', 'wb')".format(fileNameList[i])) + sys.stdout.write("Writing " + fileNameList[i]) + sys.stdout.flush() + #print("write " + fileNameList[i]) + data = dataList[i] + size = len(data) + # Loop through and write a buffer size chunk of data at a time. + for i in range(0, size, BUFFER_SIZE): + chunk_size = min(BUFFER_SIZE, size - i) + chunk = repr(data[i : i + chunk_size]) + # Make sure to send explicit byte strings (handles python 2 compatibility). + if not chunk.startswith("b"): + chunk = "b" + chunk + self._pyboard.exec_("f.write({0})".format(chunk)) + self._pyboard.exec_("f.close()") + sys.stdout.write(" Done!\n") + sys.stdout.flush() + if exit_repl: + self._pyboard.exit_raw_repl() + + def rm(self, filename): + """Remove the specified file or directory.""" + command = """ + try: + import os + except ImportError: + import uos as os + os.remove('{0}') + """.format( + filename + ) + self._pyboard.enter_raw_repl() + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + message = ex.args[2].decode("utf-8") + # Check if this is an OSError #2, i.e. file/directory doesn't exist + # and rethrow it as something more descriptive. + if message.find("OSError: [Errno 2] ENOENT") != -1: + raise RuntimeError("No such file/directory: {0}".format(filename)) + # Check for OSError #13, the directory isn't empty. + if message.find("OSError: [Errno 13] EACCES") != -1: + raise RuntimeError("Directory is not empty: {0}".format(filename)) + else: + raise ex + self._pyboard.exit_raw_repl() + + def rmdir(self, directory, missing_okay=False): + """Forcefully remove the specified directory and all its children.""" + # Build a script to walk an entire directory structure and delete every + # file and subfolder. This is tricky because MicroPython has no os.walk + # or similar function to walk folders, so this code does it manually + # with recursion and changing directories. For each directory it lists + # the files and deletes everything it can, i.e. all the files. Then + # it lists the files again and assumes they are directories (since they + # couldn't be deleted in the first pass) and recursively clears those + # subdirectories. Finally when finished clearing all the children the + # parent directory is deleted. + command = """ + try: + import os + except ImportError: + import uos as os + + def rmdir(directory): + os.chdir(directory) + for f in os.listdir(): + try: + os.remove(f) + except OSError: + pass + for f in os.listdir(): + rmdir(f) + os.chdir('..') + os.rmdir(directory) + rmdir('{0}') + """.format( + directory + ) + self._pyboard.enter_raw_repl() + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + message = ex.args[2].decode("utf-8") + # Check if this is an OSError #2, i.e. directory doesn't exist + # and rethrow it as something more descriptive. + if message.find("OSError: [Errno 2] ENOENT") != -1: + if not missing_okay: + raise RuntimeError("No such directory: {0}".format(directory)) + else: + raise ex + self._pyboard.exit_raw_repl() + + def rename(self, oldname, newname): + command = """ + try: + import os + except ImportError: + import uos as os + + os.rename('{0}', '{1}') + """.format( + oldname, newname + ) + self._pyboard.enter_raw_repl() + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + message = ex.args[2].decode("utf-8") + raise ex + self._pyboard.exit_raw_repl() + + def cpdir(self, oldpath, newpath): + command = """ + try: + import os + except ImportError: + import uos as os + + def cpfile(src, dst): + with open(src, 'rb') as src_file: + content = src_file.read() + with open(dst, 'wb') as dst_file: + dst_file.write(content) + + def cpdir(src, dst): + try: + os.mkdir(dst) + except: + pass + for item in os.listdir(src): + src_path = src + '/' + item + dst_path = dst + '/' + item + stat = os.stat(src_path) + mode = stat[0] + if mode & 0o170000 == 0o040000: + cpdir(src_path, dst_path) + else: + cpfile(src_path, dst_path) + + cpdir('{0}', '{1}') + """.format( + oldpath, newpath + ) + self._pyboard.enter_raw_repl() + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + message = ex.args[2].decode("utf-8") + raise ex + self._pyboard.exit_raw_repl() + + def cpfile(self, oldpath, newpath): + command = """ + try: + import os + except ImportError: + import uos as os + def cpfile(src, dst): + with open(src, 'rb') as src_file: + content = src_file.read() + with open(dst, 'wb') as dst_file: + dst_file.write(content) + + cpfile('{0}', '{1}') + """.format( + oldpath, newpath + ) + self._pyboard.enter_raw_repl() + try: + out = self._pyboard.exec_(textwrap.dedent(command)) + except PyboardError as ex: + message = ex.args[2].decode("utf-8") + raise ex + self._pyboard.exit_raw_repl() + + def run(self, filename, wait_output=True, stream_output=True): + """Run the provided script and return its output. If wait_output is True + (default) then wait for the script to finish and then return its output, + otherwise just run the script and don't wait for any output. + If stream_output is True(default) then return None and print outputs to + stdout without buffering. + """ + self._pyboard.enter_raw_repl() + out = None + if stream_output: + self._pyboard.execfile(filename, stream_output=True) + elif wait_output: + # Run the file and wait for output to return. + out = self._pyboard.execfile(filename) + else: + # Read the file and run it using lower level pyboard functions that + # won't wait for it to finish or return output. + with open(filename, "rb") as infile: + self._pyboard.exec_raw_no_follow(infile.read()) + self._pyboard.exit_raw_repl() + return out diff --git a/mixly/tools/python/ampy/pyboard.py b/mixly/tools/python/ampy/pyboard.py new file mode 100644 index 00000000..a8bf3497 --- /dev/null +++ b/mixly/tools/python/ampy/pyboard.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python + +""" +pyboard interface + +This module provides the Pyboard class, used to communicate with and +control the pyboard over a serial USB connection. + +Example usage: + + import pyboard + pyb = pyboard.Pyboard('/dev/ttyACM0') + +Or: + + pyb = pyboard.Pyboard('192.168.1.1') + +Then: + + pyb.enter_raw_repl() + pyb.exec('pyb.LED(1).on()') + pyb.exit_raw_repl() + +Note: if using Python2 then pyb.exec must be written as pyb.exec_. +To run a script from the local machine on the board and print out the results: + + import pyboard + pyboard.execfile('test.py', device='/dev/ttyACM0') + +This script can also be run directly. To execute a local script, use: + + ./pyboard.py test.py + +Or: + + python pyboard.py test.py + +""" + +import sys +import time +import json + +_rawdelay = None + +try: + stdout = sys.stdout.buffer +except AttributeError: + # Python2 doesn't have buffer attr + stdout = sys.stdout + +def stdout_write_bytes(b): + b = b.replace(b"\x04", b"") + stdout.write(b) + stdout.flush() + +class PyboardError(BaseException): + pass + +class TelnetToSerial: + def __init__(self, ip, user, password, read_timeout=None): + import telnetlib + self.tn = telnetlib.Telnet(ip, timeout=15) + self.read_timeout = read_timeout + if b'Login as:' in self.tn.read_until(b'Login as:', timeout=read_timeout): + self.tn.write(bytes(user, 'ascii') + b"\r\n") + + if b'Password:' in self.tn.read_until(b'Password:', timeout=read_timeout): + # needed because of internal implementation details of the telnet server + time.sleep(0.2) + self.tn.write(bytes(password, 'ascii') + b"\r\n") + + if b'for more information.' in self.tn.read_until(b'Type "help()" for more information.', timeout=read_timeout): + # login succesful + from collections import deque + self.fifo = deque() + return + + raise PyboardError('Failed to establish a telnet connection with the board') + + def __del__(self): + self.close() + + def close(self): + try: + self.tn.close() + except: + # the telnet object might not exist yet, so ignore this one + pass + + def read(self, size=1): + while len(self.fifo) < size: + timeout_count = 0 + data = self.tn.read_eager() + if len(data): + self.fifo.extend(data) + timeout_count = 0 + else: + time.sleep(0.25) + if self.read_timeout is not None and timeout_count > 4 * self.read_timeout: + break + timeout_count += 1 + + data = b'' + while len(data) < size and len(self.fifo) > 0: + data += bytes([self.fifo.popleft()]) + return data + + def write(self, data): + self.tn.write(data) + return len(data) + + def inWaiting(self): + n_waiting = len(self.fifo) + if not n_waiting: + data = self.tn.read_eager() + self.fifo.extend(data) + return len(data) + else: + return n_waiting + +class Pyboard: + def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0, rawdelay=0, boardreset="{}", file_empty="main.py", info=True): + self.boardreset = json.loads(boardreset); + self.file_empty = file_empty; + self.info = info; + global _rawdelay + _rawdelay = rawdelay + if device and device[0].isdigit() and device[-1].isdigit() and device.count('.') == 3: + # device looks like an IP address + self.serial = TelnetToSerial(device, user, password, read_timeout=10) + else: + import serial + delayed = False + for attempt in range(wait + 1): + try: + self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=10, writeTimeout=5, timeout=40) + break + except (OSError, IOError): # Py2 and Py3 have different errors + if wait == 0: + continue + if attempt == 0: + sys.stdout.write('Waiting {} seconds for pyboard '.format(wait)) + delayed = True + time.sleep(1) + #sys.stdout.write('.') + #sys.stdout.flush() + else: + if delayed: + print('') + raise PyboardError('failed to access ' + device) + if delayed: + print('') + + def close(self): + self.serial.close() + + def read_until(self, min_num_bytes, ending, timeout=1, data_consumer=None): + data = self.serial.read(min_num_bytes) + if data_consumer: + data_consumer(data) + timeout_count = 0 + while True: + if data.endswith(ending) or data.lower().endswith(ending): + break + elif self.serial.inWaiting() > 0: + new_data = self.serial.read(1) + data = data + new_data + if data_consumer: + data_consumer(new_data) + timeout_count = 0 + else: + timeout_count += 1 + if timeout is not None and timeout_count >= 10 * timeout: + break + time.sleep(0.01) + return data + + def enter_raw_repl(self): + # Brief delay before sending RAW MODE char if requests + if _rawdelay > 0: + time.sleep(_rawdelay) + # ctrl-C twice: interrupt any running program + #sys.stdout.write("Try to delete ./{} ".format(self.file_empty)) + if self.info: + sys.stdout.write("Try to enter REPL ") + sys.stdout.flush() + repl_ok=False + oldInterCharTimeout = self.serial.interCharTimeout + oldTimeout = self.serial.timeout + self.serial.interCharTimeout = 1 + self.serial.timeout = 1 + try: + for retry in range(10): + self.serial.write(b'\r\x02\x03') + time.sleep(0.1) + self.serial.write(b'\x02\x03') + time.sleep(0.1) + data = self.read_until(1, b'>') + if self.info: + sys.stdout.write(".") + sys.stdout.flush() + if data.endswith(b'>'): + repl_ok=True + if self.info: + sys.stdout.write("\n") + sys.stdout.write("Delete {} ".format(self.file_empty)) + sys.stdout.flush() + self.serial.write(bytes("import os; os.remove('{}')\r\n".format(self.file_empty), encoding="utf8")) + time.sleep(0.1) + if self.info: + sys.stdout.write("Done!\n") + sys.stdout.flush() + break + if retry >8: + if self.info: + sys.stdout.write("\n") + sys.stdout.write('could not enter raw repl, Try to reset\n') + sys.stdout.flush() + break + finally: + self.serial.interCharTimeout = oldInterCharTimeout + self.serial.timeout = oldTimeout + if not repl_ok: + if self.info: + sys.stdout.write("Reset Start\n") + sys.stdout.flush() + resetLen = len(self.boardreset) + if resetLen: + for i in range(0, resetLen, 1): + dtr = self.boardreset[i].get("dtr", -1) + rts = self.boardreset[i].get("rts", -1) + sleep = self.boardreset[i].get("sleep", -1) + if dtr != -1 and rts != -1: + self.serial.setDTR (dtr) + self.serial.setRTS (rts) + if self.info: + sys.stdout.write("Set dtr:{}, rts:{}\n".format(dtr, rts)) + elif sleep != -1: + time.sleep(sleep / 1000) + if self.info: + sys.stdout.write("Set sleep:{}s\n".format(sleep / 1000)) + if self.info: + sys.stdout.flush() + else: + self.serial.setDTR (False) + self.serial.setRTS (False) + if self.info: + sys.stdout.write("Set dtr:{}, rts:{}\n".format(False, False)) + sys.stdout.flush() + time.sleep(0.1) + if self.info: + sys.stdout.write("Set sleep:{}s\n".format(0.1)) + sys.stdout.flush() + self.serial.setDTR (True) + self.serial.setRTS (True) + if self.info: + sys.stdout.write("Set dtr:{}, rts:{}\n".format(True, True)) + sys.stdout.flush() + time.sleep(0.1) + if self.info: + sys.stdout.write("Set sleep:{}s\n".format(0.1)) + sys.stdout.flush() + if self.info: + sys.stdout.write("Reset Done!\n") + sys.stdout.flush() + + # Brief delay before sending RAW MODE char if requests + if _rawdelay > 0: + time.sleep(_rawdelay) + # ctrl-C twice: interrupt any running program + self.serial.write(b'\r\x03') + time.sleep(0.1) + self.serial.write(b'\x03') + time.sleep(0.1) + + # flush input (without relying on serial.flushInput()) + n = self.serial.inWaiting() + while n > 0: + self.serial.read(n) + n = self.serial.inWaiting() + #time.sleep(2) + #self.serial.write(b'\x03\x04') + for retry in range(0, 5): + self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL + data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n>') + if data.endswith(b'raw REPL; CTRL-B to exit\r\n>'): + break + else: + if retry >= 4: + raise PyboardError('could not enter raw repl') + else: + self.serial.write(b'\r\x03') + time.sleep(0.1) + self.serial.write(b'\x03') + time.sleep(0.1) + self.serial.write(b'\x04') # ctrl-D: soft reset + data = self.read_until(1, b'soft reboot\r\n') + if not data.lower().endswith(b'soft reboot\r\n'): + self.serial.write(b'\x04') # ctrl-D: soft reset + raise PyboardError('could not enter raw repl') + # By splitting this into 2 reads, it allows boot.py to print stuff, + # which will show up after the soft reboot and before the raw REPL. + # Modification from original pyboard.py below: + # Add a small delay and send Ctrl-C twice after soft reboot to ensure + # any main program loop in main.py is interrupted. + time.sleep(0.5) + self.serial.write(b'\x03') + time.sleep(0.1) # (slight delay before second interrupt + self.serial.write(b'\x03') + # End modification above. + data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n') + if not data.endswith(b'raw REPL; CTRL-B to exit\r\n'): + raise PyboardError('could not enter raw repl') + + def exit_raw_repl(self): + self.serial.write(b'\r\x02') # ctrl-B: enter friendly REPL + + def follow(self, timeout, data_consumer=None): + # wait for normal output + data = self.read_until(1, b'\x04', timeout=timeout, data_consumer=data_consumer) + if not data.endswith(b'\x04'): + raise PyboardError('timeout waiting for first EOF reception') + data = data[:-1] + + # wait for error output + data_err = self.read_until(1, b'\x04', timeout=timeout) + if not data_err.endswith(b'\x04'): + raise PyboardError('timeout waiting for second EOF reception') + data_err = data_err[:-1] + + # return normal and error output + return data, data_err + + def exec_raw_no_follow(self, command): + if isinstance(command, bytes): + command_bytes = command + else: + command_bytes = bytes(command, encoding='utf8') + + # check we have a prompt + data = self.read_until(1, b'>') + if not data.endswith(b'>'): + raise PyboardError('could not enter raw repl') + + # write command + for i in range(0, len(command_bytes), 256): + self.serial.write(command_bytes[i:min(i + 256, len(command_bytes))]) + time.sleep(0.01) + self.serial.write(b'\x04') + + # check if we could exec command + data = self.serial.read(2) + if data != b'OK' and data != b'ra': + raise PyboardError('could not exec command') + + def exec_raw(self, command, timeout=10, data_consumer=None): + self.exec_raw_no_follow(command); + return self.follow(timeout, data_consumer) + + def eval(self, expression): + ret = self.exec_('print({})'.format(expression)) + ret = ret.strip() + return ret + + def exec_(self, command, stream_output=False): + data_consumer = None + if stream_output: + data_consumer = stdout_write_bytes + ret, ret_err = self.exec_raw(command, data_consumer=data_consumer) + if ret_err: + raise PyboardError('exception', ret, ret_err) + return ret + + def execfile(self, filename, stream_output=False): + with open(filename, 'rb') as f: + pyfile = f.read() + return self.exec_(pyfile, stream_output=stream_output) + + def get_time(self): + t = str(self.eval('pyb.RTC().datetime()'), encoding='utf8')[1:-1].split(', ') + return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6]) + +# in Python2 exec is a keyword so one must use "exec_" +# but for Python3 we want to provide the nicer version "exec" +setattr(Pyboard, "exec", Pyboard.exec_) + +def execfile(filename, device='/dev/ttyACM0', baudrate=115200, user='micro', password='python'): + pyb = Pyboard(device, baudrate, user, password) + pyb.enter_raw_repl() + output = pyb.execfile(filename) + stdout_write_bytes(output) + pyb.exit_raw_repl() + pyb.close() + +def main(): + import argparse + cmd_parser = argparse.ArgumentParser(description='Run scripts on the pyboard.') + cmd_parser.add_argument('--device', default='/dev/ttyACM0', help='the serial device or the IP address of the pyboard') + cmd_parser.add_argument('-b', '--baudrate', default=115200, help='the baud rate of the serial device') + cmd_parser.add_argument('-u', '--user', default='micro', help='the telnet login username') + cmd_parser.add_argument('-p', '--password', default='python', help='the telnet login password') + cmd_parser.add_argument('-c', '--command', help='program passed in as string') + cmd_parser.add_argument('-w', '--wait', default=0, type=int, help='seconds to wait for USB connected board to become available') + cmd_parser.add_argument('--follow', action='store_true', help='follow the output after running the scripts [default if no scripts given]') + cmd_parser.add_argument('files', nargs='*', help='input files') + args = cmd_parser.parse_args() + + def execbuffer(buf): + try: + pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait) + pyb.enter_raw_repl() + ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=stdout_write_bytes) + pyb.exit_raw_repl() + pyb.close() + except PyboardError as er: + print(er) + sys.exit(1) + except KeyboardInterrupt: + sys.exit(1) + if ret_err: + stdout_write_bytes(ret_err) + sys.exit(1) + + if args.command is not None: + execbuffer(args.command.encode('utf-8')) + + for filename in args.files: + with open(filename, 'rb') as f: + pyfile = f.read() + execbuffer(pyfile) + + if args.follow or (args.command is None and len(args.files) == 0): + try: + pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait) + ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes) + pyb.close() + except PyboardError as er: + print(er) + sys.exit(1) + except KeyboardInterrupt: + sys.exit(1) + if ret_err: + stdout_write_bytes(ret_err) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/mixly/tools/python/ampy_main.py b/mixly/tools/python/ampy_main.py new file mode 100644 index 00000000..899963f5 --- /dev/null +++ b/mixly/tools/python/ampy_main.py @@ -0,0 +1,24 @@ +import sys +from ampy.cli import cli, _board + + +if __name__ == "__main__": + error_exit = False + try: + cli() + except BaseException as e: + if getattr(e, 'code', True): + print('Error: {}'.format(e)) + error_exit = True + finally: + # Try to ensure the board serial connection is always gracefully closed. + if _board is not None: + try: + _board.close() + except: + # Swallow errors when attempting to close as it's just a best effort + # and shouldn't cause a new error or problem if the connection can't + # be closed. + pass + if error_exit: + sys.exit(1) \ No newline at end of file diff --git a/mixly/tools/python/backports/__init__.py b/mixly/tools/python/backports/__init__.py new file mode 100644 index 00000000..36a5b559 --- /dev/null +++ b/mixly/tools/python/backports/__init__.py @@ -0,0 +1,4 @@ +# See https://pypi.python.org/pypi/backports + +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) diff --git a/mixly/tools/python/backports/tempfile.py b/mixly/tools/python/backports/tempfile.py new file mode 100644 index 00000000..de3d79de --- /dev/null +++ b/mixly/tools/python/backports/tempfile.py @@ -0,0 +1,75 @@ +""" +Partial backport of Python 3.5's tempfile module: + + TemporaryDirectory + +Backport modifications are marked with marked with "XXX backport". +""" +from __future__ import absolute_import + +import sys +import warnings as _warnings +from shutil import rmtree as _rmtree + +from backports.weakref import finalize + + +# XXX backport: Rather than backporting all of mkdtemp(), we just create a +# thin wrapper implementing its Python 3.5 signature. +if sys.version_info < (3, 5): + from tempfile import mkdtemp as old_mkdtemp + + def mkdtemp(suffix=None, prefix=None, dir=None): + """ + Wrap `tempfile.mkdtemp()` to make the suffix and prefix optional (like Python 3.5). + """ + kwargs = {k: v for (k, v) in + dict(suffix=suffix, prefix=prefix, dir=dir).items() + if v is not None} + return old_mkdtemp(**kwargs) + +else: + from tempfile import mkdtemp + + +# XXX backport: ResourceWarning was added in Python 3.2. +# For earlier versions, fall back to RuntimeWarning instead. +_ResourceWarning = RuntimeWarning if sys.version_info < (3, 2) else ResourceWarning + + +class TemporaryDirectory(object): + """Create and return a temporary directory. This has the same + behavior as mkdtemp but can be used as a context manager. For + example: + + with TemporaryDirectory() as tmpdir: + ... + + Upon exiting the context, the directory and everything contained + in it are removed. + """ + + def __init__(self, suffix=None, prefix=None, dir=None): + self.name = mkdtemp(suffix, prefix, dir) + self._finalizer = finalize( + self, self._cleanup, self.name, + warn_message="Implicitly cleaning up {!r}".format(self)) + + @classmethod + def _cleanup(cls, name, warn_message): + _rmtree(name) + _warnings.warn(warn_message, _ResourceWarning) + + + def __repr__(self): + return "<{} {!r}>".format(self.__class__.__name__, self.name) + + def __enter__(self): + return self.name + + def __exit__(self, exc, value, tb): + self.cleanup() + + def cleanup(self): + if self._finalizer.detach(): + _rmtree(self.name) diff --git a/mixly/tools/python/backports/weakref.py b/mixly/tools/python/backports/weakref.py new file mode 100644 index 00000000..de6193bd --- /dev/null +++ b/mixly/tools/python/backports/weakref.py @@ -0,0 +1,151 @@ +""" +Partial backport of Python 3.6's weakref module: + + finalize (new in Python 3.4) + +Backport modifications are marked with "XXX backport". +""" +from __future__ import absolute_import + +import itertools +import sys +from weakref import ref + +__all__ = ['finalize'] + + +class finalize(object): + """Class for finalization of weakrefable objects + + finalize(obj, func, *args, **kwargs) returns a callable finalizer + object which will be called when obj is garbage collected. The + first time the finalizer is called it evaluates func(*arg, **kwargs) + and returns the result. After this the finalizer is dead, and + calling it just returns None. + + When the program exits any remaining finalizers for which the + atexit attribute is true will be run in reverse order of creation. + By default atexit is true. + """ + + # Finalizer objects don't have any state of their own. They are + # just used as keys to lookup _Info objects in the registry. This + # ensures that they cannot be part of a ref-cycle. + + __slots__ = () + _registry = {} + _shutdown = False + _index_iter = itertools.count() + _dirty = False + _registered_with_atexit = False + + class _Info(object): + __slots__ = ("weakref", "func", "args", "kwargs", "atexit", "index") + + def __init__(self, obj, func, *args, **kwargs): + if not self._registered_with_atexit: + # We may register the exit function more than once because + # of a thread race, but that is harmless + import atexit + atexit.register(self._exitfunc) + finalize._registered_with_atexit = True + info = self._Info() + info.weakref = ref(obj, self) + info.func = func + info.args = args + info.kwargs = kwargs or None + info.atexit = True + info.index = next(self._index_iter) + self._registry[self] = info + finalize._dirty = True + + def __call__(self, _=None): + """If alive then mark as dead and return func(*args, **kwargs); + otherwise return None""" + info = self._registry.pop(self, None) + if info and not self._shutdown: + return info.func(*info.args, **(info.kwargs or {})) + + def detach(self): + """If alive then mark as dead and return (obj, func, args, kwargs); + otherwise return None""" + info = self._registry.get(self) + obj = info and info.weakref() + if obj is not None and self._registry.pop(self, None): + return (obj, info.func, info.args, info.kwargs or {}) + + def peek(self): + """If alive then return (obj, func, args, kwargs); + otherwise return None""" + info = self._registry.get(self) + obj = info and info.weakref() + if obj is not None: + return (obj, info.func, info.args, info.kwargs or {}) + + @property + def alive(self): + """Whether finalizer is alive""" + return self in self._registry + + @property + def atexit(self): + """Whether finalizer should be called at exit""" + info = self._registry.get(self) + return bool(info) and info.atexit + + @atexit.setter + def atexit(self, value): + info = self._registry.get(self) + if info: + info.atexit = bool(value) + + def __repr__(self): + info = self._registry.get(self) + obj = info and info.weakref() + if obj is None: + return '<%s object at %#x; dead>' % (type(self).__name__, id(self)) + else: + return '<%s object at %#x; for %r at %#x>' % \ + (type(self).__name__, id(self), type(obj).__name__, id(obj)) + + @classmethod + def _select_for_exit(cls): + # Return live finalizers marked for exit, oldest first + L = [(f,i) for (f,i) in cls._registry.items() if i.atexit] + L.sort(key=lambda item:item[1].index) + return [f for (f,i) in L] + + @classmethod + def _exitfunc(cls): + # At shutdown invoke finalizers for which atexit is true. + # This is called once all other non-daemonic threads have been + # joined. + reenable_gc = False + try: + if cls._registry: + import gc + if gc.isenabled(): + reenable_gc = True + gc.disable() + pending = None + while True: + if pending is None or finalize._dirty: + pending = cls._select_for_exit() + finalize._dirty = False + if not pending: + break + f = pending.pop() + try: + # gc is disabled, so (assuming no daemonic + # threads) the following is the only line in + # this function which might trigger creation + # of a new finalizer + f() + except Exception: + sys.excepthook(*sys.exc_info()) + assert f not in cls._registry + finally: + # prevent any more finalizers from executing during shutdown + finalize._shutdown = True + if reenable_gc: + gc.enable() diff --git a/mixly/tools/python/click/__init__.py b/mixly/tools/python/click/__init__.py new file mode 100644 index 00000000..2b6008f2 --- /dev/null +++ b/mixly/tools/python/click/__init__.py @@ -0,0 +1,79 @@ +""" +Click is a simple Python module inspired by the stdlib optparse to make +writing command line scripts fun. Unlike other modules, it's based +around a simple API that does not come with too much magic and is +composable. +""" +from .core import Argument +from .core import BaseCommand +from .core import Command +from .core import CommandCollection +from .core import Context +from .core import Group +from .core import MultiCommand +from .core import Option +from .core import Parameter +from .decorators import argument +from .decorators import command +from .decorators import confirmation_option +from .decorators import group +from .decorators import help_option +from .decorators import make_pass_decorator +from .decorators import option +from .decorators import pass_context +from .decorators import pass_obj +from .decorators import password_option +from .decorators import version_option +from .exceptions import Abort +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import FileError +from .exceptions import MissingParameter +from .exceptions import NoSuchOption +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import wrap_text +from .globals import get_current_context +from .parser import OptionParser +from .termui import clear +from .termui import confirm +from .termui import echo_via_pager +from .termui import edit +from .termui import get_terminal_size +from .termui import getchar +from .termui import launch +from .termui import pause +from .termui import progressbar +from .termui import prompt +from .termui import secho +from .termui import style +from .termui import unstyle +from .types import BOOL +from .types import Choice +from .types import DateTime +from .types import File +from .types import FLOAT +from .types import FloatRange +from .types import INT +from .types import IntRange +from .types import ParamType +from .types import Path +from .types import STRING +from .types import Tuple +from .types import UNPROCESSED +from .types import UUID +from .utils import echo +from .utils import format_filename +from .utils import get_app_dir +from .utils import get_binary_stream +from .utils import get_os_args +from .utils import get_text_stream +from .utils import open_file + +# Controls if click should emit the warning about the use of unicode +# literals. +disable_unicode_literals_warning = False + +__version__ = "7.1.2" diff --git a/mixly/tools/python/click/_bashcomplete.py b/mixly/tools/python/click/_bashcomplete.py new file mode 100644 index 00000000..8bca2448 --- /dev/null +++ b/mixly/tools/python/click/_bashcomplete.py @@ -0,0 +1,375 @@ +import copy +import os +import re + +from .core import Argument +from .core import MultiCommand +from .core import Option +from .parser import split_arg_string +from .types import Choice +from .utils import echo + +try: + from collections import abc +except ImportError: + import collections as abc + +WORDBREAK = "=" + +# Note, only BASH version 4.4 and later have the nosort option. +COMPLETION_SCRIPT_BASH = """ +%(complete_func)s() { + local IFS=$'\n' + COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ + COMP_CWORD=$COMP_CWORD \\ + %(autocomplete_var)s=complete $1 ) ) + return 0 +} + +%(complete_func)setup() { + local COMPLETION_OPTIONS="" + local BASH_VERSION_ARR=(${BASH_VERSION//./ }) + # Only BASH version 4.4 and later have the nosort option. + if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \ +&& [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then + COMPLETION_OPTIONS="-o nosort" + fi + + complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s +} + +%(complete_func)setup +""" + +COMPLETION_SCRIPT_ZSH = """ +#compdef %(script_names)s + +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + (( ! $+commands[%(script_names)s] )) && return 1 + + response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\ + COMP_CWORD=$((CURRENT-1)) \\ + %(autocomplete_var)s=\"complete_zsh\" \\ + %(script_names)s )}") + + for key descr in ${(kv)response}; do + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -a completions + fi + compstate[insert]="automenu" +} + +compdef %(complete_func)s %(script_names)s +""" + +COMPLETION_SCRIPT_FISH = ( + "complete --no-files --command %(script_names)s --arguments" + ' "(env %(autocomplete_var)s=complete_fish' + " COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)" + ' %(script_names)s)"' +) + +_completion_scripts = { + "bash": COMPLETION_SCRIPT_BASH, + "zsh": COMPLETION_SCRIPT_ZSH, + "fish": COMPLETION_SCRIPT_FISH, +} + +_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]") + + +def get_completion_script(prog_name, complete_var, shell): + cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_")) + script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH) + return ( + script + % { + "complete_func": "_{}_completion".format(cf_name), + "script_names": prog_name, + "autocomplete_var": complete_var, + } + ).strip() + ";" + + +def resolve_ctx(cli, prog_name, args): + """Parse into a hierarchy of contexts. Contexts are connected + through the parent variable. + + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :return: the final context/command parsed + """ + ctx = cli.make_context(prog_name, args, resilient_parsing=True) + args = ctx.protected_args + ctx.args + while args: + if isinstance(ctx.command, MultiCommand): + if not ctx.command.chain: + cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) + if cmd is None: + return ctx + ctx = cmd.make_context( + cmd_name, args, parent=ctx, resilient_parsing=True + ) + args = ctx.protected_args + ctx.args + else: + # Walk chained subcommand contexts saving the last one. + while args: + cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) + if cmd is None: + return ctx + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True, + ) + args = sub_ctx.args + ctx = sub_ctx + args = sub_ctx.protected_args + sub_ctx.args + else: + break + return ctx + + +def start_of_option(param_str): + """ + :param param_str: param_str to check + :return: whether or not this is the start of an option declaration + (i.e. starts "-" or "--") + """ + return param_str and param_str[:1] == "-" + + +def is_incomplete_option(all_args, cmd_param): + """ + :param all_args: the full original list of args supplied + :param cmd_param: the current command paramter + :return: whether or not the last option declaration (i.e. starts + "-" or "--") is incomplete and corresponds to this cmd_param. In + other words whether this cmd_param option can still accept + values + """ + if not isinstance(cmd_param, Option): + return False + if cmd_param.is_flag: + return False + last_option = None + for index, arg_str in enumerate( + reversed([arg for arg in all_args if arg != WORDBREAK]) + ): + if index + 1 > cmd_param.nargs: + break + if start_of_option(arg_str): + last_option = arg_str + + return True if last_option and last_option in cmd_param.opts else False + + +def is_incomplete_argument(current_params, cmd_param): + """ + :param current_params: the current params and values for this + argument as already entered + :param cmd_param: the current command parameter + :return: whether or not the last argument is incomplete and + corresponds to this cmd_param. In other words whether or not the + this cmd_param argument can still accept values + """ + if not isinstance(cmd_param, Argument): + return False + current_param_values = current_params[cmd_param.name] + if current_param_values is None: + return True + if cmd_param.nargs == -1: + return True + if ( + isinstance(current_param_values, abc.Iterable) + and cmd_param.nargs > 1 + and len(current_param_values) < cmd_param.nargs + ): + return True + return False + + +def get_user_autocompletions(ctx, args, incomplete, cmd_param): + """ + :param ctx: context associated with the parsed command + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :param cmd_param: command definition + :return: all the possible user-specified completions for the param + """ + results = [] + if isinstance(cmd_param.type, Choice): + # Choices don't support descriptions. + results = [ + (c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete) + ] + elif cmd_param.autocompletion is not None: + dynamic_completions = cmd_param.autocompletion( + ctx=ctx, args=args, incomplete=incomplete + ) + results = [ + c if isinstance(c, tuple) else (c, None) for c in dynamic_completions + ] + return results + + +def get_visible_commands_starting_with(ctx, starts_with): + """ + :param ctx: context associated with the parsed command + :starts_with: string that visible commands must start with. + :return: all visible (not hidden) commands that start with starts_with. + """ + for c in ctx.command.list_commands(ctx): + if c.startswith(starts_with): + command = ctx.command.get_command(ctx, c) + if not command.hidden: + yield command + + +def add_subcommand_completions(ctx, incomplete, completions_out): + # Add subcommand completions. + if isinstance(ctx.command, MultiCommand): + completions_out.extend( + [ + (c.name, c.get_short_help_str()) + for c in get_visible_commands_starting_with(ctx, incomplete) + ] + ) + + # Walk up the context list and add any other completion + # possibilities from chained commands + while ctx.parent is not None: + ctx = ctx.parent + if isinstance(ctx.command, MultiCommand) and ctx.command.chain: + remaining_commands = [ + c + for c in get_visible_commands_starting_with(ctx, incomplete) + if c.name not in ctx.protected_args + ] + completions_out.extend( + [(c.name, c.get_short_help_str()) for c in remaining_commands] + ) + + +def get_choices(cli, prog_name, args, incomplete): + """ + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :return: all the possible completions for the incomplete + """ + all_args = copy.deepcopy(args) + + ctx = resolve_ctx(cli, prog_name, args) + if ctx is None: + return [] + + has_double_dash = "--" in all_args + + # In newer versions of bash long opts with '='s are partitioned, but + # it's easier to parse without the '=' + if start_of_option(incomplete) and WORDBREAK in incomplete: + partition_incomplete = incomplete.partition(WORDBREAK) + all_args.append(partition_incomplete[0]) + incomplete = partition_incomplete[2] + elif incomplete == WORDBREAK: + incomplete = "" + + completions = [] + if not has_double_dash and start_of_option(incomplete): + # completions for partial options + for param in ctx.command.params: + if isinstance(param, Option) and not param.hidden: + param_opts = [ + param_opt + for param_opt in param.opts + param.secondary_opts + if param_opt not in all_args or param.multiple + ] + completions.extend( + [(o, param.help) for o in param_opts if o.startswith(incomplete)] + ) + return completions + # completion for option values from user supplied values + for param in ctx.command.params: + if is_incomplete_option(all_args, param): + return get_user_autocompletions(ctx, all_args, incomplete, param) + # completion for argument values from user supplied values + for param in ctx.command.params: + if is_incomplete_argument(ctx.params, param): + return get_user_autocompletions(ctx, all_args, incomplete, param) + + add_subcommand_completions(ctx, incomplete, completions) + # Sort before returning so that proper ordering can be enforced in custom types. + return sorted(completions) + + +def do_complete(cli, prog_name, include_descriptions): + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + for item in get_choices(cli, prog_name, args, incomplete): + echo(item[0]) + if include_descriptions: + # ZSH has trouble dealing with empty array parameters when + # returned from commands, use '_' to indicate no description + # is present. + echo(item[1] if item[1] else "_") + + return True + + +def do_complete_fish(cli, prog_name): + cwords = split_arg_string(os.environ["COMP_WORDS"]) + incomplete = os.environ["COMP_CWORD"] + args = cwords[1:] + + for item in get_choices(cli, prog_name, args, incomplete): + if item[1]: + echo("{arg}\t{desc}".format(arg=item[0], desc=item[1])) + else: + echo(item[0]) + + return True + + +def bashcomplete(cli, prog_name, complete_var, complete_instr): + if "_" in complete_instr: + command, shell = complete_instr.split("_", 1) + else: + command = complete_instr + shell = "bash" + + if command == "source": + echo(get_completion_script(prog_name, complete_var, shell)) + return True + elif command == "complete": + if shell == "fish": + return do_complete_fish(cli, prog_name) + elif shell in {"bash", "zsh"}: + return do_complete(cli, prog_name, shell == "zsh") + + return False diff --git a/mixly/tools/python/click/_compat.py b/mixly/tools/python/click/_compat.py new file mode 100644 index 00000000..60cb115b --- /dev/null +++ b/mixly/tools/python/click/_compat.py @@ -0,0 +1,786 @@ +# flake8: noqa +import codecs +import io +import os +import re +import sys +from weakref import WeakKeyDictionary + +PY2 = sys.version_info[0] == 2 +CYGWIN = sys.platform.startswith("cygwin") +MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version) +# Determine local App Engine environment, per Google's own suggestion +APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.get( + "SERVER_SOFTWARE", "" +) +WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2 +DEFAULT_COLUMNS = 80 + + +_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") + + +def get_filesystem_encoding(): + return sys.getfilesystemencoding() or sys.getdefaultencoding() + + +def _make_text_stream( + stream, encoding, errors, force_readable=False, force_writable=False +): + if encoding is None: + encoding = get_best_encoding(stream) + if errors is None: + errors = "replace" + return _NonClosingTextIOWrapper( + stream, + encoding, + errors, + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def is_ascii_encoding(encoding): + """Checks if a given encoding is ascii.""" + try: + return codecs.lookup(encoding).name == "ascii" + except LookupError: + return False + + +def get_best_encoding(stream): + """Returns the default stream encoding if not found.""" + rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() + if is_ascii_encoding(rv): + return "utf-8" + return rv + + +class _NonClosingTextIOWrapper(io.TextIOWrapper): + def __init__( + self, + stream, + encoding, + errors, + force_readable=False, + force_writable=False, + **extra + ): + self._stream = stream = _FixupStream(stream, force_readable, force_writable) + io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra) + + # The io module is a place where the Python 3 text behavior + # was forced upon Python 2, so we need to unbreak + # it to look like Python 2. + if PY2: + + def write(self, x): + if isinstance(x, str) or is_bytes(x): + try: + self.flush() + except Exception: + pass + return self.buffer.write(str(x)) + return io.TextIOWrapper.write(self, x) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def __del__(self): + try: + self.detach() + except Exception: + pass + + def isatty(self): + # https://bitbucket.org/pypy/pypy/issue/1803 + return self._stream.isatty() + + +class _FixupStream(object): + """The new io interface needs more from streams than streams + traditionally implement. As such, this fix-up code is necessary in + some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). + """ + + def __init__(self, stream, force_readable=False, force_writable=False): + self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable + + def __getattr__(self, name): + return getattr(self._stream, name) + + def read1(self, size): + f = getattr(self._stream, "read1", None) + if f is not None: + return f(size) + # We only dispatch to readline instead of read in Python 2 as we + # do not want cause problems with the different implementation + # of line buffering. + if PY2: + return self._stream.readline(size) + return self._stream.read(size) + + def readable(self): + if self._force_readable: + return True + x = getattr(self._stream, "readable", None) + if x is not None: + return x() + try: + self._stream.read(0) + except Exception: + return False + return True + + def writable(self): + if self._force_writable: + return True + x = getattr(self._stream, "writable", None) + if x is not None: + return x() + try: + self._stream.write("") + except Exception: + try: + self._stream.write(b"") + except Exception: + return False + return True + + def seekable(self): + x = getattr(self._stream, "seekable", None) + if x is not None: + return x() + try: + self._stream.seek(self._stream.tell()) + except Exception: + return False + return True + + +if PY2: + text_type = unicode + raw_input = raw_input + string_types = (str, unicode) + int_types = (int, long) + iteritems = lambda x: x.iteritems() + range_type = xrange + + def is_bytes(x): + return isinstance(x, (buffer, bytearray)) + + _identifier_re = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$") + + # For Windows, we need to force stdout/stdin/stderr to binary if it's + # fetched for that. This obviously is not the most correct way to do + # it as it changes global state. Unfortunately, there does not seem to + # be a clear better way to do it as just reopening the file in binary + # mode does not change anything. + # + # An option would be to do what Python 3 does and to open the file as + # binary only, patch it back to the system, and then use a wrapper + # stream that converts newlines. It's not quite clear what's the + # correct option here. + # + # This code also lives in _winconsole for the fallback to the console + # emulation stream. + # + # There are also Windows environments where the `msvcrt` module is not + # available (which is why we use try-catch instead of the WIN variable + # here), such as the Google App Engine development server on Windows. In + # those cases there is just nothing we can do. + def set_binary_mode(f): + return f + + try: + import msvcrt + except ImportError: + pass + else: + + def set_binary_mode(f): + try: + fileno = f.fileno() + except Exception: + pass + else: + msvcrt.setmode(fileno, os.O_BINARY) + return f + + try: + import fcntl + except ImportError: + pass + else: + + def set_binary_mode(f): + try: + fileno = f.fileno() + except Exception: + pass + else: + flags = fcntl.fcntl(fileno, fcntl.F_GETFL) + fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + return f + + def isidentifier(x): + return _identifier_re.search(x) is not None + + def get_binary_stdin(): + return set_binary_mode(sys.stdin) + + def get_binary_stdout(): + _wrap_std_stream("stdout") + return set_binary_mode(sys.stdout) + + def get_binary_stderr(): + _wrap_std_stream("stderr") + return set_binary_mode(sys.stderr) + + def get_text_stdin(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _make_text_stream(sys.stdin, encoding, errors, force_readable=True) + + def get_text_stdout(encoding=None, errors=None): + _wrap_std_stream("stdout") + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _make_text_stream(sys.stdout, encoding, errors, force_writable=True) + + def get_text_stderr(encoding=None, errors=None): + _wrap_std_stream("stderr") + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _make_text_stream(sys.stderr, encoding, errors, force_writable=True) + + def filename_to_ui(value): + if isinstance(value, bytes): + value = value.decode(get_filesystem_encoding(), "replace") + return value + + +else: + import io + + text_type = str + raw_input = input + string_types = (str,) + int_types = (int,) + range_type = range + isidentifier = lambda x: x.isidentifier() + iteritems = lambda x: iter(x.items()) + + def is_bytes(x): + return isinstance(x, (bytes, memoryview, bytearray)) + + def _is_binary_reader(stream, default=False): + try: + return isinstance(stream.read(0), bytes) + except Exception: + return default + # This happens in some cases where the stream was already + # closed. In this case, we assume the default. + + def _is_binary_writer(stream, default=False): + try: + stream.write(b"") + except Exception: + try: + stream.write("") + return False + except Exception: + pass + return default + return True + + def _find_binary_reader(stream): + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_reader(stream, False): + return stream + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_reader(buf, True): + return buf + + def _find_binary_writer(stream): + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detatching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_writer(stream, False): + return stream + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_writer(buf, True): + return buf + + def _stream_is_misconfigured(stream): + """A stream is misconfigured if its encoding is ASCII.""" + # If the stream does not have an encoding set, we assume it's set + # to ASCII. This appears to happen in certain unittest + # environments. It's not quite clear what the correct behavior is + # but this at least will force Click to recover somehow. + return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") + + def _is_compat_stream_attr(stream, attr, value): + """A stream attribute is compatible if it is equal to the + desired value or the desired value is unset and the attribute + has a value. + """ + stream_value = getattr(stream, attr, None) + return stream_value == value or (value is None and stream_value is not None) + + def _is_compatible_text_stream(stream, encoding, errors): + """Check if a stream's encoding and errors attributes are + compatible with the desired values. + """ + return _is_compat_stream_attr( + stream, "encoding", encoding + ) and _is_compat_stream_attr(stream, "errors", errors) + + def _force_correct_text_stream( + text_stream, + encoding, + errors, + is_binary, + find_binary, + force_readable=False, + force_writable=False, + ): + if is_binary(text_stream, False): + binary_reader = text_stream + else: + # If the stream looks compatible, and won't default to a + # misconfigured ascii encoding, return it as-is. + if _is_compatible_text_stream(text_stream, encoding, errors) and not ( + encoding is None and _stream_is_misconfigured(text_stream) + ): + return text_stream + + # Otherwise, get the underlying binary reader. + binary_reader = find_binary(text_stream) + + # If that's not possible, silently use the original reader + # and get mojibake instead of exceptions. + if binary_reader is None: + return text_stream + + # Default errors to replace instead of strict in order to get + # something that works. + if errors is None: + errors = "replace" + + # Wrap the binary stream in a text stream with the correct + # encoding parameters. + return _make_text_stream( + binary_reader, + encoding, + errors, + force_readable=force_readable, + force_writable=force_writable, + ) + + def _force_correct_text_reader(text_reader, encoding, errors, force_readable=False): + return _force_correct_text_stream( + text_reader, + encoding, + errors, + _is_binary_reader, + _find_binary_reader, + force_readable=force_readable, + ) + + def _force_correct_text_writer(text_writer, encoding, errors, force_writable=False): + return _force_correct_text_stream( + text_writer, + encoding, + errors, + _is_binary_writer, + _find_binary_writer, + force_writable=force_writable, + ) + + def get_binary_stdin(): + reader = _find_binary_reader(sys.stdin) + if reader is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdin.") + return reader + + def get_binary_stdout(): + writer = _find_binary_writer(sys.stdout) + if writer is None: + raise RuntimeError( + "Was not able to determine binary stream for sys.stdout." + ) + return writer + + def get_binary_stderr(): + writer = _find_binary_writer(sys.stderr) + if writer is None: + raise RuntimeError( + "Was not able to determine binary stream for sys.stderr." + ) + return writer + + def get_text_stdin(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_reader( + sys.stdin, encoding, errors, force_readable=True + ) + + def get_text_stdout(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer( + sys.stdout, encoding, errors, force_writable=True + ) + + def get_text_stderr(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer( + sys.stderr, encoding, errors, force_writable=True + ) + + def filename_to_ui(value): + if isinstance(value, bytes): + value = value.decode(get_filesystem_encoding(), "replace") + else: + value = value.encode("utf-8", "surrogateescape").decode("utf-8", "replace") + return value + + +def get_streerror(e, default=None): + if hasattr(e, "strerror"): + msg = e.strerror + else: + if default is not None: + msg = default + else: + msg = str(e) + if isinstance(msg, bytes): + msg = msg.decode("utf-8", "replace") + return msg + + +def _wrap_io_open(file, mode, encoding, errors): + """On Python 2, :func:`io.open` returns a text file wrapper that + requires passing ``unicode`` to ``write``. Need to open the file in + binary mode then wrap it in a subclass that can write ``str`` and + ``unicode``. + + Also handles not passing ``encoding`` and ``errors`` in binary mode. + """ + binary = "b" in mode + + if binary: + kwargs = {} + else: + kwargs = {"encoding": encoding, "errors": errors} + + if not PY2 or binary: + return io.open(file, mode, **kwargs) + + f = io.open(file, "{}b".format(mode.replace("t", ""))) + return _make_text_stream(f, **kwargs) + + +def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False): + binary = "b" in mode + + # Standard streams first. These are simple because they don't need + # special handling for the atomic flag. It's entirely ignored. + if filename == "-": + if any(m in mode for m in ["w", "a", "x"]): + if binary: + return get_binary_stdout(), False + return get_text_stdout(encoding=encoding, errors=errors), False + if binary: + return get_binary_stdin(), False + return get_text_stdin(encoding=encoding, errors=errors), False + + # Non-atomic writes directly go out through the regular open functions. + if not atomic: + return _wrap_io_open(filename, mode, encoding, errors), True + + # Some usability stuff for atomic writes + if "a" in mode: + raise ValueError( + "Appending to an existing file is not supported, because that" + " would involve an expensive `copy`-operation to a temporary" + " file. Open the file in normal `w`-mode and copy explicitly" + " if that's what you're after." + ) + if "x" in mode: + raise ValueError("Use the `overwrite`-parameter instead.") + if "w" not in mode: + raise ValueError("Atomic writes only make sense with `w`-mode.") + + # Atomic writes are more complicated. They work by opening a file + # as a proxy in the same folder and then using the fdopen + # functionality to wrap it in a Python file. Then we wrap it in an + # atomic file that moves the file over on close. + import errno + import random + + try: + perm = os.stat(filename).st_mode + except OSError: + perm = None + + flags = os.O_RDWR | os.O_CREAT | os.O_EXCL + + if binary: + flags |= getattr(os, "O_BINARY", 0) + + while True: + tmp_filename = os.path.join( + os.path.dirname(filename), + ".__atomic-write{:08x}".format(random.randrange(1 << 32)), + ) + try: + fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) + break + except OSError as e: + if e.errno == errno.EEXIST or ( + os.name == "nt" + and e.errno == errno.EACCES + and os.path.isdir(e.filename) + and os.access(e.filename, os.W_OK) + ): + continue + raise + + if perm is not None: + os.chmod(tmp_filename, perm) # in case perm includes bits in umask + + f = _wrap_io_open(fd, mode, encoding, errors) + return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True + + +# Used in a destructor call, needs extra protection from interpreter cleanup. +if hasattr(os, "replace"): + _replace = os.replace + _can_replace = True +else: + _replace = os.rename + _can_replace = not WIN + + +class _AtomicFile(object): + def __init__(self, f, tmp_filename, real_filename): + self._f = f + self._tmp_filename = tmp_filename + self._real_filename = real_filename + self.closed = False + + @property + def name(self): + return self._real_filename + + def close(self, delete=False): + if self.closed: + return + self._f.close() + if not _can_replace: + try: + os.remove(self._real_filename) + except OSError: + pass + _replace(self._tmp_filename, self._real_filename) + self.closed = True + + def __getattr__(self, name): + return getattr(self._f, name) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + self.close(delete=exc_type is not None) + + def __repr__(self): + return repr(self._f) + + +auto_wrap_for_ansi = None +colorama = None +get_winterm_size = None + + +def strip_ansi(value): + return _ansi_re.sub("", value) + + +def _is_jupyter_kernel_output(stream): + if WIN: + # TODO: Couldn't test on Windows, should't try to support until + # someone tests the details wrt colorama. + return + + while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): + stream = stream._stream + + return stream.__class__.__module__.startswith("ipykernel.") + + +def should_strip_ansi(stream=None, color=None): + if color is None: + if stream is None: + stream = sys.stdin + return not isatty(stream) and not _is_jupyter_kernel_output(stream) + return not color + + +# If we're on Windows, we provide transparent integration through +# colorama. This will make ANSI colors through the echo function +# work automatically. +if WIN: + # Windows has a smaller terminal + DEFAULT_COLUMNS = 79 + + from ._winconsole import _get_windows_console_stream, _wrap_std_stream + + def _get_argv_encoding(): + import locale + + return locale.getpreferredencoding() + + if PY2: + + def raw_input(prompt=""): + sys.stderr.flush() + if prompt: + stdout = _default_text_stdout() + stdout.write(prompt) + stdin = _default_text_stdin() + return stdin.readline().rstrip("\r\n") + + try: + import colorama + except ImportError: + pass + else: + _ansi_stream_wrappers = WeakKeyDictionary() + + def auto_wrap_for_ansi(stream, color=None): + """This function wraps a stream so that calls through colorama + are issued to the win32 console API to recolor on demand. It + also ensures to reset the colors if a write call is interrupted + to not destroy the console afterwards. + """ + try: + cached = _ansi_stream_wrappers.get(stream) + except Exception: + cached = None + if cached is not None: + return cached + strip = should_strip_ansi(stream, color) + ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) + rv = ansi_wrapper.stream + _write = rv.write + + def _safe_write(s): + try: + return _write(s) + except: + ansi_wrapper.reset_all() + raise + + rv.write = _safe_write + try: + _ansi_stream_wrappers[stream] = rv + except Exception: + pass + return rv + + def get_winterm_size(): + win = colorama.win32.GetConsoleScreenBufferInfo( + colorama.win32.STDOUT + ).srWindow + return win.Right - win.Left, win.Bottom - win.Top + + +else: + + def _get_argv_encoding(): + return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding() + + _get_windows_console_stream = lambda *x: None + _wrap_std_stream = lambda *x: None + + +def term_len(x): + return len(strip_ansi(x)) + + +def isatty(stream): + try: + return stream.isatty() + except Exception: + return False + + +def _make_cached_stream_func(src_func, wrapper_func): + cache = WeakKeyDictionary() + + def func(): + stream = src_func() + try: + rv = cache.get(stream) + except Exception: + rv = None + if rv is not None: + return rv + rv = wrapper_func() + try: + stream = src_func() # In case wrapper_func() modified the stream + cache[stream] = rv + except Exception: + pass + return rv + + return func + + +_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin) +_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout) +_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) + + +binary_streams = { + "stdin": get_binary_stdin, + "stdout": get_binary_stdout, + "stderr": get_binary_stderr, +} + +text_streams = { + "stdin": get_text_stdin, + "stdout": get_text_stdout, + "stderr": get_text_stderr, +} diff --git a/mixly/tools/python/click/_termui_impl.py b/mixly/tools/python/click/_termui_impl.py new file mode 100644 index 00000000..88bec377 --- /dev/null +++ b/mixly/tools/python/click/_termui_impl.py @@ -0,0 +1,657 @@ +# -*- coding: utf-8 -*- +""" +This module contains implementations for the termui module. To keep the +import time of Click down, some infrequently used functionality is +placed in this module and only imported as needed. +""" +import contextlib +import math +import os +import sys +import time + +from ._compat import _default_text_stdout +from ._compat import CYGWIN +from ._compat import get_best_encoding +from ._compat import int_types +from ._compat import isatty +from ._compat import open_stream +from ._compat import range_type +from ._compat import strip_ansi +from ._compat import term_len +from ._compat import WIN +from .exceptions import ClickException +from .utils import echo + +if os.name == "nt": + BEFORE_BAR = "\r" + AFTER_BAR = "\n" +else: + BEFORE_BAR = "\r\033[?25l" + AFTER_BAR = "\033[?25h\n" + + +def _length_hint(obj): + """Returns the length hint of an object.""" + try: + return len(obj) + except (AttributeError, TypeError): + try: + get_hint = type(obj).__length_hint__ + except AttributeError: + return None + try: + hint = get_hint(obj) + except TypeError: + return None + if hint is NotImplemented or not isinstance(hint, int_types) or hint < 0: + return None + return hint + + +class ProgressBar(object): + def __init__( + self, + iterable, + length=None, + fill_char="#", + empty_char=" ", + bar_template="%(bar)s", + info_sep=" ", + show_eta=True, + show_percent=None, + show_pos=False, + item_show_func=None, + label=None, + file=None, + color=None, + width=30, + ): + self.fill_char = fill_char + self.empty_char = empty_char + self.bar_template = bar_template + self.info_sep = info_sep + self.show_eta = show_eta + self.show_percent = show_percent + self.show_pos = show_pos + self.item_show_func = item_show_func + self.label = label or "" + if file is None: + file = _default_text_stdout() + self.file = file + self.color = color + self.width = width + self.autowidth = width == 0 + + if length is None: + length = _length_hint(iterable) + if iterable is None: + if length is None: + raise TypeError("iterable or length is required") + iterable = range_type(length) + self.iter = iter(iterable) + self.length = length + self.length_known = length is not None + self.pos = 0 + self.avg = [] + self.start = self.last_eta = time.time() + self.eta_known = False + self.finished = False + self.max_width = None + self.entered = False + self.current_item = None + self.is_hidden = not isatty(self.file) + self._last_line = None + self.short_limit = 0.5 + + def __enter__(self): + self.entered = True + self.render_progress() + return self + + def __exit__(self, exc_type, exc_value, tb): + self.render_finish() + + def __iter__(self): + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + self.render_progress() + return self.generator() + + def __next__(self): + # Iteration is defined in terms of a generator function, + # returned by iter(self); use that to define next(). This works + # because `self.iter` is an iterable consumed by that generator, + # so it is re-entry safe. Calling `next(self.generator())` + # twice works and does "what you want". + return next(iter(self)) + + # Python 2 compat + next = __next__ + + def is_fast(self): + return time.time() - self.start <= self.short_limit + + def render_finish(self): + if self.is_hidden or self.is_fast(): + return + self.file.write(AFTER_BAR) + self.file.flush() + + @property + def pct(self): + if self.finished: + return 1.0 + return min(self.pos / (float(self.length) or 1), 1.0) + + @property + def time_per_iteration(self): + if not self.avg: + return 0.0 + return sum(self.avg) / float(len(self.avg)) + + @property + def eta(self): + if self.length_known and not self.finished: + return self.time_per_iteration * (self.length - self.pos) + return 0.0 + + def format_eta(self): + if self.eta_known: + t = int(self.eta) + seconds = t % 60 + t //= 60 + minutes = t % 60 + t //= 60 + hours = t % 24 + t //= 24 + if t > 0: + return "{}d {:02}:{:02}:{:02}".format(t, hours, minutes, seconds) + else: + return "{:02}:{:02}:{:02}".format(hours, minutes, seconds) + return "" + + def format_pos(self): + pos = str(self.pos) + if self.length_known: + pos += "/{}".format(self.length) + return pos + + def format_pct(self): + return "{: 4}%".format(int(self.pct * 100))[1:] + + def format_bar(self): + if self.length_known: + bar_length = int(self.pct * self.width) + bar = self.fill_char * bar_length + bar += self.empty_char * (self.width - bar_length) + elif self.finished: + bar = self.fill_char * self.width + else: + bar = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + bar[ + int( + (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5) + * self.width + ) + ] = self.fill_char + bar = "".join(bar) + return bar + + def format_progress_line(self): + show_percent = self.show_percent + + info_bits = [] + if self.length_known and show_percent is None: + show_percent = not self.show_pos + + if self.show_pos: + info_bits.append(self.format_pos()) + if show_percent: + info_bits.append(self.format_pct()) + if self.show_eta and self.eta_known and not self.finished: + info_bits.append(self.format_eta()) + if self.item_show_func is not None: + item_info = self.item_show_func(self.current_item) + if item_info is not None: + info_bits.append(item_info) + + return ( + self.bar_template + % { + "label": self.label, + "bar": self.format_bar(), + "info": self.info_sep.join(info_bits), + } + ).rstrip() + + def render_progress(self): + from .termui import get_terminal_size + + if self.is_hidden: + return + + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, get_terminal_size()[0] - clutter_length) + if new_width < old_width: + buf.append(BEFORE_BAR) + buf.append(" " * self.max_width) + self.max_width = new_width + self.width = new_width + + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width + + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(" " * (clear_width - line_len)) + line = "".join(buf) + # Render the line only if it changed. + + if line != self._last_line and not self.is_fast(): + self._last_line = line + echo(line, file=self.file, color=self.color, nl=False) + self.file.flush() + + def make_step(self, n_steps): + self.pos += n_steps + if self.length_known and self.pos >= self.length: + self.finished = True + + if (time.time() - self.last_eta) < 1.0: + return + + self.last_eta = time.time() + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] + + self.eta_known = self.length_known + + def update(self, n_steps): + self.make_step(n_steps) + self.render_progress() + + def finish(self): + self.eta_known = 0 + self.current_item = None + self.finished = True + + def generator(self): + """Return a generator which yields the items added to the bar + during construction, and updates the progress bar *after* the + yielded block returns. + """ + # WARNING: the iterator interface for `ProgressBar` relies on + # this and only works because this is a simple generator which + # doesn't create or manage additional state. If this function + # changes, the impact should be evaluated both against + # `iter(bar)` and `next(bar)`. `next()` in particular may call + # `self.generator()` repeatedly, and this must remain safe in + # order for that interface to work. + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + + if self.is_hidden: + for rv in self.iter: + yield rv + else: + for rv in self.iter: + self.current_item = rv + yield rv + self.update(1) + self.finish() + self.render_progress() + + +def pager(generator, color=None): + """Decide what method to use for paging through text.""" + stdout = _default_text_stdout() + if not isatty(sys.stdin) or not isatty(stdout): + return _nullpager(stdout, generator, color) + pager_cmd = (os.environ.get("PAGER", None) or "").strip() + if pager_cmd: + if WIN: + return _tempfilepager(generator, pager_cmd, color) + return _pipepager(generator, pager_cmd, color) + if os.environ.get("TERM") in ("dumb", "emacs"): + return _nullpager(stdout, generator, color) + if WIN or sys.platform.startswith("os2"): + return _tempfilepager(generator, "more <", color) + if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0: + return _pipepager(generator, "less", color) + + import tempfile + + fd, filename = tempfile.mkstemp() + os.close(fd) + try: + if hasattr(os, "system") and os.system('more "{}"'.format(filename)) == 0: + return _pipepager(generator, "more", color) + return _nullpager(stdout, generator, color) + finally: + os.unlink(filename) + + +def _pipepager(generator, cmd, color): + """Page through text by feeding it to another program. Invoking a + pager through this might support colors. + """ + import subprocess + + env = dict(os.environ) + + # If we're piping to less we might support colors under the + # condition that + cmd_detail = cmd.rsplit("/", 1)[-1].split() + if color is None and cmd_detail[0] == "less": + less_flags = "{}{}".format(os.environ.get("LESS", ""), " ".join(cmd_detail[1:])) + if not less_flags: + env["LESS"] = "-R" + color = True + elif "r" in less_flags or "R" in less_flags: + color = True + + c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env) + encoding = get_best_encoding(c.stdin) + try: + for text in generator: + if not color: + text = strip_ansi(text) + + c.stdin.write(text.encode(encoding, "replace")) + except (IOError, KeyboardInterrupt): + pass + else: + c.stdin.close() + + # Less doesn't respect ^C, but catches it for its own UI purposes (aborting + # search or other commands inside less). + # + # That means when the user hits ^C, the parent process (click) terminates, + # but less is still alive, paging the output and messing up the terminal. + # + # If the user wants to make the pager exit on ^C, they should set + # `LESS='-K'`. It's not our decision to make. + while True: + try: + c.wait() + except KeyboardInterrupt: + pass + else: + break + + +def _tempfilepager(generator, cmd, color): + """Page through text by invoking a program on a temporary file.""" + import tempfile + + filename = tempfile.mktemp() + # TODO: This never terminates if the passed generator never terminates. + text = "".join(generator) + if not color: + text = strip_ansi(text) + encoding = get_best_encoding(sys.stdout) + with open_stream(filename, "wb")[0] as f: + f.write(text.encode(encoding)) + try: + os.system('{} "{}"'.format(cmd, filename)) + finally: + os.unlink(filename) + + +def _nullpager(stream, generator, color): + """Simply print unformatted text. This is the ultimate fallback.""" + for text in generator: + if not color: + text = strip_ansi(text) + stream.write(text) + + +class Editor(object): + def __init__(self, editor=None, env=None, require_save=True, extension=".txt"): + self.editor = editor + self.env = env + self.require_save = require_save + self.extension = extension + + def get_editor(self): + if self.editor is not None: + return self.editor + for key in "VISUAL", "EDITOR": + rv = os.environ.get(key) + if rv: + return rv + if WIN: + return "notepad" + for editor in "sensible-editor", "vim", "nano": + if os.system("which {} >/dev/null 2>&1".format(editor)) == 0: + return editor + return "vi" + + def edit_file(self, filename): + import subprocess + + editor = self.get_editor() + if self.env: + environ = os.environ.copy() + environ.update(self.env) + else: + environ = None + try: + c = subprocess.Popen( + '{} "{}"'.format(editor, filename), env=environ, shell=True, + ) + exit_code = c.wait() + if exit_code != 0: + raise ClickException("{}: Editing failed!".format(editor)) + except OSError as e: + raise ClickException("{}: Editing failed: {}".format(editor, e)) + + def edit(self, text): + import tempfile + + text = text or "" + if text and not text.endswith("\n"): + text += "\n" + + fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension) + try: + if WIN: + encoding = "utf-8-sig" + text = text.replace("\n", "\r\n") + else: + encoding = "utf-8" + text = text.encode(encoding) + + f = os.fdopen(fd, "wb") + f.write(text) + f.close() + timestamp = os.path.getmtime(name) + + self.edit_file(name) + + if self.require_save and os.path.getmtime(name) == timestamp: + return None + + f = open(name, "rb") + try: + rv = f.read() + finally: + f.close() + return rv.decode("utf-8-sig").replace("\r\n", "\n") + finally: + os.unlink(name) + + +def open_url(url, wait=False, locate=False): + import subprocess + + def _unquote_file(url): + try: + import urllib + except ImportError: + import urllib + if url.startswith("file://"): + url = urllib.unquote(url[7:]) + return url + + if sys.platform == "darwin": + args = ["open"] + if wait: + args.append("-W") + if locate: + args.append("-R") + args.append(_unquote_file(url)) + null = open("/dev/null", "w") + try: + return subprocess.Popen(args, stderr=null).wait() + finally: + null.close() + elif WIN: + if locate: + url = _unquote_file(url) + args = 'explorer /select,"{}"'.format(_unquote_file(url.replace('"', ""))) + else: + args = 'start {} "" "{}"'.format( + "/WAIT" if wait else "", url.replace('"', "") + ) + return os.system(args) + elif CYGWIN: + if locate: + url = _unquote_file(url) + args = 'cygstart "{}"'.format(os.path.dirname(url).replace('"', "")) + else: + args = 'cygstart {} "{}"'.format("-w" if wait else "", url.replace('"', "")) + return os.system(args) + + try: + if locate: + url = os.path.dirname(_unquote_file(url)) or "." + else: + url = _unquote_file(url) + c = subprocess.Popen(["xdg-open", url]) + if wait: + return c.wait() + return 0 + except OSError: + if url.startswith(("http://", "https://")) and not locate and not wait: + import webbrowser + + webbrowser.open(url) + return 0 + return 1 + + +def _translate_ch_to_exc(ch): + if ch == u"\x03": + raise KeyboardInterrupt() + if ch == u"\x04" and not WIN: # Unix-like, Ctrl+D + raise EOFError() + if ch == u"\x1a" and WIN: # Windows, Ctrl+Z + raise EOFError() + + +if WIN: + import msvcrt + + @contextlib.contextmanager + def raw_terminal(): + yield + + def getchar(echo): + # The function `getch` will return a bytes object corresponding to + # the pressed character. Since Windows 10 build 1803, it will also + # return \x00 when called a second time after pressing a regular key. + # + # `getwch` does not share this probably-bugged behavior. Moreover, it + # returns a Unicode object by default, which is what we want. + # + # Either of these functions will return \x00 or \xe0 to indicate + # a special key, and you need to call the same function again to get + # the "rest" of the code. The fun part is that \u00e0 is + # "latin small letter a with grave", so if you type that on a French + # keyboard, you _also_ get a \xe0. + # E.g., consider the Up arrow. This returns \xe0 and then \x48. The + # resulting Unicode string reads as "a with grave" + "capital H". + # This is indistinguishable from when the user actually types + # "a with grave" and then "capital H". + # + # When \xe0 is returned, we assume it's part of a special-key sequence + # and call `getwch` again, but that means that when the user types + # the \u00e0 character, `getchar` doesn't return until a second + # character is typed. + # The alternative is returning immediately, but that would mess up + # cross-platform handling of arrow keys and others that start with + # \xe0. Another option is using `getch`, but then we can't reliably + # read non-ASCII characters, because return values of `getch` are + # limited to the current 8-bit codepage. + # + # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` + # is doing the right thing in more situations than with `getch`. + if echo: + func = msvcrt.getwche + else: + func = msvcrt.getwch + + rv = func() + if rv in (u"\x00", u"\xe0"): + # \x00 and \xe0 are control characters that indicate special key, + # see above. + rv += func() + _translate_ch_to_exc(rv) + return rv + + +else: + import tty + import termios + + @contextlib.contextmanager + def raw_terminal(): + if not isatty(sys.stdin): + f = open("/dev/tty") + fd = f.fileno() + else: + fd = sys.stdin.fileno() + f = None + try: + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + yield fd + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + sys.stdout.flush() + if f is not None: + f.close() + except termios.error: + pass + + def getchar(echo): + with raw_terminal() as fd: + ch = os.read(fd, 32) + ch = ch.decode(get_best_encoding(sys.stdin), "replace") + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + _translate_ch_to_exc(ch) + return ch diff --git a/mixly/tools/python/click/_textwrap.py b/mixly/tools/python/click/_textwrap.py new file mode 100644 index 00000000..6959087b --- /dev/null +++ b/mixly/tools/python/click/_textwrap.py @@ -0,0 +1,37 @@ +import textwrap +from contextlib import contextmanager + + +class TextWrapper(textwrap.TextWrapper): + def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): + space_left = max(width - cur_len, 1) + + if self.break_long_words: + last = reversed_chunks[-1] + cut = last[:space_left] + res = last[space_left:] + cur_line.append(cut) + reversed_chunks[-1] = res + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + @contextmanager + def extra_indent(self, indent): + old_initial_indent = self.initial_indent + old_subsequent_indent = self.subsequent_indent + self.initial_indent += indent + self.subsequent_indent += indent + try: + yield + finally: + self.initial_indent = old_initial_indent + self.subsequent_indent = old_subsequent_indent + + def indent_only(self, text): + rv = [] + for idx, line in enumerate(text.splitlines()): + indent = self.initial_indent + if idx > 0: + indent = self.subsequent_indent + rv.append(indent + line) + return "\n".join(rv) diff --git a/mixly/tools/python/click/_unicodefun.py b/mixly/tools/python/click/_unicodefun.py new file mode 100644 index 00000000..781c3652 --- /dev/null +++ b/mixly/tools/python/click/_unicodefun.py @@ -0,0 +1,131 @@ +import codecs +import os +import sys + +from ._compat import PY2 + + +def _find_unicode_literals_frame(): + import __future__ + + if not hasattr(sys, "_getframe"): # not all Python implementations have it + return 0 + frm = sys._getframe(1) + idx = 1 + while frm is not None: + if frm.f_globals.get("__name__", "").startswith("click."): + frm = frm.f_back + idx += 1 + elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag: + return idx + else: + break + return 0 + + +def _check_for_unicode_literals(): + if not __debug__: + return + + from . import disable_unicode_literals_warning + + if not PY2 or disable_unicode_literals_warning: + return + bad_frame = _find_unicode_literals_frame() + if bad_frame <= 0: + return + from warnings import warn + + warn( + Warning( + "Click detected the use of the unicode_literals __future__" + " import. This is heavily discouraged because it can" + " introduce subtle bugs in your code. You should instead" + ' use explicit u"" literals for your unicode strings. For' + " more information see" + " https://click.palletsprojects.com/python3/" + ), + stacklevel=bad_frame, + ) + + +def _verify_python3_env(): + """Ensures that the environment is good for unicode on Python 3.""" + if PY2: + return + try: + import locale + + fs_enc = codecs.lookup(locale.getpreferredencoding()).name + except Exception: + fs_enc = "ascii" + if fs_enc != "ascii": + return + + extra = "" + if os.name == "posix": + import subprocess + + try: + rv = subprocess.Popen( + ["locale", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ).communicate()[0] + except OSError: + rv = b"" + good_locales = set() + has_c_utf8 = False + + # Make sure we're operating on text here. + if isinstance(rv, bytes): + rv = rv.decode("ascii", "replace") + + for line in rv.splitlines(): + locale = line.strip() + if locale.lower().endswith((".utf-8", ".utf8")): + good_locales.add(locale) + if locale.lower() in ("c.utf8", "c.utf-8"): + has_c_utf8 = True + + extra += "\n\n" + if not good_locales: + extra += ( + "Additional information: on this system no suitable" + " UTF-8 locales were discovered. This most likely" + " requires resolving by reconfiguring the locale" + " system." + ) + elif has_c_utf8: + extra += ( + "This system supports the C.UTF-8 locale which is" + " recommended. You might be able to resolve your issue" + " by exporting the following environment variables:\n\n" + " export LC_ALL=C.UTF-8\n" + " export LANG=C.UTF-8" + ) + else: + extra += ( + "This system lists a couple of UTF-8 supporting locales" + " that you can pick from. The following suitable" + " locales were discovered: {}".format(", ".join(sorted(good_locales))) + ) + + bad_locale = None + for locale in os.environ.get("LC_ALL"), os.environ.get("LANG"): + if locale and locale.lower().endswith((".utf-8", ".utf8")): + bad_locale = locale + if locale is not None: + break + if bad_locale is not None: + extra += ( + "\n\nClick discovered that you exported a UTF-8 locale" + " but the locale system could not pick up from it" + " because it does not exist. The exported locale is" + " '{}' but it is not supported".format(bad_locale) + ) + + raise RuntimeError( + "Click will abort further execution because Python 3 was" + " configured to use ASCII as encoding for the environment." + " Consult https://click.palletsprojects.com/python3/ for" + " mitigation steps.{}".format(extra) + ) diff --git a/mixly/tools/python/click/_winconsole.py b/mixly/tools/python/click/_winconsole.py new file mode 100644 index 00000000..b6c4274a --- /dev/null +++ b/mixly/tools/python/click/_winconsole.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +# This module is based on the excellent work by Adam Bartoš who +# provided a lot of what went into the implementation here in +# the discussion to issue1602 in the Python bug tracker. +# +# There are some general differences in regards to how this works +# compared to the original patches as we do not need to patch +# the entire interpreter but just work in our little world of +# echo and prmopt. +import ctypes +import io +import os +import sys +import time +import zlib +from ctypes import byref +from ctypes import c_char +from ctypes import c_char_p +from ctypes import c_int +from ctypes import c_ssize_t +from ctypes import c_ulong +from ctypes import c_void_p +from ctypes import POINTER +from ctypes import py_object +from ctypes import windll +from ctypes import WinError +from ctypes import WINFUNCTYPE +from ctypes.wintypes import DWORD +from ctypes.wintypes import HANDLE +from ctypes.wintypes import LPCWSTR +from ctypes.wintypes import LPWSTR + +import msvcrt + +from ._compat import _NonClosingTextIOWrapper +from ._compat import PY2 +from ._compat import text_type + +try: + from ctypes import pythonapi + + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release +except ImportError: + pythonapi = None + + +c_ssize_p = POINTER(c_ssize_t) + +kernel32 = windll.kernel32 +GetStdHandle = kernel32.GetStdHandle +ReadConsoleW = kernel32.ReadConsoleW +WriteConsoleW = kernel32.WriteConsoleW +GetConsoleMode = kernel32.GetConsoleMode +GetLastError = kernel32.GetLastError +GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) +CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( + ("CommandLineToArgvW", windll.shell32) +) +LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)( + ("LocalFree", windll.kernel32) +) + + +STDIN_HANDLE = GetStdHandle(-10) +STDOUT_HANDLE = GetStdHandle(-11) +STDERR_HANDLE = GetStdHandle(-12) + + +PyBUF_SIMPLE = 0 +PyBUF_WRITABLE = 1 + +ERROR_SUCCESS = 0 +ERROR_NOT_ENOUGH_MEMORY = 8 +ERROR_OPERATION_ABORTED = 995 + +STDIN_FILENO = 0 +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + +EOF = b"\x1a" +MAX_BYTES_WRITTEN = 32767 + + +class Py_buffer(ctypes.Structure): + _fields_ = [ + ("buf", c_void_p), + ("obj", py_object), + ("len", c_ssize_t), + ("itemsize", c_ssize_t), + ("readonly", c_int), + ("ndim", c_int), + ("format", c_char_p), + ("shape", c_ssize_p), + ("strides", c_ssize_p), + ("suboffsets", c_ssize_p), + ("internal", c_void_p), + ] + + if PY2: + _fields_.insert(-1, ("smalltable", c_ssize_t * 2)) + + +# On PyPy we cannot get buffers so our ability to operate here is +# serverly limited. +if pythonapi is None: + get_buffer = None +else: + + def get_buffer(obj, writable=False): + buf = Py_buffer() + flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE + PyObject_GetBuffer(py_object(obj), byref(buf), flags) + try: + buffer_type = c_char * buf.len + return buffer_type.from_address(buf.buf) + finally: + PyBuffer_Release(byref(buf)) + + +class _WindowsConsoleRawIOBase(io.RawIOBase): + def __init__(self, handle): + self.handle = handle + + def isatty(self): + io.RawIOBase.isatty(self) + return True + + +class _WindowsConsoleReader(_WindowsConsoleRawIOBase): + def readable(self): + return True + + def readinto(self, b): + bytes_to_be_read = len(b) + if not bytes_to_be_read: + return 0 + elif bytes_to_be_read % 2: + raise ValueError( + "cannot read odd number of bytes from UTF-16-LE encoded console" + ) + + buffer = get_buffer(b, writable=True) + code_units_to_be_read = bytes_to_be_read // 2 + code_units_read = c_ulong() + + rv = ReadConsoleW( + HANDLE(self.handle), + buffer, + code_units_to_be_read, + byref(code_units_read), + None, + ) + if GetLastError() == ERROR_OPERATION_ABORTED: + # wait for KeyboardInterrupt + time.sleep(0.1) + if not rv: + raise OSError("Windows error: {}".format(GetLastError())) + + if buffer[0] == EOF: + return 0 + return 2 * code_units_read.value + + +class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): + def writable(self): + return True + + @staticmethod + def _get_error_message(errno): + if errno == ERROR_SUCCESS: + return "ERROR_SUCCESS" + elif errno == ERROR_NOT_ENOUGH_MEMORY: + return "ERROR_NOT_ENOUGH_MEMORY" + return "Windows error {}".format(errno) + + def write(self, b): + bytes_to_be_written = len(b) + buf = get_buffer(b) + code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 + code_units_written = c_ulong() + + WriteConsoleW( + HANDLE(self.handle), + buf, + code_units_to_be_written, + byref(code_units_written), + None, + ) + bytes_written = 2 * code_units_written.value + + if bytes_written == 0 and bytes_to_be_written > 0: + raise OSError(self._get_error_message(GetLastError())) + return bytes_written + + +class ConsoleStream(object): + def __init__(self, text_stream, byte_stream): + self._text_stream = text_stream + self.buffer = byte_stream + + @property + def name(self): + return self.buffer.name + + def write(self, x): + if isinstance(x, text_type): + return self._text_stream.write(x) + try: + self.flush() + except Exception: + pass + return self.buffer.write(x) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def __getattr__(self, name): + return getattr(self._text_stream, name) + + def isatty(self): + return self.buffer.isatty() + + def __repr__(self): + return "".format( + self.name, self.encoding + ) + + +class WindowsChunkedWriter(object): + """ + Wraps a stream (such as stdout), acting as a transparent proxy for all + attribute access apart from method 'write()' which we wrap to write in + limited chunks due to a Windows limitation on binary console streams. + """ + + def __init__(self, wrapped): + # double-underscore everything to prevent clashes with names of + # attributes on the wrapped stream object. + self.__wrapped = wrapped + + def __getattr__(self, name): + return getattr(self.__wrapped, name) + + def write(self, text): + total_to_write = len(text) + written = 0 + + while written < total_to_write: + to_write = min(total_to_write - written, MAX_BYTES_WRITTEN) + self.__wrapped.write(text[written : written + to_write]) + written += to_write + + +_wrapped_std_streams = set() + + +def _wrap_std_stream(name): + # Python 2 & Windows 7 and below + if ( + PY2 + and sys.getwindowsversion()[:2] <= (6, 1) + and name not in _wrapped_std_streams + ): + setattr(sys, name, WindowsChunkedWriter(getattr(sys, name))) + _wrapped_std_streams.add(name) + + +def _get_text_stdin(buffer_stream): + text_stream = _NonClosingTextIOWrapper( + io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return ConsoleStream(text_stream, buffer_stream) + + +def _get_text_stdout(buffer_stream): + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return ConsoleStream(text_stream, buffer_stream) + + +def _get_text_stderr(buffer_stream): + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return ConsoleStream(text_stream, buffer_stream) + + +if PY2: + + def _hash_py_argv(): + return zlib.crc32("\x00".join(sys.argv[1:])) + + _initial_argv_hash = _hash_py_argv() + + def _get_windows_argv(): + argc = c_int(0) + argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc)) + if not argv_unicode: + raise WinError() + try: + argv = [argv_unicode[i] for i in range(0, argc.value)] + finally: + LocalFree(argv_unicode) + del argv_unicode + + if not hasattr(sys, "frozen"): + argv = argv[1:] + while len(argv) > 0: + arg = argv[0] + if not arg.startswith("-") or arg == "-": + break + argv = argv[1:] + if arg.startswith(("-c", "-m")): + break + + return argv[1:] + + +_stream_factories = { + 0: _get_text_stdin, + 1: _get_text_stdout, + 2: _get_text_stderr, +} + + +def _is_console(f): + if not hasattr(f, "fileno"): + return False + + try: + fileno = f.fileno() + except OSError: + return False + + handle = msvcrt.get_osfhandle(fileno) + return bool(GetConsoleMode(handle, byref(DWORD()))) + + +def _get_windows_console_stream(f, encoding, errors): + if ( + get_buffer is not None + and encoding in ("utf-16-le", None) + and errors in ("strict", None) + and _is_console(f) + ): + func = _stream_factories.get(f.fileno()) + if func is not None: + if not PY2: + f = getattr(f, "buffer", None) + if f is None: + return None + else: + # If we are on Python 2 we need to set the stream that we + # deal with to binary mode as otherwise the exercise if a + # bit moot. The same problems apply as for + # get_binary_stdin and friends from _compat. + msvcrt.setmode(f.fileno(), os.O_BINARY) + return func(f) diff --git a/mixly/tools/python/click/core.py b/mixly/tools/python/click/core.py new file mode 100644 index 00000000..f58bf26d --- /dev/null +++ b/mixly/tools/python/click/core.py @@ -0,0 +1,2030 @@ +import errno +import inspect +import os +import sys +from contextlib import contextmanager +from functools import update_wrapper +from itertools import repeat + +from ._compat import isidentifier +from ._compat import iteritems +from ._compat import PY2 +from ._compat import string_types +from ._unicodefun import _check_for_unicode_literals +from ._unicodefun import _verify_python3_env +from .exceptions import Abort +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import Exit +from .exceptions import MissingParameter +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import join_options +from .globals import pop_context +from .globals import push_context +from .parser import OptionParser +from .parser import split_opt +from .termui import confirm +from .termui import prompt +from .termui import style +from .types import BOOL +from .types import convert_type +from .types import IntRange +from .utils import echo +from .utils import get_os_args +from .utils import make_default_short_help +from .utils import make_str +from .utils import PacifyFlushWrapper + +_missing = object() + +SUBCOMMAND_METAVAR = "COMMAND [ARGS]..." +SUBCOMMANDS_METAVAR = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." + +DEPRECATED_HELP_NOTICE = " (DEPRECATED)" +DEPRECATED_INVOKE_NOTICE = "DeprecationWarning: The command %(name)s is deprecated." + + +def _maybe_show_deprecated_notice(cmd): + if cmd.deprecated: + echo(style(DEPRECATED_INVOKE_NOTICE % {"name": cmd.name}, fg="red"), err=True) + + +def fast_exit(code): + """Exit without garbage collection, this speeds up exit by about 10ms for + things like bash completion. + """ + sys.stdout.flush() + sys.stderr.flush() + os._exit(code) + + +def _bashcomplete(cmd, prog_name, complete_var=None): + """Internal handler for the bash completion support.""" + if complete_var is None: + complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper()) + complete_instr = os.environ.get(complete_var) + if not complete_instr: + return + + from ._bashcomplete import bashcomplete + + if bashcomplete(cmd, prog_name, complete_var, complete_instr): + fast_exit(1) + + +def _check_multicommand(base_command, cmd_name, cmd, register=False): + if not base_command.chain or not isinstance(cmd, MultiCommand): + return + if register: + hint = ( + "It is not possible to add multi commands as children to" + " another multi command that is in chain mode." + ) + else: + hint = ( + "Found a multi command as subcommand to a multi command" + " that is in chain mode. This is not supported." + ) + raise RuntimeError( + "{}. Command '{}' is set to chain and '{}' was added as" + " subcommand but it in itself is a multi command. ('{}' is a {}" + " within a chained {} named '{}').".format( + hint, + base_command.name, + cmd_name, + cmd_name, + cmd.__class__.__name__, + base_command.__class__.__name__, + base_command.name, + ) + ) + + +def batch(iterable, batch_size): + return list(zip(*repeat(iter(iterable), batch_size))) + + +def invoke_param_callback(callback, ctx, param, value): + code = getattr(callback, "__code__", None) + args = getattr(code, "co_argcount", 3) + + if args < 3: + from warnings import warn + + warn( + "Parameter callbacks take 3 args, (ctx, param, value). The" + " 2-arg style is deprecated and will be removed in 8.0.".format(callback), + DeprecationWarning, + stacklevel=3, + ) + return callback(ctx, value) + + return callback(ctx, param, value) + + +@contextmanager +def augment_usage_errors(ctx, param=None): + """Context manager that attaches extra information to exceptions.""" + try: + yield + except BadParameter as e: + if e.ctx is None: + e.ctx = ctx + if param is not None and e.param is None: + e.param = param + raise + except UsageError as e: + if e.ctx is None: + e.ctx = ctx + raise + + +def iter_params_for_processing(invocation_order, declaration_order): + """Given a sequence of parameters in the order as should be considered + for processing and an iterable of parameters that exist, this returns + a list in the correct order as they should be processed. + """ + + def sort_key(item): + try: + idx = invocation_order.index(item) + except ValueError: + idx = float("inf") + return (not item.is_eager, idx) + + return sorted(declaration_order, key=sort_key) + + +class Context(object): + """The context is a special internal object that holds state relevant + for the script execution at every single level. It's normally invisible + to commands unless they opt-in to getting access to it. + + The context is useful as it can pass internal objects around and can + control special execution features such as reading data from + environment variables. + + A context can be used as context manager in which case it will call + :meth:`close` on teardown. + + .. versionadded:: 2.0 + Added the `resilient_parsing`, `help_option_names`, + `token_normalize_func` parameters. + + .. versionadded:: 3.0 + Added the `allow_extra_args` and `allow_interspersed_args` + parameters. + + .. versionadded:: 4.0 + Added the `color`, `ignore_unknown_options`, and + `max_content_width` parameters. + + .. versionadded:: 7.1 + Added the `show_default` parameter. + + :param command: the command class for this context. + :param parent: the parent context. + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it is usually + the name of the script, for commands below it it's + the name of the script. + :param obj: an arbitrary object of user data. + :param auto_envvar_prefix: the prefix to use for automatic environment + variables. If this is `None` then reading + from environment variables is disabled. This + does not affect manually set environment + variables which are always read. + :param default_map: a dictionary (like object) with default values + for parameters. + :param terminal_width: the width of the terminal. The default is + inherit from parent context. If no context + defines the terminal width then auto + detection will be applied. + :param max_content_width: the maximum width for content rendered by + Click (this currently only affects help + pages). This defaults to 80 characters if + not overridden. In other words: even if the + terminal is larger than that, Click will not + format things wider than 80 characters by + default. In addition to that, formatters might + add some safety mapping on the right. + :param resilient_parsing: if this flag is enabled then Click will + parse without any interactivity or callback + invocation. Default values will also be + ignored. This is useful for implementing + things such as completion support. + :param allow_extra_args: if this is set to `True` then extra arguments + at the end will not raise an error and will be + kept on the context. The default is to inherit + from the command. + :param allow_interspersed_args: if this is set to `False` then options + and arguments cannot be mixed. The + default is to inherit from the command. + :param ignore_unknown_options: instructs click to ignore options it does + not know and keeps them for later + processing. + :param help_option_names: optionally a list of strings that define how + the default help parameter is named. The + default is ``['--help']``. + :param token_normalize_func: an optional function that is used to + normalize tokens (options, choices, + etc.). This for instance can be used to + implement case insensitive behavior. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are used in texts that Click prints which is by + default not the case. This for instance would affect + help output. + :param show_default: if True, shows defaults for all options. + Even if an option is later created with show_default=False, + this command-level setting overrides it. + """ + + def __init__( + self, + command, + parent=None, + info_name=None, + obj=None, + auto_envvar_prefix=None, + default_map=None, + terminal_width=None, + max_content_width=None, + resilient_parsing=False, + allow_extra_args=None, + allow_interspersed_args=None, + ignore_unknown_options=None, + help_option_names=None, + token_normalize_func=None, + color=None, + show_default=None, + ): + #: the parent context or `None` if none exists. + self.parent = parent + #: the :class:`Command` for this context. + self.command = command + #: the descriptive information name + self.info_name = info_name + #: the parsed parameters except if the value is hidden in which + #: case it's not remembered. + self.params = {} + #: the leftover arguments. + self.args = [] + #: protected arguments. These are arguments that are prepended + #: to `args` when certain parsing scenarios are encountered but + #: must be never propagated to another arguments. This is used + #: to implement nested parsing. + self.protected_args = [] + if obj is None and parent is not None: + obj = parent.obj + #: the user object stored. + self.obj = obj + self._meta = getattr(parent, "meta", {}) + + #: A dictionary (-like object) with defaults for parameters. + if ( + default_map is None + and parent is not None + and parent.default_map is not None + ): + default_map = parent.default_map.get(info_name) + self.default_map = default_map + + #: This flag indicates if a subcommand is going to be executed. A + #: group callback can use this information to figure out if it's + #: being executed directly or because the execution flow passes + #: onwards to a subcommand. By default it's None, but it can be + #: the name of the subcommand to execute. + #: + #: If chaining is enabled this will be set to ``'*'`` in case + #: any commands are executed. It is however not possible to + #: figure out which ones. If you require this knowledge you + #: should use a :func:`resultcallback`. + self.invoked_subcommand = None + + if terminal_width is None and parent is not None: + terminal_width = parent.terminal_width + #: The width of the terminal (None is autodetection). + self.terminal_width = terminal_width + + if max_content_width is None and parent is not None: + max_content_width = parent.max_content_width + #: The maximum width of formatted content (None implies a sensible + #: default which is 80 for most things). + self.max_content_width = max_content_width + + if allow_extra_args is None: + allow_extra_args = command.allow_extra_args + #: Indicates if the context allows extra args or if it should + #: fail on parsing. + #: + #: .. versionadded:: 3.0 + self.allow_extra_args = allow_extra_args + + if allow_interspersed_args is None: + allow_interspersed_args = command.allow_interspersed_args + #: Indicates if the context allows mixing of arguments and + #: options or not. + #: + #: .. versionadded:: 3.0 + self.allow_interspersed_args = allow_interspersed_args + + if ignore_unknown_options is None: + ignore_unknown_options = command.ignore_unknown_options + #: Instructs click to ignore options that a command does not + #: understand and will store it on the context for later + #: processing. This is primarily useful for situations where you + #: want to call into external programs. Generally this pattern is + #: strongly discouraged because it's not possibly to losslessly + #: forward all arguments. + #: + #: .. versionadded:: 4.0 + self.ignore_unknown_options = ignore_unknown_options + + if help_option_names is None: + if parent is not None: + help_option_names = parent.help_option_names + else: + help_option_names = ["--help"] + + #: The names for the help options. + self.help_option_names = help_option_names + + if token_normalize_func is None and parent is not None: + token_normalize_func = parent.token_normalize_func + + #: An optional normalization function for tokens. This is + #: options, choices, commands etc. + self.token_normalize_func = token_normalize_func + + #: Indicates if resilient parsing is enabled. In that case Click + #: will do its best to not cause any failures and default values + #: will be ignored. Useful for completion. + self.resilient_parsing = resilient_parsing + + # If there is no envvar prefix yet, but the parent has one and + # the command on this level has a name, we can expand the envvar + # prefix automatically. + if auto_envvar_prefix is None: + if ( + parent is not None + and parent.auto_envvar_prefix is not None + and self.info_name is not None + ): + auto_envvar_prefix = "{}_{}".format( + parent.auto_envvar_prefix, self.info_name.upper() + ) + else: + auto_envvar_prefix = auto_envvar_prefix.upper() + if auto_envvar_prefix is not None: + auto_envvar_prefix = auto_envvar_prefix.replace("-", "_") + self.auto_envvar_prefix = auto_envvar_prefix + + if color is None and parent is not None: + color = parent.color + + #: Controls if styling output is wanted or not. + self.color = color + + self.show_default = show_default + + self._close_callbacks = [] + self._depth = 0 + + def __enter__(self): + self._depth += 1 + push_context(self) + return self + + def __exit__(self, exc_type, exc_value, tb): + self._depth -= 1 + if self._depth == 0: + self.close() + pop_context() + + @contextmanager + def scope(self, cleanup=True): + """This helper method can be used with the context object to promote + it to the current thread local (see :func:`get_current_context`). + The default behavior of this is to invoke the cleanup functions which + can be disabled by setting `cleanup` to `False`. The cleanup + functions are typically used for things such as closing file handles. + + If the cleanup is intended the context object can also be directly + used as a context manager. + + Example usage:: + + with ctx.scope(): + assert get_current_context() is ctx + + This is equivalent:: + + with ctx: + assert get_current_context() is ctx + + .. versionadded:: 5.0 + + :param cleanup: controls if the cleanup functions should be run or + not. The default is to run these functions. In + some situations the context only wants to be + temporarily pushed in which case this can be disabled. + Nested pushes automatically defer the cleanup. + """ + if not cleanup: + self._depth += 1 + try: + with self as rv: + yield rv + finally: + if not cleanup: + self._depth -= 1 + + @property + def meta(self): + """This is a dictionary which is shared with all the contexts + that are nested. It exists so that click utilities can store some + state here if they need to. It is however the responsibility of + that code to manage this dictionary well. + + The keys are supposed to be unique dotted strings. For instance + module paths are a good choice for it. What is stored in there is + irrelevant for the operation of click. However what is important is + that code that places data here adheres to the general semantics of + the system. + + Example usage:: + + LANG_KEY = f'{__name__}.lang' + + def set_language(value): + ctx = get_current_context() + ctx.meta[LANG_KEY] = value + + def get_language(): + return get_current_context().meta.get(LANG_KEY, 'en_US') + + .. versionadded:: 5.0 + """ + return self._meta + + def make_formatter(self): + """Creates the formatter for the help and usage output.""" + return HelpFormatter( + width=self.terminal_width, max_width=self.max_content_width + ) + + def call_on_close(self, f): + """This decorator remembers a function as callback that should be + executed when the context tears down. This is most useful to bind + resource handling to the script execution. For instance, file objects + opened by the :class:`File` type will register their close callbacks + here. + + :param f: the function to execute on teardown. + """ + self._close_callbacks.append(f) + return f + + def close(self): + """Invokes all close callbacks.""" + for cb in self._close_callbacks: + cb() + self._close_callbacks = [] + + @property + def command_path(self): + """The computed command path. This is used for the ``usage`` + information on the help page. It's automatically created by + combining the info names of the chain of contexts to the root. + """ + rv = "" + if self.info_name is not None: + rv = self.info_name + if self.parent is not None: + rv = "{} {}".format(self.parent.command_path, rv) + return rv.lstrip() + + def find_root(self): + """Finds the outermost context.""" + node = self + while node.parent is not None: + node = node.parent + return node + + def find_object(self, object_type): + """Finds the closest object of a given type.""" + node = self + while node is not None: + if isinstance(node.obj, object_type): + return node.obj + node = node.parent + + def ensure_object(self, object_type): + """Like :meth:`find_object` but sets the innermost object to a + new instance of `object_type` if it does not exist. + """ + rv = self.find_object(object_type) + if rv is None: + self.obj = rv = object_type() + return rv + + def lookup_default(self, name): + """Looks up the default for a parameter name. This by default + looks into the :attr:`default_map` if available. + """ + if self.default_map is not None: + rv = self.default_map.get(name) + if callable(rv): + rv = rv() + return rv + + def fail(self, message): + """Aborts the execution of the program with a specific error + message. + + :param message: the error message to fail with. + """ + raise UsageError(message, self) + + def abort(self): + """Aborts the script.""" + raise Abort() + + def exit(self, code=0): + """Exits the application with a given exit code.""" + raise Exit(code) + + def get_usage(self): + """Helper method to get formatted usage string for the current + context and command. + """ + return self.command.get_usage(self) + + def get_help(self): + """Helper method to get formatted help page for the current + context and command. + """ + return self.command.get_help(self) + + def invoke(*args, **kwargs): # noqa: B902 + """Invokes a command callback in exactly the way it expects. There + are two ways to invoke this method: + + 1. the first argument can be a callback and all other arguments and + keyword arguments are forwarded directly to the function. + 2. the first argument is a click command object. In that case all + arguments are forwarded as well but proper click parameters + (options and click arguments) must be keyword arguments and Click + will fill in defaults. + + Note that before Click 3.2 keyword arguments were not properly filled + in against the intention of this code and no context was created. For + more information about this change and why it was done in a bugfix + release see :ref:`upgrade-to-3.2`. + """ + self, callback = args[:2] + ctx = self + + # It's also possible to invoke another command which might or + # might not have a callback. In that case we also fill + # in defaults and make a new context for this command. + if isinstance(callback, Command): + other_cmd = callback + callback = other_cmd.callback + ctx = Context(other_cmd, info_name=other_cmd.name, parent=self) + if callback is None: + raise TypeError( + "The given command does not have a callback that can be invoked." + ) + + for param in other_cmd.params: + if param.name not in kwargs and param.expose_value: + kwargs[param.name] = param.get_default(ctx) + + args = args[2:] + with augment_usage_errors(self): + with ctx: + return callback(*args, **kwargs) + + def forward(*args, **kwargs): # noqa: B902 + """Similar to :meth:`invoke` but fills in default keyword + arguments from the current context if the other command expects + it. This cannot invoke callbacks directly, only other commands. + """ + self, cmd = args[:2] + + # It's also possible to invoke another command which might or + # might not have a callback. + if not isinstance(cmd, Command): + raise TypeError("Callback is not a command.") + + for param in self.params: + if param not in kwargs: + kwargs[param] = self.params[param] + + return self.invoke(cmd, **kwargs) + + +class BaseCommand(object): + """The base command implements the minimal API contract of commands. + Most code will never use this as it does not implement a lot of useful + functionality but it can act as the direct subclass of alternative + parsing methods that do not depend on the Click parser. + + For instance, this can be used to bridge Click and other systems like + argparse or docopt. + + Because base commands do not implement a lot of the API that other + parts of Click take for granted, they are not supported for all + operations. For instance, they cannot be used with the decorators + usually and they have no built-in callback system. + + .. versionchanged:: 2.0 + Added the `context_settings` parameter. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + """ + + #: the default for the :attr:`Context.allow_extra_args` flag. + allow_extra_args = False + #: the default for the :attr:`Context.allow_interspersed_args` flag. + allow_interspersed_args = True + #: the default for the :attr:`Context.ignore_unknown_options` flag. + ignore_unknown_options = False + + def __init__(self, name, context_settings=None): + #: the name the command thinks it has. Upon registering a command + #: on a :class:`Group` the group will default the command name + #: with this information. You should instead use the + #: :class:`Context`\'s :attr:`~Context.info_name` attribute. + self.name = name + if context_settings is None: + context_settings = {} + #: an optional dictionary with defaults passed to the context. + self.context_settings = context_settings + + def __repr__(self): + return "<{} {}>".format(self.__class__.__name__, self.name) + + def get_usage(self, ctx): + raise NotImplementedError("Base commands cannot get usage") + + def get_help(self, ctx): + raise NotImplementedError("Base commands cannot get help") + + def make_context(self, info_name, args, parent=None, **extra): + """This function when given an info name and arguments will kick + off the parsing and create a new :class:`Context`. It does not + invoke the actual command callback though. + + :param info_name: the info name for this invokation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it's usually + the name of the script, for commands below it it's + the name of the script. + :param args: the arguments to parse as list of strings. + :param parent: the parent context if available. + :param extra: extra keyword arguments forwarded to the context + constructor. + """ + for key, value in iteritems(self.context_settings): + if key not in extra: + extra[key] = value + ctx = Context(self, info_name=info_name, parent=parent, **extra) + with ctx.scope(cleanup=False): + self.parse_args(ctx, args) + return ctx + + def parse_args(self, ctx, args): + """Given a context and a list of arguments this creates the parser + and parses the arguments, then modifies the context as necessary. + This is automatically invoked by :meth:`make_context`. + """ + raise NotImplementedError("Base commands do not know how to parse arguments.") + + def invoke(self, ctx): + """Given a context, this invokes the command. The default + implementation is raising a not implemented error. + """ + raise NotImplementedError("Base commands are not invokable by default") + + def main( + self, + args=None, + prog_name=None, + complete_var=None, + standalone_mode=True, + **extra + ): + """This is the way to invoke a script with all the bells and + whistles as a command line application. This will always terminate + the application after a call. If this is not wanted, ``SystemExit`` + needs to be caught. + + This method is also available by directly calling the instance of + a :class:`Command`. + + .. versionadded:: 3.0 + Added the `standalone_mode` flag to control the standalone mode. + + :param args: the arguments that should be used for parsing. If not + provided, ``sys.argv[1:]`` is used. + :param prog_name: the program name that should be used. By default + the program name is constructed by taking the file + name from ``sys.argv[0]``. + :param complete_var: the environment variable that controls the + bash completion support. The default is + ``"__COMPLETE"`` with prog_name in + uppercase. + :param standalone_mode: the default behavior is to invoke the script + in standalone mode. Click will then + handle exceptions and convert them into + error messages and the function will never + return but shut down the interpreter. If + this is set to `False` they will be + propagated to the caller and the return + value of this function is the return value + of :meth:`invoke`. + :param extra: extra keyword arguments are forwarded to the context + constructor. See :class:`Context` for more information. + """ + # If we are in Python 3, we will verify that the environment is + # sane at this point or reject further execution to avoid a + # broken script. + if not PY2: + _verify_python3_env() + else: + _check_for_unicode_literals() + + if args is None: + args = get_os_args() + else: + args = list(args) + + if prog_name is None: + prog_name = make_str( + os.path.basename(sys.argv[0] if sys.argv else __file__) + ) + + # Hook for the Bash completion. This only activates if the Bash + # completion is actually enabled, otherwise this is quite a fast + # noop. + _bashcomplete(self, prog_name, complete_var) + + try: + try: + with self.make_context(prog_name, args, **extra) as ctx: + rv = self.invoke(ctx) + if not standalone_mode: + return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness + ctx.exit() + except (EOFError, KeyboardInterrupt): + echo(file=sys.stderr) + raise Abort() + except ClickException as e: + if not standalone_mode: + raise + e.show() + sys.exit(e.exit_code) + except IOError as e: + if e.errno == errno.EPIPE: + sys.stdout = PacifyFlushWrapper(sys.stdout) + sys.stderr = PacifyFlushWrapper(sys.stderr) + sys.exit(1) + else: + raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code + except Abort: + if not standalone_mode: + raise + echo("Aborted!", file=sys.stderr) + sys.exit(1) + + def __call__(self, *args, **kwargs): + """Alias for :meth:`main`.""" + return self.main(*args, **kwargs) + + +class Command(BaseCommand): + """Commands are the basic building block of command line interfaces in + Click. A basic command handles command line parsing and might dispatch + more parsing to commands nested below it. + + .. versionchanged:: 2.0 + Added the `context_settings` parameter. + .. versionchanged:: 7.1 + Added the `no_args_is_help` parameter. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + :param callback: the callback to invoke. This is optional. + :param params: the parameters to register with this command. This can + be either :class:`Option` or :class:`Argument` objects. + :param help: the help string to use for this command. + :param epilog: like the help string but it's printed at the end of the + help page after everything else. + :param short_help: the short help to use for this command. This is + shown on the command listing of the parent command. + :param add_help_option: by default each command registers a ``--help`` + option. This can be disabled by this parameter. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is disabled by default. + If enabled this will add ``--help`` as argument + if no arguments are passed + :param hidden: hide this command from help outputs. + + :param deprecated: issues a message indicating that + the command is deprecated. + """ + + def __init__( + self, + name, + context_settings=None, + callback=None, + params=None, + help=None, + epilog=None, + short_help=None, + options_metavar="[OPTIONS]", + add_help_option=True, + no_args_is_help=False, + hidden=False, + deprecated=False, + ): + BaseCommand.__init__(self, name, context_settings) + #: the callback to execute when the command fires. This might be + #: `None` in which case nothing happens. + self.callback = callback + #: the list of parameters for this command in the order they + #: should show up in the help page and execute. Eager parameters + #: will automatically be handled before non eager ones. + self.params = params or [] + # if a form feed (page break) is found in the help text, truncate help + # text to the content preceding the first form feed + if help and "\f" in help: + help = help.split("\f", 1)[0] + self.help = help + self.epilog = epilog + self.options_metavar = options_metavar + self.short_help = short_help + self.add_help_option = add_help_option + self.no_args_is_help = no_args_is_help + self.hidden = hidden + self.deprecated = deprecated + + def get_usage(self, ctx): + """Formats the usage line into a string and returns it. + + Calls :meth:`format_usage` internally. + """ + formatter = ctx.make_formatter() + self.format_usage(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_params(self, ctx): + rv = self.params + help_option = self.get_help_option(ctx) + if help_option is not None: + rv = rv + [help_option] + return rv + + def format_usage(self, ctx, formatter): + """Writes the usage line into the formatter. + + This is a low-level method called by :meth:`get_usage`. + """ + pieces = self.collect_usage_pieces(ctx) + formatter.write_usage(ctx.command_path, " ".join(pieces)) + + def collect_usage_pieces(self, ctx): + """Returns all the pieces that go into the usage line and returns + it as a list of strings. + """ + rv = [self.options_metavar] + for param in self.get_params(ctx): + rv.extend(param.get_usage_pieces(ctx)) + return rv + + def get_help_option_names(self, ctx): + """Returns the names for the help option.""" + all_names = set(ctx.help_option_names) + for param in self.params: + all_names.difference_update(param.opts) + all_names.difference_update(param.secondary_opts) + return all_names + + def get_help_option(self, ctx): + """Returns the help option object.""" + help_options = self.get_help_option_names(ctx) + if not help_options or not self.add_help_option: + return + + def show_help(ctx, param, value): + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + return Option( + help_options, + is_flag=True, + is_eager=True, + expose_value=False, + callback=show_help, + help="Show this message and exit.", + ) + + def make_parser(self, ctx): + """Creates the underlying option parser for this command.""" + parser = OptionParser(ctx) + for param in self.get_params(ctx): + param.add_to_parser(parser, ctx) + return parser + + def get_help(self, ctx): + """Formats the help into a string and returns it. + + Calls :meth:`format_help` internally. + """ + formatter = ctx.make_formatter() + self.format_help(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_short_help_str(self, limit=45): + """Gets short help for the command or makes it by shortening the + long help string. + """ + return ( + self.short_help + or self.help + and make_default_short_help(self.help, limit) + or "" + ) + + def format_help(self, ctx, formatter): + """Writes the help into the formatter if it exists. + + This is a low-level method called by :meth:`get_help`. + + This calls the following methods: + + - :meth:`format_usage` + - :meth:`format_help_text` + - :meth:`format_options` + - :meth:`format_epilog` + """ + self.format_usage(ctx, formatter) + self.format_help_text(ctx, formatter) + self.format_options(ctx, formatter) + self.format_epilog(ctx, formatter) + + def format_help_text(self, ctx, formatter): + """Writes the help text to the formatter if it exists.""" + if self.help: + formatter.write_paragraph() + with formatter.indentation(): + help_text = self.help + if self.deprecated: + help_text += DEPRECATED_HELP_NOTICE + formatter.write_text(help_text) + elif self.deprecated: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(DEPRECATED_HELP_NOTICE) + + def format_options(self, ctx, formatter): + """Writes all the options into the formatter if they exist.""" + opts = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None: + opts.append(rv) + + if opts: + with formatter.section("Options"): + formatter.write_dl(opts) + + def format_epilog(self, ctx, formatter): + """Writes the epilog into the formatter if it exists.""" + if self.epilog: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(self.epilog) + + def parse_args(self, ctx, args): + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + parser = self.make_parser(ctx) + opts, args, param_order = parser.parse_args(args=args) + + for param in iter_params_for_processing(param_order, self.get_params(ctx)): + value, args = param.handle_parse_result(ctx, opts, args) + + if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + ctx.fail( + "Got unexpected extra argument{} ({})".format( + "s" if len(args) != 1 else "", " ".join(map(make_str, args)) + ) + ) + + ctx.args = args + return args + + def invoke(self, ctx): + """Given a context, this invokes the attached callback (if it exists) + in the right way. + """ + _maybe_show_deprecated_notice(self) + if self.callback is not None: + return ctx.invoke(self.callback, **ctx.params) + + +class MultiCommand(Command): + """A multi command is the basic implementation of a command that + dispatches to subcommands. The most common version is the + :class:`Group`. + + :param invoke_without_command: this controls how the multi command itself + is invoked. By default it's only invoked + if a subcommand is provided. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is enabled by default if + `invoke_without_command` is disabled or disabled + if it's enabled. If enabled this will add + ``--help`` as argument if no arguments are + passed. + :param subcommand_metavar: the string that is used in the documentation + to indicate the subcommand place. + :param chain: if this is set to `True` chaining of multiple subcommands + is enabled. This restricts the form of commands in that + they cannot have optional arguments but it allows + multiple commands to be chained together. + :param result_callback: the result callback to attach to this multi + command. + """ + + allow_extra_args = True + allow_interspersed_args = False + + def __init__( + self, + name=None, + invoke_without_command=False, + no_args_is_help=None, + subcommand_metavar=None, + chain=False, + result_callback=None, + **attrs + ): + Command.__init__(self, name, **attrs) + if no_args_is_help is None: + no_args_is_help = not invoke_without_command + self.no_args_is_help = no_args_is_help + self.invoke_without_command = invoke_without_command + if subcommand_metavar is None: + if chain: + subcommand_metavar = SUBCOMMANDS_METAVAR + else: + subcommand_metavar = SUBCOMMAND_METAVAR + self.subcommand_metavar = subcommand_metavar + self.chain = chain + #: The result callback that is stored. This can be set or + #: overridden with the :func:`resultcallback` decorator. + self.result_callback = result_callback + + if self.chain: + for param in self.params: + if isinstance(param, Argument) and not param.required: + raise RuntimeError( + "Multi commands in chain mode cannot have" + " optional arguments." + ) + + def collect_usage_pieces(self, ctx): + rv = Command.collect_usage_pieces(self, ctx) + rv.append(self.subcommand_metavar) + return rv + + def format_options(self, ctx, formatter): + Command.format_options(self, ctx, formatter) + self.format_commands(ctx, formatter) + + def resultcallback(self, replace=False): + """Adds a result callback to the chain command. By default if a + result callback is already registered this will chain them but + this can be disabled with the `replace` parameter. The result + callback is invoked with the return value of the subcommand + (or the list of return values from all subcommands if chaining + is enabled) as well as the parameters as they would be passed + to the main callback. + + Example:: + + @click.group() + @click.option('-i', '--input', default=23) + def cli(input): + return 42 + + @cli.resultcallback() + def process_result(result, input): + return result + input + + .. versionadded:: 3.0 + + :param replace: if set to `True` an already existing result + callback will be removed. + """ + + def decorator(f): + old_callback = self.result_callback + if old_callback is None or replace: + self.result_callback = f + return f + + def function(__value, *args, **kwargs): + return f(old_callback(__value, *args, **kwargs), *args, **kwargs) + + self.result_callback = rv = update_wrapper(function, f) + return rv + + return decorator + + def format_commands(self, ctx, formatter): + """Extra format methods for multi methods that adds all the commands + after the options. + """ + commands = [] + for subcommand in self.list_commands(ctx): + cmd = self.get_command(ctx, subcommand) + # What is this, the tool lied about a command. Ignore it + if cmd is None: + continue + if cmd.hidden: + continue + + commands.append((subcommand, cmd)) + + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section("Commands"): + formatter.write_dl(rows) + + def parse_args(self, ctx, args): + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + rest = Command.parse_args(self, ctx, args) + if self.chain: + ctx.protected_args = rest + ctx.args = [] + elif rest: + ctx.protected_args, ctx.args = rest[:1], rest[1:] + + return ctx.args + + def invoke(self, ctx): + def _process_result(value): + if self.result_callback is not None: + value = ctx.invoke(self.result_callback, value, **ctx.params) + return value + + if not ctx.protected_args: + # If we are invoked without command the chain flag controls + # how this happens. If we are not in chain mode, the return + # value here is the return value of the command. + # If however we are in chain mode, the return value is the + # return value of the result processor invoked with an empty + # list (which means that no subcommand actually was executed). + if self.invoke_without_command: + if not self.chain: + return Command.invoke(self, ctx) + with ctx: + Command.invoke(self, ctx) + return _process_result([]) + ctx.fail("Missing command.") + + # Fetch args back out + args = ctx.protected_args + ctx.args + ctx.args = [] + ctx.protected_args = [] + + # If we're not in chain mode, we only allow the invocation of a + # single command but we also inform the current context about the + # name of the command to invoke. + if not self.chain: + # Make sure the context is entered so we do not clean up + # resources until the result processor has worked. + with ctx: + cmd_name, cmd, args = self.resolve_command(ctx, args) + ctx.invoked_subcommand = cmd_name + Command.invoke(self, ctx) + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) + with sub_ctx: + return _process_result(sub_ctx.command.invoke(sub_ctx)) + + # In chain mode we create the contexts step by step, but after the + # base command has been invoked. Because at that point we do not + # know the subcommands yet, the invoked subcommand attribute is + # set to ``*`` to inform the command that subcommands are executed + # but nothing else. + with ctx: + ctx.invoked_subcommand = "*" if args else None + Command.invoke(self, ctx) + + # Otherwise we make every single context and invoke them in a + # chain. In that case the return value to the result processor + # is the list of all invoked subcommand's results. + contexts = [] + while args: + cmd_name, cmd, args = self.resolve_command(ctx, args) + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + ) + contexts.append(sub_ctx) + args, sub_ctx.args = sub_ctx.args, [] + + rv = [] + for sub_ctx in contexts: + with sub_ctx: + rv.append(sub_ctx.command.invoke(sub_ctx)) + return _process_result(rv) + + def resolve_command(self, ctx, args): + cmd_name = make_str(args[0]) + original_cmd_name = cmd_name + + # Get the command + cmd = self.get_command(ctx, cmd_name) + + # If we can't find the command but there is a normalization + # function available, we try with that one. + if cmd is None and ctx.token_normalize_func is not None: + cmd_name = ctx.token_normalize_func(cmd_name) + cmd = self.get_command(ctx, cmd_name) + + # If we don't find the command we want to show an error message + # to the user that it was not provided. However, there is + # something else we should do: if the first argument looks like + # an option we want to kick off parsing again for arguments to + # resolve things like --help which now should go to the main + # place. + if cmd is None and not ctx.resilient_parsing: + if split_opt(cmd_name)[0]: + self.parse_args(ctx, ctx.args) + ctx.fail("No such command '{}'.".format(original_cmd_name)) + + return cmd_name, cmd, args[1:] + + def get_command(self, ctx, cmd_name): + """Given a context and a command name, this returns a + :class:`Command` object if it exists or returns `None`. + """ + raise NotImplementedError() + + def list_commands(self, ctx): + """Returns a list of subcommand names in the order they should + appear. + """ + return [] + + +class Group(MultiCommand): + """A group allows a command to have subcommands attached. This is the + most common way to implement nesting in Click. + + :param commands: a dictionary of commands. + """ + + def __init__(self, name=None, commands=None, **attrs): + MultiCommand.__init__(self, name, **attrs) + #: the registered subcommands by their exported names. + self.commands = commands or {} + + def add_command(self, cmd, name=None): + """Registers another :class:`Command` with this group. If the name + is not provided, the name of the command is used. + """ + name = name or cmd.name + if name is None: + raise TypeError("Command has no name.") + _check_multicommand(self, name, cmd, register=True) + self.commands[name] = cmd + + def command(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + """ + from .decorators import command + + def decorator(f): + cmd = command(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + return decorator + + def group(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + """ + from .decorators import group + + def decorator(f): + cmd = group(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + return decorator + + def get_command(self, ctx, cmd_name): + return self.commands.get(cmd_name) + + def list_commands(self, ctx): + return sorted(self.commands) + + +class CommandCollection(MultiCommand): + """A command collection is a multi command that merges multiple multi + commands together into one. This is a straightforward implementation + that accepts a list of different multi commands as sources and + provides all the commands for each of them. + """ + + def __init__(self, name=None, sources=None, **attrs): + MultiCommand.__init__(self, name, **attrs) + #: The list of registered multi commands. + self.sources = sources or [] + + def add_source(self, multi_cmd): + """Adds a new multi command to the chain dispatcher.""" + self.sources.append(multi_cmd) + + def get_command(self, ctx, cmd_name): + for source in self.sources: + rv = source.get_command(ctx, cmd_name) + if rv is not None: + if self.chain: + _check_multicommand(self, cmd_name, rv) + return rv + + def list_commands(self, ctx): + rv = set() + for source in self.sources: + rv.update(source.list_commands(ctx)) + return sorted(rv) + + +class Parameter(object): + r"""A parameter to a command comes in two versions: they are either + :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently + not supported by design as some of the internals for parsing are + intentionally not finalized. + + Some settings are supported by both options and arguments. + + :param param_decls: the parameter declarations for this option or + argument. This is a list of flags or argument + names. + :param type: the type that should be used. Either a :class:`ParamType` + or a Python type. The later is converted into the former + automatically if supported. + :param required: controls if this is optional or not. + :param default: the default value if omitted. This can also be a callable, + in which case it's invoked when the default is needed + without any arguments. + :param callback: a callback that should be executed after the parameter + was matched. This is called as ``fn(ctx, param, + value)`` and needs to return the value. + :param nargs: the number of arguments to match. If not ``1`` the return + value is a tuple instead of single value. The default for + nargs is ``1`` (except if the type is a tuple, then it's + the arity of the tuple). + :param metavar: how the value is represented in the help page. + :param expose_value: if this is `True` then the value is passed onwards + to the command callback and stored on the context, + otherwise it's skipped. + :param is_eager: eager values are processed before non eager ones. This + should not be set for arguments or it will inverse the + order of processing. + :param envvar: a string or list of strings that are environment variables + that should be checked. + + .. versionchanged:: 7.1 + Empty environment variables are ignored rather than taking the + empty string value. This makes it possible for scripts to clear + variables if they can't unset them. + + .. versionchanged:: 2.0 + Changed signature for parameter callback to also be passed the + parameter. The old callback format will still work, but it will + raise a warning to give you a chance to migrate the code easier. + """ + param_type_name = "parameter" + + def __init__( + self, + param_decls=None, + type=None, + required=False, + default=None, + callback=None, + nargs=None, + metavar=None, + expose_value=True, + is_eager=False, + envvar=None, + autocompletion=None, + ): + self.name, self.opts, self.secondary_opts = self._parse_decls( + param_decls or (), expose_value + ) + + self.type = convert_type(type, default) + + # Default nargs to what the type tells us if we have that + # information available. + if nargs is None: + if self.type.is_composite: + nargs = self.type.arity + else: + nargs = 1 + + self.required = required + self.callback = callback + self.nargs = nargs + self.multiple = False + self.expose_value = expose_value + self.default = default + self.is_eager = is_eager + self.metavar = metavar + self.envvar = envvar + self.autocompletion = autocompletion + + def __repr__(self): + return "<{} {}>".format(self.__class__.__name__, self.name) + + @property + def human_readable_name(self): + """Returns the human readable name of this parameter. This is the + same as the name for options, but the metavar for arguments. + """ + return self.name + + def make_metavar(self): + if self.metavar is not None: + return self.metavar + metavar = self.type.get_metavar(self) + if metavar is None: + metavar = self.type.name.upper() + if self.nargs != 1: + metavar += "..." + return metavar + + def get_default(self, ctx): + """Given a context variable this calculates the default value.""" + # Otherwise go with the regular default. + if callable(self.default): + rv = self.default() + else: + rv = self.default + return self.type_cast_value(ctx, rv) + + def add_to_parser(self, parser, ctx): + pass + + def consume_value(self, ctx, opts): + value = opts.get(self.name) + if value is None: + value = self.value_from_envvar(ctx) + if value is None: + value = ctx.lookup_default(self.name) + return value + + def type_cast_value(self, ctx, value): + """Given a value this runs it properly through the type system. + This automatically handles things like `nargs` and `multiple` as + well as composite types. + """ + if self.type.is_composite: + if self.nargs <= 1: + raise TypeError( + "Attempted to invoke composite type but nargs has" + " been set to {}. This is not supported; nargs" + " needs to be set to a fixed value > 1.".format(self.nargs) + ) + if self.multiple: + return tuple(self.type(x or (), self, ctx) for x in value or ()) + return self.type(value or (), self, ctx) + + def _convert(value, level): + if level == 0: + return self.type(value, self, ctx) + return tuple(_convert(x, level - 1) for x in value or ()) + + return _convert(value, (self.nargs != 1) + bool(self.multiple)) + + def process_value(self, ctx, value): + """Given a value and context this runs the logic to convert the + value as necessary. + """ + # If the value we were given is None we do nothing. This way + # code that calls this can easily figure out if something was + # not provided. Otherwise it would be converted into an empty + # tuple for multiple invocations which is inconvenient. + if value is not None: + return self.type_cast_value(ctx, value) + + def value_is_missing(self, value): + if value is None: + return True + if (self.nargs != 1 or self.multiple) and value == (): + return True + return False + + def full_process_value(self, ctx, value): + value = self.process_value(ctx, value) + + if value is None and not ctx.resilient_parsing: + value = self.get_default(ctx) + + if self.required and self.value_is_missing(value): + raise MissingParameter(ctx=ctx, param=self) + + return value + + def resolve_envvar_value(self, ctx): + if self.envvar is None: + return + if isinstance(self.envvar, (tuple, list)): + for envvar in self.envvar: + rv = os.environ.get(envvar) + if rv is not None: + return rv + else: + rv = os.environ.get(self.envvar) + + if rv != "": + return rv + + def value_from_envvar(self, ctx): + rv = self.resolve_envvar_value(ctx) + if rv is not None and self.nargs != 1: + rv = self.type.split_envvar_value(rv) + return rv + + def handle_parse_result(self, ctx, opts, args): + with augment_usage_errors(ctx, param=self): + value = self.consume_value(ctx, opts) + try: + value = self.full_process_value(ctx, value) + except Exception: + if not ctx.resilient_parsing: + raise + value = None + if self.callback is not None: + try: + value = invoke_param_callback(self.callback, ctx, self, value) + except Exception: + if not ctx.resilient_parsing: + raise + + if self.expose_value: + ctx.params[self.name] = value + return value, args + + def get_help_record(self, ctx): + pass + + def get_usage_pieces(self, ctx): + return [] + + def get_error_hint(self, ctx): + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + """ + hint_list = self.opts or [self.human_readable_name] + return " / ".join(repr(x) for x in hint_list) + + +class Option(Parameter): + """Options are usually optional values on the command line and + have some extra features that arguments don't have. + + All other parameters are passed onwards to the parameter constructor. + + :param show_default: controls if the default value should be shown on the + help page. Normally, defaults are not shown. If this + value is a string, it shows the string instead of the + value. This is particularly useful for dynamic options. + :param show_envvar: controls if an environment variable should be shown on + the help page. Normally, environment variables + are not shown. + :param prompt: if set to `True` or a non empty string then the user will be + prompted for input. If set to `True` the prompt will be the + option name capitalized. + :param confirmation_prompt: if set then the value will need to be confirmed + if it was prompted for. + :param hide_input: if this is `True` then the input on the prompt will be + hidden from the user. This is useful for password + input. + :param is_flag: forces this option to act as a flag. The default is + auto detection. + :param flag_value: which value should be used for this flag if it's + enabled. This is set to a boolean automatically if + the option string contains a slash to mark two options. + :param multiple: if this is set to `True` then the argument is accepted + multiple times and recorded. This is similar to ``nargs`` + in how it works but supports arbitrary number of + arguments. + :param count: this flag makes an option increment an integer. + :param allow_from_autoenv: if this is enabled then the value of this + parameter will be pulled from an environment + variable in case a prefix is defined on the + context. + :param help: the help string. + :param hidden: hide this option from help outputs. + """ + + param_type_name = "option" + + def __init__( + self, + param_decls=None, + show_default=False, + prompt=False, + confirmation_prompt=False, + hide_input=False, + is_flag=None, + flag_value=None, + multiple=False, + count=False, + allow_from_autoenv=True, + type=None, + help=None, + hidden=False, + show_choices=True, + show_envvar=False, + **attrs + ): + default_is_missing = attrs.get("default", _missing) is _missing + Parameter.__init__(self, param_decls, type=type, **attrs) + + if prompt is True: + prompt_text = self.name.replace("_", " ").capitalize() + elif prompt is False: + prompt_text = None + else: + prompt_text = prompt + self.prompt = prompt_text + self.confirmation_prompt = confirmation_prompt + self.hide_input = hide_input + self.hidden = hidden + + # Flags + if is_flag is None: + if flag_value is not None: + is_flag = True + else: + is_flag = bool(self.secondary_opts) + if is_flag and default_is_missing: + self.default = False + if flag_value is None: + flag_value = not self.default + self.is_flag = is_flag + self.flag_value = flag_value + if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]: + self.type = BOOL + self.is_bool_flag = True + else: + self.is_bool_flag = False + + # Counting + self.count = count + if count: + if type is None: + self.type = IntRange(min=0) + if default_is_missing: + self.default = 0 + + self.multiple = multiple + self.allow_from_autoenv = allow_from_autoenv + self.help = help + self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar + + # Sanity check for stuff we don't support + if __debug__: + if self.nargs < 0: + raise TypeError("Options cannot have nargs < 0") + if self.prompt and self.is_flag and not self.is_bool_flag: + raise TypeError("Cannot prompt for flags that are not bools.") + if not self.is_bool_flag and self.secondary_opts: + raise TypeError("Got secondary option for non boolean flag.") + if self.is_bool_flag and self.hide_input and self.prompt is not None: + raise TypeError("Hidden input does not work with boolean flag prompts.") + if self.count: + if self.multiple: + raise TypeError( + "Options cannot be multiple and count at the same time." + ) + elif self.is_flag: + raise TypeError( + "Options cannot be count and flags at the same time." + ) + + def _parse_decls(self, decls, expose_value): + opts = [] + secondary_opts = [] + name = None + possible_names = [] + + for decl in decls: + if isidentifier(decl): + if name is not None: + raise TypeError("Name defined twice") + name = decl + else: + split_char = ";" if decl[:1] == "/" else "/" + if split_char in decl: + first, second = decl.split(split_char, 1) + first = first.rstrip() + if first: + possible_names.append(split_opt(first)) + opts.append(first) + second = second.lstrip() + if second: + secondary_opts.append(second.lstrip()) + else: + possible_names.append(split_opt(decl)) + opts.append(decl) + + if name is None and possible_names: + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace("-", "_").lower() + if not isidentifier(name): + name = None + + if name is None: + if not expose_value: + return None, opts, secondary_opts + raise TypeError("Could not determine name for option") + + if not opts and not secondary_opts: + raise TypeError( + "No options defined but a name was passed ({}). Did you" + " mean to declare an argument instead of an option?".format(name) + ) + + return name, opts, secondary_opts + + def add_to_parser(self, parser, ctx): + kwargs = { + "dest": self.name, + "nargs": self.nargs, + "obj": self, + } + + if self.multiple: + action = "append" + elif self.count: + action = "count" + else: + action = "store" + + if self.is_flag: + kwargs.pop("nargs", None) + action_const = "{}_const".format(action) + if self.is_bool_flag and self.secondary_opts: + parser.add_option(self.opts, action=action_const, const=True, **kwargs) + parser.add_option( + self.secondary_opts, action=action_const, const=False, **kwargs + ) + else: + parser.add_option( + self.opts, action=action_const, const=self.flag_value, **kwargs + ) + else: + kwargs["action"] = action + parser.add_option(self.opts, **kwargs) + + def get_help_record(self, ctx): + if self.hidden: + return + any_prefix_is_slash = [] + + def _write_opts(opts): + rv, any_slashes = join_options(opts) + if any_slashes: + any_prefix_is_slash[:] = [True] + if not self.is_flag and not self.count: + rv += " {}".format(self.make_metavar()) + return rv + + rv = [_write_opts(self.opts)] + if self.secondary_opts: + rv.append(_write_opts(self.secondary_opts)) + + help = self.help or "" + extra = [] + if self.show_envvar: + envvar = self.envvar + if envvar is None: + if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None: + envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper()) + if envvar is not None: + extra.append( + "env var: {}".format( + ", ".join(str(d) for d in envvar) + if isinstance(envvar, (list, tuple)) + else envvar + ) + ) + if self.default is not None and (self.show_default or ctx.show_default): + if isinstance(self.show_default, string_types): + default_string = "({})".format(self.show_default) + elif isinstance(self.default, (list, tuple)): + default_string = ", ".join(str(d) for d in self.default) + elif inspect.isfunction(self.default): + default_string = "(dynamic)" + else: + default_string = self.default + extra.append("default: {}".format(default_string)) + + if self.required: + extra.append("required") + if extra: + help = "{}[{}]".format( + "{} ".format(help) if help else "", "; ".join(extra) + ) + + return ("; " if any_prefix_is_slash else " / ").join(rv), help + + def get_default(self, ctx): + # If we're a non boolean flag our default is more complex because + # we need to look at all flags in the same group to figure out + # if we're the the default one in which case we return the flag + # value as default. + if self.is_flag and not self.is_bool_flag: + for param in ctx.command.params: + if param.name == self.name and param.default: + return param.flag_value + return None + return Parameter.get_default(self, ctx) + + def prompt_for_value(self, ctx): + """This is an alternative flow that can be activated in the full + value processing if a value does not exist. It will prompt the + user until a valid value exists and then returns the processed + value as result. + """ + # Calculate the default before prompting anything to be stable. + default = self.get_default(ctx) + + # If this is a prompt for a flag we need to handle this + # differently. + if self.is_bool_flag: + return confirm(self.prompt, default) + + return prompt( + self.prompt, + default=default, + type=self.type, + hide_input=self.hide_input, + show_choices=self.show_choices, + confirmation_prompt=self.confirmation_prompt, + value_proc=lambda x: self.process_value(ctx, x), + ) + + def resolve_envvar_value(self, ctx): + rv = Parameter.resolve_envvar_value(self, ctx) + if rv is not None: + return rv + if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None: + envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper()) + return os.environ.get(envvar) + + def value_from_envvar(self, ctx): + rv = self.resolve_envvar_value(ctx) + if rv is None: + return None + value_depth = (self.nargs != 1) + bool(self.multiple) + if value_depth > 0 and rv is not None: + rv = self.type.split_envvar_value(rv) + if self.multiple and self.nargs != 1: + rv = batch(rv, self.nargs) + return rv + + def full_process_value(self, ctx, value): + if value is None and self.prompt is not None and not ctx.resilient_parsing: + return self.prompt_for_value(ctx) + return Parameter.full_process_value(self, ctx, value) + + +class Argument(Parameter): + """Arguments are positional parameters to a command. They generally + provide fewer features than options but can have infinite ``nargs`` + and are required by default. + + All parameters are passed onwards to the parameter constructor. + """ + + param_type_name = "argument" + + def __init__(self, param_decls, required=None, **attrs): + if required is None: + if attrs.get("default") is not None: + required = False + else: + required = attrs.get("nargs", 1) > 0 + Parameter.__init__(self, param_decls, required=required, **attrs) + if self.default is not None and self.nargs < 0: + raise TypeError( + "nargs=-1 in combination with a default value is not supported." + ) + + @property + def human_readable_name(self): + if self.metavar is not None: + return self.metavar + return self.name.upper() + + def make_metavar(self): + if self.metavar is not None: + return self.metavar + var = self.type.get_metavar(self) + if not var: + var = self.name.upper() + if not self.required: + var = "[{}]".format(var) + if self.nargs != 1: + var += "..." + return var + + def _parse_decls(self, decls, expose_value): + if not decls: + if not expose_value: + return None, [], [] + raise TypeError("Could not determine name for argument") + if len(decls) == 1: + name = arg = decls[0] + name = name.replace("-", "_").lower() + else: + raise TypeError( + "Arguments take exactly one parameter declaration, got" + " {}".format(len(decls)) + ) + return name, [arg], [] + + def get_usage_pieces(self, ctx): + return [self.make_metavar()] + + def get_error_hint(self, ctx): + return repr(self.make_metavar()) + + def add_to_parser(self, parser, ctx): + parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) diff --git a/mixly/tools/python/click/decorators.py b/mixly/tools/python/click/decorators.py new file mode 100644 index 00000000..c7b5af6c --- /dev/null +++ b/mixly/tools/python/click/decorators.py @@ -0,0 +1,333 @@ +import inspect +import sys +from functools import update_wrapper + +from ._compat import iteritems +from ._unicodefun import _check_for_unicode_literals +from .core import Argument +from .core import Command +from .core import Group +from .core import Option +from .globals import get_current_context +from .utils import echo + + +def pass_context(f): + """Marks a callback as wanting to receive the current context + object as first argument. + """ + + def new_func(*args, **kwargs): + return f(get_current_context(), *args, **kwargs) + + return update_wrapper(new_func, f) + + +def pass_obj(f): + """Similar to :func:`pass_context`, but only pass the object on the + context onwards (:attr:`Context.obj`). This is useful if that object + represents the state of a nested system. + """ + + def new_func(*args, **kwargs): + return f(get_current_context().obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + +def make_pass_decorator(object_type, ensure=False): + """Given an object type this creates a decorator that will work + similar to :func:`pass_obj` but instead of passing the object of the + current context, it will find the innermost context of type + :func:`object_type`. + + This generates a decorator that works roughly like this:: + + from functools import update_wrapper + + def decorator(f): + @pass_context + def new_func(ctx, *args, **kwargs): + obj = ctx.find_object(object_type) + return ctx.invoke(f, obj, *args, **kwargs) + return update_wrapper(new_func, f) + return decorator + + :param object_type: the type of the object to pass. + :param ensure: if set to `True`, a new object will be created and + remembered on the context if it's not there yet. + """ + + def decorator(f): + def new_func(*args, **kwargs): + ctx = get_current_context() + if ensure: + obj = ctx.ensure_object(object_type) + else: + obj = ctx.find_object(object_type) + if obj is None: + raise RuntimeError( + "Managed to invoke callback without a context" + " object of type '{}' existing".format(object_type.__name__) + ) + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + return decorator + + +def _make_command(f, name, attrs, cls): + if isinstance(f, Command): + raise TypeError("Attempted to convert a callback into a command twice.") + try: + params = f.__click_params__ + params.reverse() + del f.__click_params__ + except AttributeError: + params = [] + help = attrs.get("help") + if help is None: + help = inspect.getdoc(f) + if isinstance(help, bytes): + help = help.decode("utf-8") + else: + help = inspect.cleandoc(help) + attrs["help"] = help + _check_for_unicode_literals() + return cls( + name=name or f.__name__.lower().replace("_", "-"), + callback=f, + params=params, + **attrs + ) + + +def command(name=None, cls=None, **attrs): + r"""Creates a new :class:`Command` and uses the decorated function as + callback. This will also automatically attach all decorated + :func:`option`\s and :func:`argument`\s as parameters to the command. + + The name of the command defaults to the name of the function with + underscores replaced by dashes. If you want to change that, you can + pass the intended name as the first argument. + + All keyword arguments are forwarded to the underlying command class. + + Once decorated the function turns into a :class:`Command` instance + that can be invoked as a command line utility or be attached to a + command :class:`Group`. + + :param name: the name of the command. This defaults to the function + name with underscores replaced by dashes. + :param cls: the command class to instantiate. This defaults to + :class:`Command`. + """ + if cls is None: + cls = Command + + def decorator(f): + cmd = _make_command(f, name, attrs, cls) + cmd.__doc__ = f.__doc__ + return cmd + + return decorator + + +def group(name=None, **attrs): + """Creates a new :class:`Group` with a function as callback. This + works otherwise the same as :func:`command` just that the `cls` + parameter is set to :class:`Group`. + """ + attrs.setdefault("cls", Group) + return command(name, **attrs) + + +def _param_memo(f, param): + if isinstance(f, Command): + f.params.append(param) + else: + if not hasattr(f, "__click_params__"): + f.__click_params__ = [] + f.__click_params__.append(param) + + +def argument(*param_decls, **attrs): + """Attaches an argument to the command. All positional arguments are + passed as parameter declarations to :class:`Argument`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Argument` instance manually + and attaching it to the :attr:`Command.params` list. + + :param cls: the argument class to instantiate. This defaults to + :class:`Argument`. + """ + + def decorator(f): + ArgumentClass = attrs.pop("cls", Argument) + _param_memo(f, ArgumentClass(param_decls, **attrs)) + return f + + return decorator + + +def option(*param_decls, **attrs): + """Attaches an option to the command. All positional arguments are + passed as parameter declarations to :class:`Option`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Option` instance manually + and attaching it to the :attr:`Command.params` list. + + :param cls: the option class to instantiate. This defaults to + :class:`Option`. + """ + + def decorator(f): + # Issue 926, copy attrs, so pre-defined options can re-use the same cls= + option_attrs = attrs.copy() + + if "help" in option_attrs: + option_attrs["help"] = inspect.cleandoc(option_attrs["help"]) + OptionClass = option_attrs.pop("cls", Option) + _param_memo(f, OptionClass(param_decls, **option_attrs)) + return f + + return decorator + + +def confirmation_option(*param_decls, **attrs): + """Shortcut for confirmation prompts that can be ignored by passing + ``--yes`` as parameter. + + This is equivalent to decorating a function with :func:`option` with + the following parameters:: + + def callback(ctx, param, value): + if not value: + ctx.abort() + + @click.command() + @click.option('--yes', is_flag=True, callback=callback, + expose_value=False, prompt='Do you want to continue?') + def dropdb(): + pass + """ + + def decorator(f): + def callback(ctx, param, value): + if not value: + ctx.abort() + + attrs.setdefault("is_flag", True) + attrs.setdefault("callback", callback) + attrs.setdefault("expose_value", False) + attrs.setdefault("prompt", "Do you want to continue?") + attrs.setdefault("help", "Confirm the action without prompting.") + return option(*(param_decls or ("--yes",)), **attrs)(f) + + return decorator + + +def password_option(*param_decls, **attrs): + """Shortcut for password prompts. + + This is equivalent to decorating a function with :func:`option` with + the following parameters:: + + @click.command() + @click.option('--password', prompt=True, confirmation_prompt=True, + hide_input=True) + def changeadmin(password): + pass + """ + + def decorator(f): + attrs.setdefault("prompt", True) + attrs.setdefault("confirmation_prompt", True) + attrs.setdefault("hide_input", True) + return option(*(param_decls or ("--password",)), **attrs)(f) + + return decorator + + +def version_option(version=None, *param_decls, **attrs): + """Adds a ``--version`` option which immediately ends the program + printing out the version number. This is implemented as an eager + option that prints the version and exits the program in the callback. + + :param version: the version number to show. If not provided Click + attempts an auto discovery via setuptools. + :param prog_name: the name of the program (defaults to autodetection) + :param message: custom message to show instead of the default + (``'%(prog)s, version %(version)s'``) + :param others: everything else is forwarded to :func:`option`. + """ + if version is None: + if hasattr(sys, "_getframe"): + module = sys._getframe(1).f_globals.get("__name__") + else: + module = "" + + def decorator(f): + prog_name = attrs.pop("prog_name", None) + message = attrs.pop("message", "%(prog)s, version %(version)s") + + def callback(ctx, param, value): + if not value or ctx.resilient_parsing: + return + prog = prog_name + if prog is None: + prog = ctx.find_root().info_name + ver = version + if ver is None: + try: + import pkg_resources + except ImportError: + pass + else: + for dist in pkg_resources.working_set: + scripts = dist.get_entry_map().get("console_scripts") or {} + for _, entry_point in iteritems(scripts): + if entry_point.module_name == module: + ver = dist.version + break + if ver is None: + raise RuntimeError("Could not determine version") + echo(message % {"prog": prog, "version": ver}, color=ctx.color) + ctx.exit() + + attrs.setdefault("is_flag", True) + attrs.setdefault("expose_value", False) + attrs.setdefault("is_eager", True) + attrs.setdefault("help", "Show the version and exit.") + attrs["callback"] = callback + return option(*(param_decls or ("--version",)), **attrs)(f) + + return decorator + + +def help_option(*param_decls, **attrs): + """Adds a ``--help`` option which immediately ends the program + printing out the help page. This is usually unnecessary to add as + this is added by default to all commands unless suppressed. + + Like :func:`version_option`, this is implemented as eager option that + prints in the callback and exits. + + All arguments are forwarded to :func:`option`. + """ + + def decorator(f): + def callback(ctx, param, value): + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + attrs.setdefault("is_flag", True) + attrs.setdefault("expose_value", False) + attrs.setdefault("help", "Show this message and exit.") + attrs.setdefault("is_eager", True) + attrs["callback"] = callback + return option(*(param_decls or ("--help",)), **attrs)(f) + + return decorator diff --git a/mixly/tools/python/click/exceptions.py b/mixly/tools/python/click/exceptions.py new file mode 100644 index 00000000..592ee38f --- /dev/null +++ b/mixly/tools/python/click/exceptions.py @@ -0,0 +1,253 @@ +from ._compat import filename_to_ui +from ._compat import get_text_stderr +from ._compat import PY2 +from .utils import echo + + +def _join_param_hints(param_hint): + if isinstance(param_hint, (tuple, list)): + return " / ".join(repr(x) for x in param_hint) + return param_hint + + +class ClickException(Exception): + """An exception that Click can handle and show to the user.""" + + #: The exit code for this exception + exit_code = 1 + + def __init__(self, message): + ctor_msg = message + if PY2: + if ctor_msg is not None: + ctor_msg = ctor_msg.encode("utf-8") + Exception.__init__(self, ctor_msg) + self.message = message + + def format_message(self): + return self.message + + def __str__(self): + return self.message + + if PY2: + __unicode__ = __str__ + + def __str__(self): + return self.message.encode("utf-8") + + def show(self, file=None): + if file is None: + file = get_text_stderr() + echo("Error: {}".format(self.format_message()), file=file) + + +class UsageError(ClickException): + """An internal exception that signals a usage error. This typically + aborts any further handling. + + :param message: the error message to display. + :param ctx: optionally the context that caused this error. Click will + fill in the context automatically in some situations. + """ + + exit_code = 2 + + def __init__(self, message, ctx=None): + ClickException.__init__(self, message) + self.ctx = ctx + self.cmd = self.ctx.command if self.ctx else None + + def show(self, file=None): + if file is None: + file = get_text_stderr() + color = None + hint = "" + if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None: + hint = "Try '{} {}' for help.\n".format( + self.ctx.command_path, self.ctx.help_option_names[0] + ) + if self.ctx is not None: + color = self.ctx.color + echo("{}\n{}".format(self.ctx.get_usage(), hint), file=file, color=color) + echo("Error: {}".format(self.format_message()), file=file, color=color) + + +class BadParameter(UsageError): + """An exception that formats out a standardized error message for a + bad parameter. This is useful when thrown from a callback or type as + Click will attach contextual information to it (for instance, which + parameter it is). + + .. versionadded:: 2.0 + + :param param: the parameter object that caused this error. This can + be left out, and Click will attach this info itself + if possible. + :param param_hint: a string that shows up as parameter name. This + can be used as alternative to `param` in cases + where custom validation should happen. If it is + a string it's used as such, if it's a list then + each item is quoted and separated. + """ + + def __init__(self, message, ctx=None, param=None, param_hint=None): + UsageError.__init__(self, message, ctx) + self.param = param + self.param_hint = param_hint + + def format_message(self): + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) + else: + return "Invalid value: {}".format(self.message) + param_hint = _join_param_hints(param_hint) + + return "Invalid value for {}: {}".format(param_hint, self.message) + + +class MissingParameter(BadParameter): + """Raised if click required an option or argument but it was not + provided when invoking the script. + + .. versionadded:: 4.0 + + :param param_type: a string that indicates the type of the parameter. + The default is to inherit the parameter type from + the given `param`. Valid values are ``'parameter'``, + ``'option'`` or ``'argument'``. + """ + + def __init__( + self, message=None, ctx=None, param=None, param_hint=None, param_type=None + ): + BadParameter.__init__(self, message, ctx, param, param_hint) + self.param_type = param_type + + def format_message(self): + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) + else: + param_hint = None + param_hint = _join_param_hints(param_hint) + + param_type = self.param_type + if param_type is None and self.param is not None: + param_type = self.param.param_type_name + + msg = self.message + if self.param is not None: + msg_extra = self.param.type.get_missing_message(self.param) + if msg_extra: + if msg: + msg += ". {}".format(msg_extra) + else: + msg = msg_extra + + return "Missing {}{}{}{}".format( + param_type, + " {}".format(param_hint) if param_hint else "", + ". " if msg else ".", + msg or "", + ) + + def __str__(self): + if self.message is None: + param_name = self.param.name if self.param else None + return "missing parameter: {}".format(param_name) + else: + return self.message + + if PY2: + __unicode__ = __str__ + + def __str__(self): + return self.__unicode__().encode("utf-8") + + +class NoSuchOption(UsageError): + """Raised if click attempted to handle an option that does not + exist. + + .. versionadded:: 4.0 + """ + + def __init__(self, option_name, message=None, possibilities=None, ctx=None): + if message is None: + message = "no such option: {}".format(option_name) + UsageError.__init__(self, message, ctx) + self.option_name = option_name + self.possibilities = possibilities + + def format_message(self): + bits = [self.message] + if self.possibilities: + if len(self.possibilities) == 1: + bits.append("Did you mean {}?".format(self.possibilities[0])) + else: + possibilities = sorted(self.possibilities) + bits.append("(Possible options: {})".format(", ".join(possibilities))) + return " ".join(bits) + + +class BadOptionUsage(UsageError): + """Raised if an option is generally supplied but the use of the option + was incorrect. This is for instance raised if the number of arguments + for an option is not correct. + + .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. + """ + + def __init__(self, option_name, message, ctx=None): + UsageError.__init__(self, message, ctx) + self.option_name = option_name + + +class BadArgumentUsage(UsageError): + """Raised if an argument is generally supplied but the use of the argument + was incorrect. This is for instance raised if the number of values + for an argument is not correct. + + .. versionadded:: 6.0 + """ + + def __init__(self, message, ctx=None): + UsageError.__init__(self, message, ctx) + + +class FileError(ClickException): + """Raised if a file cannot be opened.""" + + def __init__(self, filename, hint=None): + ui_filename = filename_to_ui(filename) + if hint is None: + hint = "unknown error" + ClickException.__init__(self, hint) + self.ui_filename = ui_filename + self.filename = filename + + def format_message(self): + return "Could not open file {}: {}".format(self.ui_filename, self.message) + + +class Abort(RuntimeError): + """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + + __slots__ = ("exit_code",) + + def __init__(self, code=0): + self.exit_code = code diff --git a/mixly/tools/python/click/formatting.py b/mixly/tools/python/click/formatting.py new file mode 100644 index 00000000..319c7f61 --- /dev/null +++ b/mixly/tools/python/click/formatting.py @@ -0,0 +1,283 @@ +from contextlib import contextmanager + +from ._compat import term_len +from .parser import split_opt +from .termui import get_terminal_size + +# Can force a width. This is used by the test system +FORCED_WIDTH = None + + +def measure_table(rows): + widths = {} + for row in rows: + for idx, col in enumerate(row): + widths[idx] = max(widths.get(idx, 0), term_len(col)) + return tuple(y for x, y in sorted(widths.items())) + + +def iter_rows(rows, col_count): + for row in rows: + row = tuple(row) + yield row + ("",) * (col_count - len(row)) + + +def wrap_text( + text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False +): + """A helper function that intelligently wraps text. By default, it + assumes that it operates on a single paragraph of text but if the + `preserve_paragraphs` parameter is provided it will intelligently + handle paragraphs (defined by two empty lines). + + If paragraphs are handled, a paragraph can be prefixed with an empty + line containing the ``\\b`` character (``\\x08``) to indicate that + no rewrapping should happen in that block. + + :param text: the text that should be rewrapped. + :param width: the maximum width for the text. + :param initial_indent: the initial indent that should be placed on the + first line as a string. + :param subsequent_indent: the indent string that should be placed on + each consecutive line. + :param preserve_paragraphs: if this flag is set then the wrapping will + intelligently handle paragraphs. + """ + from ._textwrap import TextWrapper + + text = text.expandtabs() + wrapper = TextWrapper( + width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + replace_whitespace=False, + ) + if not preserve_paragraphs: + return wrapper.fill(text) + + p = [] + buf = [] + indent = None + + def _flush_par(): + if not buf: + return + if buf[0].strip() == "\b": + p.append((indent or 0, True, "\n".join(buf[1:]))) + else: + p.append((indent or 0, False, " ".join(buf))) + del buf[:] + + for line in text.splitlines(): + if not line: + _flush_par() + indent = None + else: + if indent is None: + orig_len = term_len(line) + line = line.lstrip() + indent = orig_len - term_len(line) + buf.append(line) + _flush_par() + + rv = [] + for indent, raw, text in p: + with wrapper.extra_indent(" " * indent): + if raw: + rv.append(wrapper.indent_only(text)) + else: + rv.append(wrapper.fill(text)) + + return "\n\n".join(rv) + + +class HelpFormatter(object): + """This class helps with formatting text-based help pages. It's + usually just needed for very special internal cases, but it's also + exposed so that developers can write their own fancy outputs. + + At present, it always writes into memory. + + :param indent_increment: the additional increment for each level. + :param width: the width for the text. This defaults to the terminal + width clamped to a maximum of 78. + """ + + def __init__(self, indent_increment=2, width=None, max_width=None): + self.indent_increment = indent_increment + if max_width is None: + max_width = 80 + if width is None: + width = FORCED_WIDTH + if width is None: + width = max(min(get_terminal_size()[0], max_width) - 2, 50) + self.width = width + self.current_indent = 0 + self.buffer = [] + + def write(self, string): + """Writes a unicode string into the internal buffer.""" + self.buffer.append(string) + + def indent(self): + """Increases the indentation.""" + self.current_indent += self.indent_increment + + def dedent(self): + """Decreases the indentation.""" + self.current_indent -= self.indent_increment + + def write_usage(self, prog, args="", prefix="Usage: "): + """Writes a usage line into the buffer. + + :param prog: the program name. + :param args: whitespace separated list of arguments. + :param prefix: the prefix for the first line. + """ + usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent) + text_width = self.width - self.current_indent + + if text_width >= (term_len(usage_prefix) + 20): + # The arguments will fit to the right of the prefix. + indent = " " * term_len(usage_prefix) + self.write( + wrap_text( + args, + text_width, + initial_indent=usage_prefix, + subsequent_indent=indent, + ) + ) + else: + # The prefix is too long, put the arguments on the next line. + self.write(usage_prefix) + self.write("\n") + indent = " " * (max(self.current_indent, term_len(prefix)) + 4) + self.write( + wrap_text( + args, text_width, initial_indent=indent, subsequent_indent=indent + ) + ) + + self.write("\n") + + def write_heading(self, heading): + """Writes a heading into the buffer.""" + self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent)) + + def write_paragraph(self): + """Writes a paragraph into the buffer.""" + if self.buffer: + self.write("\n") + + def write_text(self, text): + """Writes re-indented text into the buffer. This rewraps and + preserves paragraphs. + """ + text_width = max(self.width - self.current_indent, 11) + indent = " " * self.current_indent + self.write( + wrap_text( + text, + text_width, + initial_indent=indent, + subsequent_indent=indent, + preserve_paragraphs=True, + ) + ) + self.write("\n") + + def write_dl(self, rows, col_max=30, col_spacing=2): + """Writes a definition list into the buffer. This is how options + and commands are usually formatted. + + :param rows: a list of two item tuples for the terms and values. + :param col_max: the maximum width of the first column. + :param col_spacing: the number of spaces between the first and + second column. + """ + rows = list(rows) + widths = measure_table(rows) + if len(widths) != 2: + raise TypeError("Expected two columns for definition list") + + first_col = min(widths[0], col_max) + col_spacing + + for first, second in iter_rows(rows, len(widths)): + self.write("{:>{w}}{}".format("", first, w=self.current_indent)) + if not second: + self.write("\n") + continue + if term_len(first) <= first_col - col_spacing: + self.write(" " * (first_col - term_len(first))) + else: + self.write("\n") + self.write(" " * (first_col + self.current_indent)) + + text_width = max(self.width - first_col - 2, 10) + wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) + lines = wrapped_text.splitlines() + + if lines: + self.write("{}\n".format(lines[0])) + + for line in lines[1:]: + self.write( + "{:>{w}}{}\n".format( + "", line, w=first_col + self.current_indent + ) + ) + + if len(lines) > 1: + # separate long help from next option + self.write("\n") + else: + self.write("\n") + + @contextmanager + def section(self, name): + """Helpful context manager that writes a paragraph, a heading, + and the indents. + + :param name: the section name that is written as heading. + """ + self.write_paragraph() + self.write_heading(name) + self.indent() + try: + yield + finally: + self.dedent() + + @contextmanager + def indentation(self): + """A context manager that increases the indentation.""" + self.indent() + try: + yield + finally: + self.dedent() + + def getvalue(self): + """Returns the buffer contents.""" + return "".join(self.buffer) + + +def join_options(options): + """Given a list of option strings this joins them in the most appropriate + way and returns them in the form ``(formatted_string, + any_prefix_is_slash)`` where the second item in the tuple is a flag that + indicates if any of the option prefixes was a slash. + """ + rv = [] + any_prefix_is_slash = False + for opt in options: + prefix = split_opt(opt)[0] + if prefix == "/": + any_prefix_is_slash = True + rv.append((len(prefix), opt)) + + rv.sort(key=lambda x: x[0]) + + rv = ", ".join(x[1] for x in rv) + return rv, any_prefix_is_slash diff --git a/mixly/tools/python/click/globals.py b/mixly/tools/python/click/globals.py new file mode 100644 index 00000000..1649f9a0 --- /dev/null +++ b/mixly/tools/python/click/globals.py @@ -0,0 +1,47 @@ +from threading import local + +_local = local() + + +def get_current_context(silent=False): + """Returns the current click context. This can be used as a way to + access the current context object from anywhere. This is a more implicit + alternative to the :func:`pass_context` decorator. This function is + primarily useful for helpers such as :func:`echo` which might be + interested in changing its behavior based on the current context. + + To push the current context, :meth:`Context.scope` can be used. + + .. versionadded:: 5.0 + + :param silent: if set to `True` the return value is `None` if no context + is available. The default behavior is to raise a + :exc:`RuntimeError`. + """ + try: + return _local.stack[-1] + except (AttributeError, IndexError): + if not silent: + raise RuntimeError("There is no active click context.") + + +def push_context(ctx): + """Pushes a new context to the current stack.""" + _local.__dict__.setdefault("stack", []).append(ctx) + + +def pop_context(): + """Removes the top level from the stack.""" + _local.stack.pop() + + +def resolve_color_default(color=None): + """"Internal helper to get the default value of the color flag. If a + value is passed it's returned unchanged, otherwise it's looked up from + the current context. + """ + if color is not None: + return color + ctx = get_current_context(silent=True) + if ctx is not None: + return ctx.color diff --git a/mixly/tools/python/click/parser.py b/mixly/tools/python/click/parser.py new file mode 100644 index 00000000..f43ebfe9 --- /dev/null +++ b/mixly/tools/python/click/parser.py @@ -0,0 +1,428 @@ +# -*- coding: utf-8 -*- +""" +This module started out as largely a copy paste from the stdlib's +optparse module with the features removed that we do not need from +optparse because we implement them in Click on a higher level (for +instance type handling, help formatting and a lot more). + +The plan is to remove more and more from here over time. + +The reason this is a different module and not optparse from the stdlib +is that there are differences in 2.x and 3.x about the error messages +generated and optparse in the stdlib uses gettext for no good reason +and might cause us issues. + +Click uses parts of optparse written by Gregory P. Ward and maintained +by the Python Software Foundation. This is limited to code in parser.py. + +Copyright 2001-2006 Gregory P. Ward. All rights reserved. +Copyright 2002-2006 Python Software Foundation. All rights reserved. +""" +import re +from collections import deque + +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import NoSuchOption +from .exceptions import UsageError + + +def _unpack_args(args, nargs_spec): + """Given an iterable of arguments and an iterable of nargs specifications, + it returns a tuple with all the unpacked arguments at the first index + and all remaining arguments as the second. + + The nargs specification is the number of arguments that should be consumed + or `-1` to indicate that this position should eat up all the remainders. + + Missing items are filled with `None`. + """ + args = deque(args) + nargs_spec = deque(nargs_spec) + rv = [] + spos = None + + def _fetch(c): + try: + if spos is None: + return c.popleft() + else: + return c.pop() + except IndexError: + return None + + while nargs_spec: + nargs = _fetch(nargs_spec) + if nargs == 1: + rv.append(_fetch(args)) + elif nargs > 1: + x = [_fetch(args) for _ in range(nargs)] + # If we're reversed, we're pulling in the arguments in reverse, + # so we need to turn them around. + if spos is not None: + x.reverse() + rv.append(tuple(x)) + elif nargs < 0: + if spos is not None: + raise TypeError("Cannot have two nargs < 0") + spos = len(rv) + rv.append(None) + + # spos is the position of the wildcard (star). If it's not `None`, + # we fill it with the remainder. + if spos is not None: + rv[spos] = tuple(args) + args = [] + rv[spos + 1 :] = reversed(rv[spos + 1 :]) + + return tuple(rv), list(args) + + +def _error_opt_args(nargs, opt): + if nargs == 1: + raise BadOptionUsage(opt, "{} option requires an argument".format(opt)) + raise BadOptionUsage(opt, "{} option requires {} arguments".format(opt, nargs)) + + +def split_opt(opt): + first = opt[:1] + if first.isalnum(): + return "", opt + if opt[1:2] == first: + return opt[:2], opt[2:] + return first, opt[1:] + + +def normalize_opt(opt, ctx): + if ctx is None or ctx.token_normalize_func is None: + return opt + prefix, opt = split_opt(opt) + return prefix + ctx.token_normalize_func(opt) + + +def split_arg_string(string): + """Given an argument string this attempts to split it into small parts.""" + rv = [] + for match in re.finditer( + r"('([^'\\]*(?:\\.[^'\\]*)*)'|\"([^\"\\]*(?:\\.[^\"\\]*)*)\"|\S+)\s*", + string, + re.S, + ): + arg = match.group().strip() + if arg[:1] == arg[-1:] and arg[:1] in "\"'": + arg = arg[1:-1].encode("ascii", "backslashreplace").decode("unicode-escape") + try: + arg = type(string)(arg) + except UnicodeError: + pass + rv.append(arg) + return rv + + +class Option(object): + def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None): + self._short_opts = [] + self._long_opts = [] + self.prefixes = set() + + for opt in opts: + prefix, value = split_opt(opt) + if not prefix: + raise ValueError("Invalid start character for option ({})".format(opt)) + self.prefixes.add(prefix[0]) + if len(prefix) == 1 and len(value) == 1: + self._short_opts.append(opt) + else: + self._long_opts.append(opt) + self.prefixes.add(prefix) + + if action is None: + action = "store" + + self.dest = dest + self.action = action + self.nargs = nargs + self.const = const + self.obj = obj + + @property + def takes_value(self): + return self.action in ("store", "append") + + def process(self, value, state): + if self.action == "store": + state.opts[self.dest] = value + elif self.action == "store_const": + state.opts[self.dest] = self.const + elif self.action == "append": + state.opts.setdefault(self.dest, []).append(value) + elif self.action == "append_const": + state.opts.setdefault(self.dest, []).append(self.const) + elif self.action == "count": + state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 + else: + raise ValueError("unknown action '{}'".format(self.action)) + state.order.append(self.obj) + + +class Argument(object): + def __init__(self, dest, nargs=1, obj=None): + self.dest = dest + self.nargs = nargs + self.obj = obj + + def process(self, value, state): + if self.nargs > 1: + holes = sum(1 for x in value if x is None) + if holes == len(value): + value = None + elif holes != 0: + raise BadArgumentUsage( + "argument {} takes {} values".format(self.dest, self.nargs) + ) + state.opts[self.dest] = value + state.order.append(self.obj) + + +class ParsingState(object): + def __init__(self, rargs): + self.opts = {} + self.largs = [] + self.rargs = rargs + self.order = [] + + +class OptionParser(object): + """The option parser is an internal class that is ultimately used to + parse options and arguments. It's modelled after optparse and brings + a similar but vastly simplified API. It should generally not be used + directly as the high level Click classes wrap it for you. + + It's not nearly as extensible as optparse or argparse as it does not + implement features that are implemented on a higher level (such as + types or defaults). + + :param ctx: optionally the :class:`~click.Context` where this parser + should go with. + """ + + def __init__(self, ctx=None): + #: The :class:`~click.Context` for this parser. This might be + #: `None` for some advanced use cases. + self.ctx = ctx + #: This controls how the parser deals with interspersed arguments. + #: If this is set to `False`, the parser will stop on the first + #: non-option. Click uses this to implement nested subcommands + #: safely. + self.allow_interspersed_args = True + #: This tells the parser how to deal with unknown options. By + #: default it will error out (which is sensible), but there is a + #: second mode where it will ignore it and continue processing + #: after shifting all the unknown options into the resulting args. + self.ignore_unknown_options = False + if ctx is not None: + self.allow_interspersed_args = ctx.allow_interspersed_args + self.ignore_unknown_options = ctx.ignore_unknown_options + self._short_opt = {} + self._long_opt = {} + self._opt_prefixes = {"-", "--"} + self._args = [] + + def add_option(self, opts, dest, action=None, nargs=1, const=None, obj=None): + """Adds a new option named `dest` to the parser. The destination + is not inferred (unlike with optparse) and needs to be explicitly + provided. Action can be any of ``store``, ``store_const``, + ``append``, ``appnd_const`` or ``count``. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + if obj is None: + obj = dest + opts = [normalize_opt(opt, self.ctx) for opt in opts] + option = Option(opts, dest, action=action, nargs=nargs, const=const, obj=obj) + self._opt_prefixes.update(option.prefixes) + for opt in option._short_opts: + self._short_opt[opt] = option + for opt in option._long_opts: + self._long_opt[opt] = option + + def add_argument(self, dest, nargs=1, obj=None): + """Adds a positional argument named `dest` to the parser. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + if obj is None: + obj = dest + self._args.append(Argument(dest=dest, nargs=nargs, obj=obj)) + + def parse_args(self, args): + """Parses positional arguments and returns ``(values, args, order)`` + for the parsed options and arguments as well as the leftover + arguments if there are any. The order is a list of objects as they + appear on the command line. If arguments appear multiple times they + will be memorized multiple times as well. + """ + state = ParsingState(args) + try: + self._process_args_for_options(state) + self._process_args_for_args(state) + except UsageError: + if self.ctx is None or not self.ctx.resilient_parsing: + raise + return state.opts, state.largs, state.order + + def _process_args_for_args(self, state): + pargs, args = _unpack_args( + state.largs + state.rargs, [x.nargs for x in self._args] + ) + + for idx, arg in enumerate(self._args): + arg.process(pargs[idx], state) + + state.largs = args + state.rargs = [] + + def _process_args_for_options(self, state): + while state.rargs: + arg = state.rargs.pop(0) + arglen = len(arg) + # Double dashes always handled explicitly regardless of what + # prefixes are valid. + if arg == "--": + return + elif arg[:1] in self._opt_prefixes and arglen > 1: + self._process_opts(arg, state) + elif self.allow_interspersed_args: + state.largs.append(arg) + else: + state.rargs.insert(0, arg) + return + + # Say this is the original argument list: + # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)] + # ^ + # (we are about to process arg(i)). + # + # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of + # [arg0, ..., arg(i-1)] (any options and their arguments will have + # been removed from largs). + # + # The while loop will usually consume 1 or more arguments per pass. + # If it consumes 1 (eg. arg is an option that takes no arguments), + # then after _process_arg() is done the situation is: + # + # largs = subset of [arg0, ..., arg(i)] + # rargs = [arg(i+1), ..., arg(N-1)] + # + # If allow_interspersed_args is false, largs will always be + # *empty* -- still a subset of [arg0, ..., arg(i-1)], but + # not a very interesting subset! + + def _match_long_opt(self, opt, explicit_value, state): + if opt not in self._long_opt: + possibilities = [word for word in self._long_opt if word.startswith(opt)] + raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) + + option = self._long_opt[opt] + if option.takes_value: + # At this point it's safe to modify rargs by injecting the + # explicit value, because no exception is raised in this + # branch. This means that the inserted value will be fully + # consumed. + if explicit_value is not None: + state.rargs.insert(0, explicit_value) + + nargs = option.nargs + if len(state.rargs) < nargs: + _error_opt_args(nargs, opt) + elif nargs == 1: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + elif explicit_value is not None: + raise BadOptionUsage(opt, "{} option does not take a value".format(opt)) + + else: + value = None + + option.process(value, state) + + def _match_short_opt(self, arg, state): + stop = False + i = 1 + prefix = arg[0] + unknown_options = [] + + for ch in arg[1:]: + opt = normalize_opt(prefix + ch, self.ctx) + option = self._short_opt.get(opt) + i += 1 + + if not option: + if self.ignore_unknown_options: + unknown_options.append(ch) + continue + raise NoSuchOption(opt, ctx=self.ctx) + if option.takes_value: + # Any characters left in arg? Pretend they're the + # next arg, and stop consuming characters of arg. + if i < len(arg): + state.rargs.insert(0, arg[i:]) + stop = True + + nargs = option.nargs + if len(state.rargs) < nargs: + _error_opt_args(nargs, opt) + elif nargs == 1: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + else: + value = None + + option.process(value, state) + + if stop: + break + + # If we got any unknown options we re-combinate the string of the + # remaining options and re-attach the prefix, then report that + # to the state as new larg. This way there is basic combinatorics + # that can be achieved while still ignoring unknown arguments. + if self.ignore_unknown_options and unknown_options: + state.largs.append("{}{}".format(prefix, "".join(unknown_options))) + + def _process_opts(self, arg, state): + explicit_value = None + # Long option handling happens in two parts. The first part is + # supporting explicitly attached values. In any case, we will try + # to long match the option first. + if "=" in arg: + long_opt, explicit_value = arg.split("=", 1) + else: + long_opt = arg + norm_long_opt = normalize_opt(long_opt, self.ctx) + + # At this point we will match the (assumed) long option through + # the long option matching code. Note that this allows options + # like "-foo" to be matched as long options. + try: + self._match_long_opt(norm_long_opt, explicit_value, state) + except NoSuchOption: + # At this point the long option matching failed, and we need + # to try with short options. However there is a special rule + # which says, that if we have a two character options prefix + # (applies to "--foo" for instance), we do not dispatch to the + # short option code and will instead raise the no option + # error. + if arg[:2] not in self._opt_prefixes: + return self._match_short_opt(arg, state) + if not self.ignore_unknown_options: + raise + state.largs.append(arg) diff --git a/mixly/tools/python/click/termui.py b/mixly/tools/python/click/termui.py new file mode 100644 index 00000000..02ef9e9f --- /dev/null +++ b/mixly/tools/python/click/termui.py @@ -0,0 +1,681 @@ +import inspect +import io +import itertools +import os +import struct +import sys + +from ._compat import DEFAULT_COLUMNS +from ._compat import get_winterm_size +from ._compat import isatty +from ._compat import raw_input +from ._compat import string_types +from ._compat import strip_ansi +from ._compat import text_type +from ._compat import WIN +from .exceptions import Abort +from .exceptions import UsageError +from .globals import resolve_color_default +from .types import Choice +from .types import convert_type +from .types import Path +from .utils import echo +from .utils import LazyFile + +# The prompt functions to use. The doc tools currently override these +# functions to customize how they work. +visible_prompt_func = raw_input + +_ansi_colors = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "reset": 39, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, +} +_ansi_reset_all = "\033[0m" + + +def hidden_prompt_func(prompt): + import getpass + + return getpass.getpass(prompt) + + +def _build_prompt( + text, suffix, show_default=False, default=None, show_choices=True, type=None +): + prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += " ({})".format(", ".join(map(str, type.choices))) + if default is not None and show_default: + prompt = "{} [{}]".format(prompt, _format_default(default)) + return prompt + suffix + + +def _format_default(default): + if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"): + return default.name + + return default + + +def prompt( + text, + default=None, + hide_input=False, + confirmation_prompt=False, + type=None, + value_proc=None, + prompt_suffix=": ", + show_default=True, + err=False, + show_choices=True, +): + """Prompts a user for input. This is a convenience function that can + be used to prompt a user for input later. + + If the user aborts the input by sending a interrupt signal, this + function will catch it and raise a :exc:`Abort` exception. + + .. versionadded:: 7.0 + Added the show_choices parameter. + + .. versionadded:: 6.0 + Added unicode support for cmd.exe on Windows. + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param text: the text to show for the prompt. + :param default: the default value to use if no input happens. If this + is not given it will prompt until it's aborted. + :param hide_input: if this is set to true then the input value will + be hidden. + :param confirmation_prompt: asks for confirmation for the value. + :param type: the type to use to check the value against. + :param value_proc: if this parameter is provided it's a function that + is invoked instead of the type conversion to + convert a value. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". + """ + result = None + + def prompt_func(text): + f = hidden_prompt_func if hide_input else visible_prompt_func + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(text, nl=False, err=err) + return f("") + except (KeyboardInterrupt, EOFError): + # getpass doesn't print a newline if the user aborts input with ^C. + # Allegedly this behavior is inherited from getpass(3). + # A doc bug has been filed at https://bugs.python.org/issue24711 + if hide_input: + echo(None, err=err) + raise Abort() + + if value_proc is None: + value_proc = convert_type(type, default) + + prompt = _build_prompt( + text, prompt_suffix, show_default, default, show_choices, type + ) + + while 1: + while 1: + value = prompt_func(prompt) + if value: + break + elif default is not None: + if isinstance(value_proc, Path): + # validate Path default value(exists, dir_okay etc.) + value = default + break + return default + try: + result = value_proc(value) + except UsageError as e: + echo("Error: {}".format(e.message), err=err) # noqa: B306 + continue + if not confirmation_prompt: + return result + while 1: + value2 = prompt_func("Repeat for confirmation: ") + if value2: + break + if value == value2: + return result + echo("Error: the two entered values do not match", err=err) + + +def confirm( + text, default=False, abort=False, prompt_suffix=": ", show_default=True, err=False +): + """Prompts for confirmation (yes/no question). + + If the user aborts the input by sending a interrupt signal this + function will catch it and raise a :exc:`Abort` exception. + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param text: the question to ask. + :param default: the default for the prompt. + :param abort: if this is set to `True` a negative answer aborts the + exception by raising :exc:`Abort`. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + prompt = _build_prompt( + text, prompt_suffix, show_default, "Y/n" if default else "y/N" + ) + while 1: + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(prompt, nl=False, err=err) + value = visible_prompt_func("").lower().strip() + except (KeyboardInterrupt, EOFError): + raise Abort() + if value in ("y", "yes"): + rv = True + elif value in ("n", "no"): + rv = False + elif value == "": + rv = default + else: + echo("Error: invalid input", err=err) + continue + break + if abort and not rv: + raise Abort() + return rv + + +def get_terminal_size(): + """Returns the current size of the terminal as tuple in the form + ``(width, height)`` in columns and rows. + """ + # If shutil has get_terminal_size() (Python 3.3 and later) use that + if sys.version_info >= (3, 3): + import shutil + + shutil_get_terminal_size = getattr(shutil, "get_terminal_size", None) + if shutil_get_terminal_size: + sz = shutil_get_terminal_size() + return sz.columns, sz.lines + + # We provide a sensible default for get_winterm_size() when being invoked + # inside a subprocess. Without this, it would not provide a useful input. + if get_winterm_size is not None: + size = get_winterm_size() + if size == (0, 0): + return (79, 24) + else: + return size + + def ioctl_gwinsz(fd): + try: + import fcntl + import termios + + cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) + except Exception: + return + return cr + + cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2) + if not cr: + try: + fd = os.open(os.ctermid(), os.O_RDONLY) + try: + cr = ioctl_gwinsz(fd) + finally: + os.close(fd) + except Exception: + pass + if not cr or not cr[0] or not cr[1]: + cr = (os.environ.get("LINES", 25), os.environ.get("COLUMNS", DEFAULT_COLUMNS)) + return int(cr[1]), int(cr[0]) + + +def echo_via_pager(text_or_generator, color=None): + """This function takes a text and shows it via an environment specific + pager on stdout. + + .. versionchanged:: 3.0 + Added the `color` flag. + + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + color = resolve_color_default(color) + + if inspect.isgeneratorfunction(text_or_generator): + i = text_or_generator() + elif isinstance(text_or_generator, string_types): + i = [text_or_generator] + else: + i = iter(text_or_generator) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, string_types) else text_type(el) for el in i) + + from ._termui_impl import pager + + return pager(itertools.chain(text_generator, "\n"), color) + + +def progressbar( + iterable=None, + length=None, + label=None, + show_eta=True, + show_percent=None, + show_pos=False, + item_show_func=None, + fill_char="#", + empty_char="-", + bar_template="%(label)s [%(bar)s] %(info)s", + info_sep=" ", + width=36, + file=None, + color=None, +): + """This function creates an iterable context manager that can be used + to iterate over something while showing a progress bar. It will + either iterate over the `iterable` or `length` items (that are counted + up). While iteration happens, this function will print a rendered + progress bar to the given `file` (defaults to stdout) and will attempt + to calculate remaining time and more. By default, this progress bar + will not be rendered if the file is not a terminal. + + The context manager creates the progress bar. When the context + manager is entered the progress bar is already created. With every + iteration over the progress bar, the iterable passed to the bar is + advanced and the bar is updated. When the context manager exits, + a newline is printed and the progress bar is finalized on screen. + + Note: The progress bar is currently designed for use cases where the + total progress can be expected to take at least several seconds. + Because of this, the ProgressBar class object won't display + progress that is considered too fast, and progress where the time + between steps is less than a second. + + No printing must happen or the progress bar will be unintentionally + destroyed. + + Example usage:: + + with progressbar(items) as bar: + for item in bar: + do_something_with(item) + + Alternatively, if no iterable is specified, one can manually update the + progress bar through the `update()` method instead of directly + iterating over the progress bar. The update method accepts the number + of steps to increment the bar with:: + + with progressbar(length=chunks.total_bytes) as bar: + for chunk in chunks: + process_chunk(chunk) + bar.update(chunks.bytes) + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `color` parameter. Added a `update` method to the + progressbar object. + + :param iterable: an iterable to iterate over. If not provided the length + is required. + :param length: the number of items to iterate over. By default the + progressbar will attempt to ask the iterator about its + length, which might or might not work. If an iterable is + also provided this parameter can be used to override the + length. If an iterable is not provided the progress bar + will iterate over a range of that length. + :param label: the label to show next to the progress bar. + :param show_eta: enables or disables the estimated time display. This is + automatically disabled if the length cannot be + determined. + :param show_percent: enables or disables the percentage display. The + default is `True` if the iterable has a length or + `False` if not. + :param show_pos: enables or disables the absolute position display. The + default is `False`. + :param item_show_func: a function called with the current item which + can return a string to show the current item + next to the progress bar. Note that the current + item can be `None`! + :param fill_char: the character to use to show the filled part of the + progress bar. + :param empty_char: the character to use to show the non-filled part of + the progress bar. + :param bar_template: the format string to use as template for the bar. + The parameters in it are ``label`` for the label, + ``bar`` for the progress bar and ``info`` for the + info section. + :param info_sep: the separator between multiple info items (eta etc.) + :param width: the width of the progress bar in characters, 0 means full + terminal width + :param file: the file to write to. If this is not a terminal then + only the label is printed. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are included anywhere in the progress bar output + which is not the case by default. + """ + from ._termui_impl import ProgressBar + + color = resolve_color_default(color) + return ProgressBar( + iterable=iterable, + length=length, + show_eta=show_eta, + show_percent=show_percent, + show_pos=show_pos, + item_show_func=item_show_func, + fill_char=fill_char, + empty_char=empty_char, + bar_template=bar_template, + info_sep=info_sep, + file=file, + label=label, + width=width, + color=color, + ) + + +def clear(): + """Clears the terminal screen. This will have the effect of clearing + the whole visible space of the terminal and moving the cursor to the + top left. This does not do anything if not connected to a terminal. + + .. versionadded:: 2.0 + """ + if not isatty(sys.stdout): + return + # If we're on Windows and we don't have colorama available, then we + # clear the screen by shelling out. Otherwise we can use an escape + # sequence. + if WIN: + os.system("cls") + else: + sys.stdout.write("\033[2J\033[1;1H") + + +def style( + text, + fg=None, + bg=None, + bold=None, + dim=None, + underline=None, + blink=None, + reverse=None, + reset=True, +): + """Styles a text with ANSI styles and returns the new string. By + default the styling is self contained which means that at the end + of the string a reset code is issued. This can be prevented by + passing ``reset=False``. + + Examples:: + + click.echo(click.style('Hello World!', fg='green')) + click.echo(click.style('ATTENTION!', blink=True)) + click.echo(click.style('Some things', reverse=True, fg='cyan')) + + Supported color names: + + * ``black`` (might be a gray) + * ``red`` + * ``green`` + * ``yellow`` (might be an orange) + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` + * ``reset`` (reset the color code only) + + .. versionadded:: 2.0 + + .. versionadded:: 7.0 + Added support for bright colors. + + :param text: the string to style with ansi codes. + :param fg: if provided this will become the foreground color. + :param bg: if provided this will become the background color. + :param bold: if provided this will enable or disable bold mode. + :param dim: if provided this will enable or disable dim mode. This is + badly supported. + :param underline: if provided this will enable or disable underline. + :param blink: if provided this will enable or disable blinking. + :param reverse: if provided this will enable or disable inverse + rendering (foreground becomes background and the + other way round). + :param reset: by default a reset-all code is added at the end of the + string which means that styles do not carry over. This + can be disabled to compose styles. + """ + bits = [] + if fg: + try: + bits.append("\033[{}m".format(_ansi_colors[fg])) + except KeyError: + raise TypeError("Unknown color '{}'".format(fg)) + if bg: + try: + bits.append("\033[{}m".format(_ansi_colors[bg] + 10)) + except KeyError: + raise TypeError("Unknown color '{}'".format(bg)) + if bold is not None: + bits.append("\033[{}m".format(1 if bold else 22)) + if dim is not None: + bits.append("\033[{}m".format(2 if dim else 22)) + if underline is not None: + bits.append("\033[{}m".format(4 if underline else 24)) + if blink is not None: + bits.append("\033[{}m".format(5 if blink else 25)) + if reverse is not None: + bits.append("\033[{}m".format(7 if reverse else 27)) + bits.append(text) + if reset: + bits.append(_ansi_reset_all) + return "".join(bits) + + +def unstyle(text): + """Removes ANSI styling information from a string. Usually it's not + necessary to use this function as Click's echo function will + automatically remove styling if necessary. + + .. versionadded:: 2.0 + + :param text: the text to remove style information from. + """ + return strip_ansi(text) + + +def secho(message=None, file=None, nl=True, err=False, color=None, **styles): + """This function combines :func:`echo` and :func:`style` into one + call. As such the following two calls are the same:: + + click.secho('Hello World!', fg='green') + click.echo(click.style('Hello World!', fg='green')) + + All keyword arguments are forwarded to the underlying functions + depending on which one they go with. + + .. versionadded:: 2.0 + """ + if message is not None: + message = style(message, **styles) + return echo(message, file=file, nl=nl, err=err, color=color) + + +def edit( + text=None, editor=None, env=None, require_save=True, extension=".txt", filename=None +): + r"""Edits the given text in the defined editor. If an editor is given + (should be the full path to the executable but the regular operating + system search path is used for finding the executable) it overrides + the detected editor. Optionally, some environment variables can be + used. If the editor is closed without changes, `None` is returned. In + case a file is edited directly the return value is always `None` and + `require_save` and `extension` are ignored. + + If the editor cannot be opened a :exc:`UsageError` is raised. + + Note for Windows: to simplify cross-platform usage, the newlines are + automatically converted from POSIX to Windows and vice versa. As such, + the message here will have ``\n`` as newline markers. + + :param text: the text to edit. + :param editor: optionally the editor to use. Defaults to automatic + detection. + :param env: environment variables to forward to the editor. + :param require_save: if this is true, then not saving in the editor + will make the return value become `None`. + :param extension: the extension to tell the editor about. This defaults + to `.txt` but changing this might change syntax + highlighting. + :param filename: if provided it will edit this file instead of the + provided text contents. It will not use a temporary + file as an indirection in that case. + """ + from ._termui_impl import Editor + + editor = Editor( + editor=editor, env=env, require_save=require_save, extension=extension + ) + if filename is None: + return editor.edit(text) + editor.edit_file(filename) + + +def launch(url, wait=False, locate=False): + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + Examples:: + + click.launch('https://click.palletsprojects.com/') + click.launch('/my/downloaded/file', locate=True) + + .. versionadded:: 2.0 + + :param url: URL or filename of the thing to launch. + :param wait: waits for the program to stop. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + from ._termui_impl import open_url + + return open_url(url, wait=wait, locate=locate) + + +# If this is provided, getchar() calls into this instead. This is used +# for unittesting purposes. +_getchar = None + + +def getchar(echo=False): + """Fetches a single character from the terminal and returns it. This + will always return a unicode character and under certain rare + circumstances this might return more than one character. The + situations which more than one character is returned is when for + whatever reason multiple characters end up in the terminal buffer or + standard input was not actually a terminal. + + Note that this will always read from the terminal, even if something + is piped into the standard input. + + Note for Windows: in rare cases when typing non-ASCII characters, this + function might wait for a second character and then return both at once. + This is because certain Unicode characters look like special-key markers. + + .. versionadded:: 2.0 + + :param echo: if set to `True`, the character read will also show up on + the terminal. The default is to not show it. + """ + f = _getchar + if f is None: + from ._termui_impl import getchar as f + return f(echo) + + +def raw_terminal(): + from ._termui_impl import raw_terminal as f + + return f() + + +def pause(info="Press any key to continue ...", err=False): + """This command stops execution and waits for the user to press any + key to continue. This is similar to the Windows batch "pause" + command. If the program is not run through a terminal, this command + will instead do nothing. + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param info: the info string to print before pausing. + :param err: if set to message goes to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + if not isatty(sys.stdin) or not isatty(sys.stdout): + return + try: + if info: + echo(info, nl=False, err=err) + try: + getchar() + except (KeyboardInterrupt, EOFError): + pass + finally: + if info: + echo(err=err) diff --git a/mixly/tools/python/click/testing.py b/mixly/tools/python/click/testing.py new file mode 100644 index 00000000..a3dba3b3 --- /dev/null +++ b/mixly/tools/python/click/testing.py @@ -0,0 +1,382 @@ +import contextlib +import os +import shlex +import shutil +import sys +import tempfile + +from . import formatting +from . import termui +from . import utils +from ._compat import iteritems +from ._compat import PY2 +from ._compat import string_types + + +if PY2: + from cStringIO import StringIO +else: + import io + from ._compat import _find_binary_reader + + +class EchoingStdin(object): + def __init__(self, input, output): + self._input = input + self._output = output + + def __getattr__(self, x): + return getattr(self._input, x) + + def _echo(self, rv): + self._output.write(rv) + return rv + + def read(self, n=-1): + return self._echo(self._input.read(n)) + + def readline(self, n=-1): + return self._echo(self._input.readline(n)) + + def readlines(self): + return [self._echo(x) for x in self._input.readlines()] + + def __iter__(self): + return iter(self._echo(x) for x in self._input) + + def __repr__(self): + return repr(self._input) + + +def make_input_stream(input, charset): + # Is already an input stream. + if hasattr(input, "read"): + if PY2: + return input + rv = _find_binary_reader(input) + if rv is not None: + return rv + raise TypeError("Could not find binary reader for input stream.") + + if input is None: + input = b"" + elif not isinstance(input, bytes): + input = input.encode(charset) + if PY2: + return StringIO(input) + return io.BytesIO(input) + + +class Result(object): + """Holds the captured result of an invoked CLI script.""" + + def __init__( + self, runner, stdout_bytes, stderr_bytes, exit_code, exception, exc_info=None + ): + #: The runner that created the result + self.runner = runner + #: The standard output as bytes. + self.stdout_bytes = stdout_bytes + #: The standard error as bytes, or None if not available + self.stderr_bytes = stderr_bytes + #: The exit code as integer. + self.exit_code = exit_code + #: The exception that happened if one did. + self.exception = exception + #: The traceback + self.exc_info = exc_info + + @property + def output(self): + """The (standard) output as unicode string.""" + return self.stdout + + @property + def stdout(self): + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stderr(self): + """The standard error as unicode string.""" + if self.stderr_bytes is None: + raise ValueError("stderr not separately captured") + return self.stderr_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + def __repr__(self): + return "<{} {}>".format( + type(self).__name__, repr(self.exception) if self.exception else "okay" + ) + + +class CliRunner(object): + """The CLI runner provides functionality to invoke a Click command line + script for unittesting purposes in a isolated environment. This only + works in single-threaded systems without any concurrency as it changes the + global interpreter state. + + :param charset: the character set for the input and output data. This is + UTF-8 by default and should not be changed currently as + the reporting to Click only works in Python 2 properly. + :param env: a dictionary with environment variables for overriding. + :param echo_stdin: if this is set to `True`, then reading from stdin writes + to stdout. This is useful for showing examples in + some circumstances. Note that regular prompts + will automatically echo the input. + :param mix_stderr: if this is set to `False`, then stdout and stderr are + preserved as independent streams. This is useful for + Unix-philosophy apps that have predictable stdout and + noisy stderr, such that each may be measured + independently + """ + + def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True): + if charset is None: + charset = "utf-8" + self.charset = charset + self.env = env or {} + self.echo_stdin = echo_stdin + self.mix_stderr = mix_stderr + + def get_default_prog_name(self, cli): + """Given a command object it will return the default program name + for it. The default is the `name` attribute or ``"root"`` if not + set. + """ + return cli.name or "root" + + def make_env(self, overrides=None): + """Returns the environment overrides for invoking a script.""" + rv = dict(self.env) + if overrides: + rv.update(overrides) + return rv + + @contextlib.contextmanager + def isolation(self, input=None, env=None, color=False): + """A context manager that sets up the isolation for invoking of a + command line tool. This sets up stdin with the given input data + and `os.environ` with the overrides from the given dictionary. + This also rebinds some internals in Click to be mocked (like the + prompt functionality). + + This is automatically done in the :meth:`invoke` method. + + .. versionadded:: 4.0 + The ``color`` parameter was added. + + :param input: the input stream to put into sys.stdin. + :param env: the environment overrides as dictionary. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + """ + input = make_input_stream(input, self.charset) + + old_stdin = sys.stdin + old_stdout = sys.stdout + old_stderr = sys.stderr + old_forced_width = formatting.FORCED_WIDTH + formatting.FORCED_WIDTH = 80 + + env = self.make_env(env) + + if PY2: + bytes_output = StringIO() + if self.echo_stdin: + input = EchoingStdin(input, bytes_output) + sys.stdout = bytes_output + if not self.mix_stderr: + bytes_error = StringIO() + sys.stderr = bytes_error + else: + bytes_output = io.BytesIO() + if self.echo_stdin: + input = EchoingStdin(input, bytes_output) + input = io.TextIOWrapper(input, encoding=self.charset) + sys.stdout = io.TextIOWrapper(bytes_output, encoding=self.charset) + if not self.mix_stderr: + bytes_error = io.BytesIO() + sys.stderr = io.TextIOWrapper(bytes_error, encoding=self.charset) + + if self.mix_stderr: + sys.stderr = sys.stdout + + sys.stdin = input + + def visible_input(prompt=None): + sys.stdout.write(prompt or "") + val = input.readline().rstrip("\r\n") + sys.stdout.write("{}\n".format(val)) + sys.stdout.flush() + return val + + def hidden_input(prompt=None): + sys.stdout.write("{}\n".format(prompt or "")) + sys.stdout.flush() + return input.readline().rstrip("\r\n") + + def _getchar(echo): + char = sys.stdin.read(1) + if echo: + sys.stdout.write(char) + sys.stdout.flush() + return char + + default_color = color + + def should_strip_ansi(stream=None, color=None): + if color is None: + return not default_color + return not color + + old_visible_prompt_func = termui.visible_prompt_func + old_hidden_prompt_func = termui.hidden_prompt_func + old__getchar_func = termui._getchar + old_should_strip_ansi = utils.should_strip_ansi + termui.visible_prompt_func = visible_input + termui.hidden_prompt_func = hidden_input + termui._getchar = _getchar + utils.should_strip_ansi = should_strip_ansi + + old_env = {} + try: + for key, value in iteritems(env): + old_env[key] = os.environ.get(key) + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + yield (bytes_output, not self.mix_stderr and bytes_error) + finally: + for key, value in iteritems(old_env): + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + sys.stdout = old_stdout + sys.stderr = old_stderr + sys.stdin = old_stdin + termui.visible_prompt_func = old_visible_prompt_func + termui.hidden_prompt_func = old_hidden_prompt_func + termui._getchar = old__getchar_func + utils.should_strip_ansi = old_should_strip_ansi + formatting.FORCED_WIDTH = old_forced_width + + def invoke( + self, + cli, + args=None, + input=None, + env=None, + catch_exceptions=True, + color=False, + **extra + ): + """Invokes a command in an isolated environment. The arguments are + forwarded directly to the command line script, the `extra` keyword + arguments are passed to the :meth:`~clickpkg.Command.main` function of + the command. + + This returns a :class:`Result` object. + + .. versionadded:: 3.0 + The ``catch_exceptions`` parameter was added. + + .. versionchanged:: 3.0 + The result object now has an `exc_info` attribute with the + traceback if available. + + .. versionadded:: 4.0 + The ``color`` parameter was added. + + :param cli: the command to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. + :param input: the input data for `sys.stdin`. + :param env: the environment overrides. + :param catch_exceptions: Whether to catch any other exceptions than + ``SystemExit``. + :param extra: the keyword arguments to pass to :meth:`main`. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + """ + exc_info = None + with self.isolation(input=input, env=env, color=color) as outstreams: + exception = None + exit_code = 0 + + if isinstance(args, string_types): + args = shlex.split(args) + + try: + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + cli.main(args=args or (), prog_name=prog_name, **extra) + except SystemExit as e: + exc_info = sys.exc_info() + exit_code = e.code + if exit_code is None: + exit_code = 0 + + if exit_code != 0: + exception = e + + if not isinstance(exit_code, int): + sys.stdout.write(str(exit_code)) + sys.stdout.write("\n") + exit_code = 1 + + except Exception as e: + if not catch_exceptions: + raise + exception = e + exit_code = 1 + exc_info = sys.exc_info() + finally: + sys.stdout.flush() + stdout = outstreams[0].getvalue() + if self.mix_stderr: + stderr = None + else: + stderr = outstreams[1].getvalue() + + return Result( + runner=self, + stdout_bytes=stdout, + stderr_bytes=stderr, + exit_code=exit_code, + exception=exception, + exc_info=exc_info, + ) + + @contextlib.contextmanager + def isolated_filesystem(self): + """A context manager that creates a temporary folder and changes + the current working directory to it for isolated filesystem tests. + """ + cwd = os.getcwd() + t = tempfile.mkdtemp() + os.chdir(t) + try: + yield t + finally: + os.chdir(cwd) + try: + shutil.rmtree(t) + except (OSError, IOError): # noqa: B014 + pass diff --git a/mixly/tools/python/click/types.py b/mixly/tools/python/click/types.py new file mode 100644 index 00000000..505c39f8 --- /dev/null +++ b/mixly/tools/python/click/types.py @@ -0,0 +1,762 @@ +import os +import stat +from datetime import datetime + +from ._compat import _get_argv_encoding +from ._compat import filename_to_ui +from ._compat import get_filesystem_encoding +from ._compat import get_streerror +from ._compat import open_stream +from ._compat import PY2 +from ._compat import text_type +from .exceptions import BadParameter +from .utils import LazyFile +from .utils import safecall + + +class ParamType(object): + """Helper for converting values through types. The following is + necessary for a valid type: + + * it needs a name + * it needs to pass through None unchanged + * it needs to convert from a string + * it needs to convert its result type through unchanged + (eg: needs to be idempotent) + * it needs to be able to deal with param and context being `None`. + This can be the case when the object is used with prompt + inputs. + """ + + is_composite = False + + #: the descriptive name of this type + name = None + + #: if a list of this type is expected and the value is pulled from a + #: string environment variable, this is what splits it up. `None` + #: means any whitespace. For all parameters the general rule is that + #: whitespace splits them up. The exception are paths and files which + #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on + #: Windows). + envvar_list_splitter = None + + def __call__(self, value, param=None, ctx=None): + if value is not None: + return self.convert(value, param, ctx) + + def get_metavar(self, param): + """Returns the metavar default for this param if it provides one.""" + + def get_missing_message(self, param): + """Optionally might return extra information about a missing + parameter. + + .. versionadded:: 2.0 + """ + + def convert(self, value, param, ctx): + """Converts the value. This is not invoked for values that are + `None` (the missing value). + """ + return value + + def split_envvar_value(self, rv): + """Given a value from an environment variable this splits it up + into small chunks depending on the defined envvar list splitter. + + If the splitter is set to `None`, which means that whitespace splits, + then leading and trailing whitespace is ignored. Otherwise, leading + and trailing splitters usually lead to empty items being included. + """ + return (rv or "").split(self.envvar_list_splitter) + + def fail(self, message, param=None, ctx=None): + """Helper method to fail with an invalid value message.""" + raise BadParameter(message, ctx=ctx, param=param) + + +class CompositeParamType(ParamType): + is_composite = True + + @property + def arity(self): + raise NotImplementedError() + + +class FuncParamType(ParamType): + def __init__(self, func): + self.name = func.__name__ + self.func = func + + def convert(self, value, param, ctx): + try: + return self.func(value) + except ValueError: + try: + value = text_type(value) + except UnicodeError: + value = str(value).decode("utf-8", "replace") + self.fail(value, param, ctx) + + +class UnprocessedParamType(ParamType): + name = "text" + + def convert(self, value, param, ctx): + return value + + def __repr__(self): + return "UNPROCESSED" + + +class StringParamType(ParamType): + name = "text" + + def convert(self, value, param, ctx): + if isinstance(value, bytes): + enc = _get_argv_encoding() + try: + value = value.decode(enc) + except UnicodeError: + fs_enc = get_filesystem_encoding() + if fs_enc != enc: + try: + value = value.decode(fs_enc) + except UnicodeError: + value = value.decode("utf-8", "replace") + else: + value = value.decode("utf-8", "replace") + return value + return value + + def __repr__(self): + return "STRING" + + +class Choice(ParamType): + """The choice type allows a value to be checked against a fixed set + of supported values. All of these values have to be strings. + + You should only pass a list or tuple of choices. Other iterables + (like generators) may lead to surprising results. + + The resulting value will always be one of the originally passed choices + regardless of ``case_sensitive`` or any ``ctx.token_normalize_func`` + being specified. + + See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. + """ + + name = "choice" + + def __init__(self, choices, case_sensitive=True): + self.choices = choices + self.case_sensitive = case_sensitive + + def get_metavar(self, param): + return "[{}]".format("|".join(self.choices)) + + def get_missing_message(self, param): + return "Choose from:\n\t{}.".format(",\n\t".join(self.choices)) + + def convert(self, value, param, ctx): + # Match through normalization and case sensitivity + # first do token_normalize_func, then lowercase + # preserve original `value` to produce an accurate message in + # `self.fail` + normed_value = value + normed_choices = {choice: choice for choice in self.choices} + + if ctx is not None and ctx.token_normalize_func is not None: + normed_value = ctx.token_normalize_func(value) + normed_choices = { + ctx.token_normalize_func(normed_choice): original + for normed_choice, original in normed_choices.items() + } + + if not self.case_sensitive: + if PY2: + lower = str.lower + else: + lower = str.casefold + + normed_value = lower(normed_value) + normed_choices = { + lower(normed_choice): original + for normed_choice, original in normed_choices.items() + } + + if normed_value in normed_choices: + return normed_choices[normed_value] + + self.fail( + "invalid choice: {}. (choose from {})".format( + value, ", ".join(self.choices) + ), + param, + ctx, + ) + + def __repr__(self): + return "Choice('{}')".format(list(self.choices)) + + +class DateTime(ParamType): + """The DateTime type converts date strings into `datetime` objects. + + The format strings which are checked are configurable, but default to some + common (non-timezone aware) ISO 8601 formats. + + When specifying *DateTime* formats, you should only pass a list or a tuple. + Other iterables, like generators, may lead to surprising results. + + The format strings are processed using ``datetime.strptime``, and this + consequently defines the format strings which are allowed. + + Parsing is tried using each format, in order, and the first format which + parses successfully is used. + + :param formats: A list or tuple of date format strings, in the order in + which they should be tried. Defaults to + ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, + ``'%Y-%m-%d %H:%M:%S'``. + """ + + name = "datetime" + + def __init__(self, formats=None): + self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] + + def get_metavar(self, param): + return "[{}]".format("|".join(self.formats)) + + def _try_to_convert_date(self, value, format): + try: + return datetime.strptime(value, format) + except ValueError: + return None + + def convert(self, value, param, ctx): + # Exact match + for format in self.formats: + dtime = self._try_to_convert_date(value, format) + if dtime: + return dtime + + self.fail( + "invalid datetime format: {}. (choose from {})".format( + value, ", ".join(self.formats) + ) + ) + + def __repr__(self): + return "DateTime" + + +class IntParamType(ParamType): + name = "integer" + + def convert(self, value, param, ctx): + try: + return int(value) + except ValueError: + self.fail("{} is not a valid integer".format(value), param, ctx) + + def __repr__(self): + return "INT" + + +class IntRange(IntParamType): + """A parameter that works similar to :data:`click.INT` but restricts + the value to fit into a range. The default behavior is to fail if the + value falls outside the range, but it can also be silently clamped + between the two edges. + + See :ref:`ranges` for an example. + """ + + name = "integer range" + + def __init__(self, min=None, max=None, clamp=False): + self.min = min + self.max = max + self.clamp = clamp + + def convert(self, value, param, ctx): + rv = IntParamType.convert(self, value, param, ctx) + if self.clamp: + if self.min is not None and rv < self.min: + return self.min + if self.max is not None and rv > self.max: + return self.max + if ( + self.min is not None + and rv < self.min + or self.max is not None + and rv > self.max + ): + if self.min is None: + self.fail( + "{} is bigger than the maximum valid value {}.".format( + rv, self.max + ), + param, + ctx, + ) + elif self.max is None: + self.fail( + "{} is smaller than the minimum valid value {}.".format( + rv, self.min + ), + param, + ctx, + ) + else: + self.fail( + "{} is not in the valid range of {} to {}.".format( + rv, self.min, self.max + ), + param, + ctx, + ) + return rv + + def __repr__(self): + return "IntRange({}, {})".format(self.min, self.max) + + +class FloatParamType(ParamType): + name = "float" + + def convert(self, value, param, ctx): + try: + return float(value) + except ValueError: + self.fail( + "{} is not a valid floating point value".format(value), param, ctx + ) + + def __repr__(self): + return "FLOAT" + + +class FloatRange(FloatParamType): + """A parameter that works similar to :data:`click.FLOAT` but restricts + the value to fit into a range. The default behavior is to fail if the + value falls outside the range, but it can also be silently clamped + between the two edges. + + See :ref:`ranges` for an example. + """ + + name = "float range" + + def __init__(self, min=None, max=None, clamp=False): + self.min = min + self.max = max + self.clamp = clamp + + def convert(self, value, param, ctx): + rv = FloatParamType.convert(self, value, param, ctx) + if self.clamp: + if self.min is not None and rv < self.min: + return self.min + if self.max is not None and rv > self.max: + return self.max + if ( + self.min is not None + and rv < self.min + or self.max is not None + and rv > self.max + ): + if self.min is None: + self.fail( + "{} is bigger than the maximum valid value {}.".format( + rv, self.max + ), + param, + ctx, + ) + elif self.max is None: + self.fail( + "{} is smaller than the minimum valid value {}.".format( + rv, self.min + ), + param, + ctx, + ) + else: + self.fail( + "{} is not in the valid range of {} to {}.".format( + rv, self.min, self.max + ), + param, + ctx, + ) + return rv + + def __repr__(self): + return "FloatRange({}, {})".format(self.min, self.max) + + +class BoolParamType(ParamType): + name = "boolean" + + def convert(self, value, param, ctx): + if isinstance(value, bool): + return bool(value) + value = value.lower() + if value in ("true", "t", "1", "yes", "y"): + return True + elif value in ("false", "f", "0", "no", "n"): + return False + self.fail("{} is not a valid boolean".format(value), param, ctx) + + def __repr__(self): + return "BOOL" + + +class UUIDParameterType(ParamType): + name = "uuid" + + def convert(self, value, param, ctx): + import uuid + + try: + if PY2 and isinstance(value, text_type): + value = value.encode("ascii") + return uuid.UUID(value) + except ValueError: + self.fail("{} is not a valid UUID value".format(value), param, ctx) + + def __repr__(self): + return "UUID" + + +class File(ParamType): + """Declares a parameter to be a file for reading or writing. The file + is automatically closed once the context tears down (after the command + finished working). + + Files can be opened for reading or writing. The special value ``-`` + indicates stdin or stdout depending on the mode. + + By default, the file is opened for reading text data, but it can also be + opened in binary mode or for writing. The encoding parameter can be used + to force a specific encoding. + + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. + + Starting with Click 2.0, files can also be opened atomically in which + case all writes go into a separate file in the same folder and upon + completion the file will be moved over to the original location. This + is useful if a file regularly read by other users is modified. + + See :ref:`file-args` for more information. + """ + + name = "filename" + envvar_list_splitter = os.path.pathsep + + def __init__( + self, mode="r", encoding=None, errors="strict", lazy=None, atomic=False + ): + self.mode = mode + self.encoding = encoding + self.errors = errors + self.lazy = lazy + self.atomic = atomic + + def resolve_lazy_flag(self, value): + if self.lazy is not None: + return self.lazy + if value == "-": + return False + elif "w" in self.mode: + return True + return False + + def convert(self, value, param, ctx): + try: + if hasattr(value, "read") or hasattr(value, "write"): + return value + + lazy = self.resolve_lazy_flag(value) + + if lazy: + f = LazyFile( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + if ctx is not None: + ctx.call_on_close(f.close_intelligently) + return f + + f, should_close = open_stream( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + # If a context is provided, we automatically close the file + # at the end of the context execution (or flush out). If a + # context does not exist, it's the caller's responsibility to + # properly close the file. This for instance happens when the + # type is used with prompts. + if ctx is not None: + if should_close: + ctx.call_on_close(safecall(f.close)) + else: + ctx.call_on_close(safecall(f.flush)) + return f + except (IOError, OSError) as e: # noqa: B014 + self.fail( + "Could not open file: {}: {}".format( + filename_to_ui(value), get_streerror(e) + ), + param, + ctx, + ) + + +class Path(ParamType): + """The path type is similar to the :class:`File` type but it performs + different checks. First of all, instead of returning an open file + handle it returns just the filename. Secondly, it can perform various + basic checks about what the file or directory should be. + + .. versionchanged:: 6.0 + `allow_dash` was added. + + :param exists: if set to true, the file or directory needs to exist for + this value to be valid. If this is not required and a + file does indeed not exist, then all further checks are + silently skipped. + :param file_okay: controls if a file is a possible value. + :param dir_okay: controls if a directory is a possible value. + :param writable: if true, a writable check is performed. + :param readable: if true, a readable check is performed. + :param resolve_path: if this is true, then the path is fully resolved + before the value is passed onwards. This means + that it's absolute and symlinks are resolved. It + will not expand a tilde-prefix, as this is + supposed to be done by the shell only. + :param allow_dash: If this is set to `True`, a single dash to indicate + standard streams is permitted. + :param path_type: optionally a string type that should be used to + represent the path. The default is `None` which + means the return value will be either bytes or + unicode depending on what makes most sense given the + input data Click deals with. + """ + + envvar_list_splitter = os.path.pathsep + + def __init__( + self, + exists=False, + file_okay=True, + dir_okay=True, + writable=False, + readable=True, + resolve_path=False, + allow_dash=False, + path_type=None, + ): + self.exists = exists + self.file_okay = file_okay + self.dir_okay = dir_okay + self.writable = writable + self.readable = readable + self.resolve_path = resolve_path + self.allow_dash = allow_dash + self.type = path_type + + if self.file_okay and not self.dir_okay: + self.name = "file" + self.path_type = "File" + elif self.dir_okay and not self.file_okay: + self.name = "directory" + self.path_type = "Directory" + else: + self.name = "path" + self.path_type = "Path" + + def coerce_path_result(self, rv): + if self.type is not None and not isinstance(rv, self.type): + if self.type is text_type: + rv = rv.decode(get_filesystem_encoding()) + else: + rv = rv.encode(get_filesystem_encoding()) + return rv + + def convert(self, value, param, ctx): + rv = value + + is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-") + + if not is_dash: + if self.resolve_path: + rv = os.path.realpath(rv) + + try: + st = os.stat(rv) + except OSError: + if not self.exists: + return self.coerce_path_result(rv) + self.fail( + "{} '{}' does not exist.".format( + self.path_type, filename_to_ui(value) + ), + param, + ctx, + ) + + if not self.file_okay and stat.S_ISREG(st.st_mode): + self.fail( + "{} '{}' is a file.".format(self.path_type, filename_to_ui(value)), + param, + ctx, + ) + if not self.dir_okay and stat.S_ISDIR(st.st_mode): + self.fail( + "{} '{}' is a directory.".format( + self.path_type, filename_to_ui(value) + ), + param, + ctx, + ) + if self.writable and not os.access(value, os.W_OK): + self.fail( + "{} '{}' is not writable.".format( + self.path_type, filename_to_ui(value) + ), + param, + ctx, + ) + if self.readable and not os.access(value, os.R_OK): + self.fail( + "{} '{}' is not readable.".format( + self.path_type, filename_to_ui(value) + ), + param, + ctx, + ) + + return self.coerce_path_result(rv) + + +class Tuple(CompositeParamType): + """The default behavior of Click is to apply a type on a value directly. + This works well in most cases, except for when `nargs` is set to a fixed + count and different types should be used for different items. In this + case the :class:`Tuple` type can be used. This type can only be used + if `nargs` is set to a fixed number. + + For more information see :ref:`tuple-type`. + + This can be selected by using a Python tuple literal as a type. + + :param types: a list of types that should be used for the tuple items. + """ + + def __init__(self, types): + self.types = [convert_type(ty) for ty in types] + + @property + def name(self): + return "<{}>".format(" ".join(ty.name for ty in self.types)) + + @property + def arity(self): + return len(self.types) + + def convert(self, value, param, ctx): + if len(value) != len(self.types): + raise TypeError( + "It would appear that nargs is set to conflict with the" + " composite type arity." + ) + return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value)) + + +def convert_type(ty, default=None): + """Converts a callable or python type into the most appropriate + param type. + """ + guessed_type = False + if ty is None and default is not None: + if isinstance(default, tuple): + ty = tuple(map(type, default)) + else: + ty = type(default) + guessed_type = True + + if isinstance(ty, tuple): + return Tuple(ty) + if isinstance(ty, ParamType): + return ty + if ty is text_type or ty is str or ty is None: + return STRING + if ty is int: + return INT + # Booleans are only okay if not guessed. This is done because for + # flags the default value is actually a bit of a lie in that it + # indicates which of the flags is the one we want. See get_default() + # for more information. + if ty is bool and not guessed_type: + return BOOL + if ty is float: + return FLOAT + if guessed_type: + return STRING + + # Catch a common mistake + if __debug__: + try: + if issubclass(ty, ParamType): + raise AssertionError( + "Attempted to use an uninstantiated parameter type ({}).".format(ty) + ) + except TypeError: + pass + return FuncParamType(ty) + + +#: A dummy parameter type that just does nothing. From a user's +#: perspective this appears to just be the same as `STRING` but internally +#: no string conversion takes place. This is necessary to achieve the +#: same bytes/unicode behavior on Python 2/3 in situations where you want +#: to not convert argument types. This is usually useful when working +#: with file paths as they can appear in bytes and unicode. +#: +#: For path related uses the :class:`Path` type is a better choice but +#: there are situations where an unprocessed type is useful which is why +#: it is is provided. +#: +#: .. versionadded:: 4.0 +UNPROCESSED = UnprocessedParamType() + +#: A unicode string parameter type which is the implicit default. This +#: can also be selected by using ``str`` as type. +STRING = StringParamType() + +#: An integer parameter. This can also be selected by using ``int`` as +#: type. +INT = IntParamType() + +#: A floating point value parameter. This can also be selected by using +#: ``float`` as type. +FLOAT = FloatParamType() + +#: A boolean parameter. This is the default for boolean flags. This can +#: also be selected by using ``bool`` as a type. +BOOL = BoolParamType() + +#: A UUID parameter. +UUID = UUIDParameterType() diff --git a/mixly/tools/python/click/utils.py b/mixly/tools/python/click/utils.py new file mode 100644 index 00000000..79265e73 --- /dev/null +++ b/mixly/tools/python/click/utils.py @@ -0,0 +1,455 @@ +import os +import sys + +from ._compat import _default_text_stderr +from ._compat import _default_text_stdout +from ._compat import auto_wrap_for_ansi +from ._compat import binary_streams +from ._compat import filename_to_ui +from ._compat import get_filesystem_encoding +from ._compat import get_streerror +from ._compat import is_bytes +from ._compat import open_stream +from ._compat import PY2 +from ._compat import should_strip_ansi +from ._compat import string_types +from ._compat import strip_ansi +from ._compat import text_streams +from ._compat import text_type +from ._compat import WIN +from .globals import resolve_color_default + +if not PY2: + from ._compat import _find_binary_writer +elif WIN: + from ._winconsole import _get_windows_argv + from ._winconsole import _hash_py_argv + from ._winconsole import _initial_argv_hash + +echo_native_types = string_types + (bytes, bytearray) + + +def _posixify(name): + return "-".join(name.split()).lower() + + +def safecall(func): + """Wraps a function so that it swallows exceptions.""" + + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: + pass + + return wrapper + + +def make_str(value): + """Converts a value into a valid string.""" + if isinstance(value, bytes): + try: + return value.decode(get_filesystem_encoding()) + except UnicodeError: + return value.decode("utf-8", "replace") + return text_type(value) + + +def make_default_short_help(help, max_length=45): + """Return a condensed version of help string.""" + words = help.split() + total_length = 0 + result = [] + done = False + + for word in words: + if word[-1:] == ".": + done = True + new_length = 1 + len(word) if result else len(word) + if total_length + new_length > max_length: + result.append("...") + done = True + else: + if result: + result.append(" ") + result.append(word) + if done: + break + total_length += new_length + + return "".join(result) + + +class LazyFile(object): + """A lazy file works like a regular file but it does not fully open + the file but it does perform some basic checks early to see if the + filename parameter does make sense. This is useful for safely opening + files for writing. + """ + + def __init__( + self, filename, mode="r", encoding=None, errors="strict", atomic=False + ): + self.name = filename + self.mode = mode + self.encoding = encoding + self.errors = errors + self.atomic = atomic + + if filename == "-": + self._f, self.should_close = open_stream(filename, mode, encoding, errors) + else: + if "r" in mode: + # Open and close the file in case we're opening it for + # reading so that we can catch at least some errors in + # some cases early. + open(filename, mode).close() + self._f = None + self.should_close = True + + def __getattr__(self, name): + return getattr(self.open(), name) + + def __repr__(self): + if self._f is not None: + return repr(self._f) + return "".format(self.name, self.mode) + + def open(self): + """Opens the file if it's not yet open. This call might fail with + a :exc:`FileError`. Not handling this error will produce an error + that Click shows. + """ + if self._f is not None: + return self._f + try: + rv, self.should_close = open_stream( + self.name, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + except (IOError, OSError) as e: # noqa: E402 + from .exceptions import FileError + + raise FileError(self.name, hint=get_streerror(e)) + self._f = rv + return rv + + def close(self): + """Closes the underlying file, no matter what.""" + if self._f is not None: + self._f.close() + + def close_intelligently(self): + """This function only closes the file if it was opened by the lazy + file wrapper. For instance this will never close stdin. + """ + if self.should_close: + self.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + self.close_intelligently() + + def __iter__(self): + self.open() + return iter(self._f) + + +class KeepOpenFile(object): + def __init__(self, file): + self._file = file + + def __getattr__(self, name): + return getattr(self._file, name) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + pass + + def __repr__(self): + return repr(self._file) + + def __iter__(self): + return iter(self._file) + + +def echo(message=None, file=None, nl=True, err=False, color=None): + """Prints a message plus a newline to the given file or stdout. On + first sight, this looks like the print function, but it has improved + support for handling Unicode and binary data that does not fail no + matter how badly configured the system is. + + Primarily it means that you can print binary data as well as Unicode + data on both 2.x and 3.x to the given file in the most appropriate way + possible. This is a very carefree function in that it will try its + best to not fail. As of Click 6.0 this includes support for unicode + output on the Windows console. + + In addition to that, if `colorama`_ is installed, the echo function will + also support clever handling of ANSI codes. Essentially it will then + do the following: + + - add transparent handling of ANSI color codes on Windows. + - hide ANSI codes automatically if the destination file is not a + terminal. + + .. _colorama: https://pypi.org/project/colorama/ + + .. versionchanged:: 6.0 + As of Click 6.0 the echo function will properly support unicode + output on the windows console. Not that click does not modify + the interpreter in any way which means that `sys.stdout` or the + print statement or function will still not provide unicode support. + + .. versionchanged:: 2.0 + Starting with version 2.0 of Click, the echo function will work + with colorama if it's installed. + + .. versionadded:: 3.0 + The `err` parameter was added. + + .. versionchanged:: 4.0 + Added the `color` flag. + + :param message: the message to print + :param file: the file to write to (defaults to ``stdout``) + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``. This is faster and easier than calling + :func:`get_text_stderr` yourself. + :param nl: if set to `True` (the default) a newline is printed afterwards. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. + """ + if file is None: + if err: + file = _default_text_stderr() + else: + file = _default_text_stdout() + + # Convert non bytes/text into the native string type. + if message is not None and not isinstance(message, echo_native_types): + message = text_type(message) + + if nl: + message = message or u"" + if isinstance(message, text_type): + message += u"\n" + else: + message += b"\n" + + # If there is a message, and we're in Python 3, and the value looks + # like bytes, we manually need to find the binary stream and write the + # message in there. This is done separately so that most stream + # types will work as you would expect. Eg: you can write to StringIO + # for other cases. + if message and not PY2 and is_bytes(message): + binary_file = _find_binary_writer(file) + if binary_file is not None: + file.flush() + binary_file.write(message) + binary_file.flush() + return + + # ANSI-style support. If there is no message or we are dealing with + # bytes nothing is happening. If we are connected to a file we want + # to strip colors. If we are on windows we either wrap the stream + # to strip the color or we use the colorama support to translate the + # ansi codes to API calls. + if message and not is_bytes(message): + color = resolve_color_default(color) + if should_strip_ansi(file, color): + message = strip_ansi(message) + elif WIN: + if auto_wrap_for_ansi is not None: + file = auto_wrap_for_ansi(file) + elif not color: + message = strip_ansi(message) + + if message: + file.write(message) + file.flush() + + +def get_binary_stream(name): + """Returns a system stream for byte processing. This essentially + returns the stream from the sys module with the given name but it + solves some compatibility issues between different Python versions. + Primarily this function is necessary for getting binary streams on + Python 3. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + """ + opener = binary_streams.get(name) + if opener is None: + raise TypeError("Unknown standard stream '{}'".format(name)) + return opener() + + +def get_text_stream(name, encoding=None, errors="strict"): + """Returns a system stream for text processing. This usually returns + a wrapped stream around a binary stream returned from + :func:`get_binary_stream` but it also can take shortcuts on Python 3 + for already correctly configured streams. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + :param encoding: overrides the detected default encoding. + :param errors: overrides the default error mode. + """ + opener = text_streams.get(name) + if opener is None: + raise TypeError("Unknown standard stream '{}'".format(name)) + return opener(encoding, errors) + + +def open_file( + filename, mode="r", encoding=None, errors="strict", lazy=False, atomic=False +): + """This is similar to how the :class:`File` works but for manual + usage. Files are opened non lazy by default. This can open regular + files as well as stdin/stdout if ``'-'`` is passed. + + If stdin/stdout is returned the stream is wrapped so that the context + manager will not close the stream accidentally. This makes it possible + to always use the function like this without having to worry to + accidentally close a standard stream:: + + with open_file(filename) as f: + ... + + .. versionadded:: 3.0 + + :param filename: the name of the file to open (or ``'-'`` for stdin/stdout). + :param mode: the mode in which to open the file. + :param encoding: the encoding to use. + :param errors: the error handling for this file. + :param lazy: can be flipped to true to open the file lazily. + :param atomic: in atomic mode writes go into a temporary file and it's + moved on close. + """ + if lazy: + return LazyFile(filename, mode, encoding, errors, atomic=atomic) + f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) + if not should_close: + f = KeepOpenFile(f) + return f + + +def get_os_args(): + """This returns the argument part of sys.argv in the most appropriate + form for processing. What this means is that this return value is in + a format that works for Click to process but does not necessarily + correspond well to what's actually standard for the interpreter. + + On most environments the return value is ``sys.argv[:1]`` unchanged. + However if you are on Windows and running Python 2 the return value + will actually be a list of unicode strings instead because the + default behavior on that platform otherwise will not be able to + carry all possible values that sys.argv can have. + + .. versionadded:: 6.0 + """ + # We can only extract the unicode argv if sys.argv has not been + # changed since the startup of the application. + if PY2 and WIN and _initial_argv_hash == _hash_py_argv(): + return _get_windows_argv() + return sys.argv[1:] + + +def format_filename(filename, shorten=False): + """Formats a filename for user display. The main purpose of this + function is to ensure that the filename can be displayed at all. This + will decode the filename to unicode if necessary in a way that it will + not fail. Optionally, it can shorten the filename to not include the + full path to the filename. + + :param filename: formats a filename for UI display. This will also convert + the filename into unicode without failing. + :param shorten: this optionally shortens the filename to strip of the + path that leads up to it. + """ + if shorten: + filename = os.path.basename(filename) + return filename_to_ui(filename) + + +def get_app_dir(app_name, roaming=True, force_posix=False): + r"""Returns the config folder for the application. The default behavior + is to return whatever is most appropriate for the operating system. + + To give you an idea, for an app called ``"Foo Bar"``, something like + the following folders could be returned: + + Mac OS X: + ``~/Library/Application Support/Foo Bar`` + Mac OS X (POSIX): + ``~/.foo-bar`` + Unix: + ``~/.config/foo-bar`` + Unix (POSIX): + ``~/.foo-bar`` + Win XP (roaming): + ``C:\Documents and Settings\\Local Settings\Application Data\Foo Bar`` + Win XP (not roaming): + ``C:\Documents and Settings\\Application Data\Foo Bar`` + Win 7 (roaming): + ``C:\Users\\AppData\Roaming\Foo Bar`` + Win 7 (not roaming): + ``C:\Users\\AppData\Local\Foo Bar`` + + .. versionadded:: 2.0 + + :param app_name: the application name. This should be properly capitalized + and can contain whitespace. + :param roaming: controls if the folder should be roaming or not on Windows. + Has no affect otherwise. + :param force_posix: if this is set to `True` then on any POSIX system the + folder will be stored in the home folder with a leading + dot instead of the XDG config home or darwin's + application support folder. + """ + if WIN: + key = "APPDATA" if roaming else "LOCALAPPDATA" + folder = os.environ.get(key) + if folder is None: + folder = os.path.expanduser("~") + return os.path.join(folder, app_name) + if force_posix: + return os.path.join(os.path.expanduser("~/.{}".format(_posixify(app_name)))) + if sys.platform == "darwin": + return os.path.join( + os.path.expanduser("~/Library/Application Support"), app_name + ) + return os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + _posixify(app_name), + ) + + +class PacifyFlushWrapper(object): + """This wrapper is used to catch and suppress BrokenPipeErrors resulting + from ``.flush()`` being called on broken pipe during the shutdown/final-GC + of the Python interpreter. Notably ``.flush()`` is always called on + ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any + other cleanup code, and the case where the underlying file is not a broken + pipe, all calls and attributes are proxied. + """ + + def __init__(self, wrapped): + self.wrapped = wrapped + + def flush(self): + try: + self.wrapped.flush() + except IOError as e: + import errno + + if e.errno != errno.EPIPE: + raise + + def __getattr__(self, attr): + return getattr(self.wrapped, attr) diff --git a/mixly/tools/python/dotenv/__init__.py b/mixly/tools/python/dotenv/__init__.py new file mode 100644 index 00000000..b88d9bc2 --- /dev/null +++ b/mixly/tools/python/dotenv/__init__.py @@ -0,0 +1,46 @@ +from .compat import IS_TYPE_CHECKING +from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv, dotenv_values + +if IS_TYPE_CHECKING: + from typing import Any, Optional + + +def load_ipython_extension(ipython): + # type: (Any) -> None + from .ipython import load_ipython_extension + load_ipython_extension(ipython) + + +def get_cli_string(path=None, action=None, key=None, value=None, quote=None): + # type: (Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> str + """Returns a string suitable for running as a shell script. + + Useful for converting a arguments passed to a fabric task + to be passed to a `local` or `run` command. + """ + command = ['dotenv'] + if quote: + command.append('-q %s' % quote) + if path: + command.append('-f %s' % path) + if action: + command.append(action) + if key: + command.append(key) + if value: + if ' ' in value: + command.append('"%s"' % value) + else: + command.append(value) + + return ' '.join(command).strip() + + +__all__ = ['get_cli_string', + 'load_dotenv', + 'dotenv_values', + 'get_key', + 'set_key', + 'unset_key', + 'find_dotenv', + 'load_ipython_extension'] diff --git a/mixly/tools/python/dotenv/cli.py b/mixly/tools/python/dotenv/cli.py new file mode 100644 index 00000000..bb96c023 --- /dev/null +++ b/mixly/tools/python/dotenv/cli.py @@ -0,0 +1,174 @@ +import os +import sys +from subprocess import Popen + +try: + import click +except ImportError: + sys.stderr.write('It seems python-dotenv is not installed with cli option. \n' + 'Run pip install "python-dotenv[cli]" to fix this.') + sys.exit(1) + +from .compat import IS_TYPE_CHECKING, to_env +from .main import dotenv_values, get_key, set_key, unset_key +from .version import __version__ + +if IS_TYPE_CHECKING: + from typing import Any, List, Dict + + +@click.group() +@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'), + type=click.Path(file_okay=True), + help="Location of the .env file, defaults to .env file in current working directory.") +@click.option('-q', '--quote', default='always', + type=click.Choice(['always', 'never', 'auto']), + help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.") +@click.option('-e', '--export', default=False, + type=click.BOOL, + help="Whether to write the dot file as an executable bash script.") +@click.version_option(version=__version__) +@click.pass_context +def cli(ctx, file, quote, export): + # type: (click.Context, Any, Any, Any) -> None + '''This script is used to set, get or unset values from a .env file.''' + ctx.obj = {} + ctx.obj['QUOTE'] = quote + ctx.obj['EXPORT'] = export + ctx.obj['FILE'] = file + + +@cli.command() +@click.pass_context +def list(ctx): + # type: (click.Context) -> None + '''Display all the stored key/value.''' + file = ctx.obj['FILE'] + if not os.path.isfile(file): + raise click.BadParameter( + 'Path "%s" does not exist.' % (file), + ctx=ctx + ) + dotenv_as_dict = dotenv_values(file) + for k, v in dotenv_as_dict.items(): + click.echo('%s=%s' % (k, v)) + + +@cli.command() +@click.pass_context +@click.argument('key', required=True) +@click.argument('value', required=True) +def set(ctx, key, value): + # type: (click.Context, Any, Any) -> None + '''Store the given key/value.''' + file = ctx.obj['FILE'] + quote = ctx.obj['QUOTE'] + export = ctx.obj['EXPORT'] + success, key, value = set_key(file, key, value, quote, export) + if success: + click.echo('%s=%s' % (key, value)) + else: + exit(1) + + +@cli.command() +@click.pass_context +@click.argument('key', required=True) +def get(ctx, key): + # type: (click.Context, Any) -> None + '''Retrieve the value for the given key.''' + file = ctx.obj['FILE'] + if not os.path.isfile(file): + raise click.BadParameter( + 'Path "%s" does not exist.' % (file), + ctx=ctx + ) + stored_value = get_key(file, key) + if stored_value: + click.echo(stored_value) + else: + exit(1) + + +@cli.command() +@click.pass_context +@click.argument('key', required=True) +def unset(ctx, key): + # type: (click.Context, Any) -> None + '''Removes the given key.''' + file = ctx.obj['FILE'] + quote = ctx.obj['QUOTE'] + success, key = unset_key(file, key, quote) + if success: + click.echo("Successfully removed %s" % key) + else: + exit(1) + + +@cli.command(context_settings={'ignore_unknown_options': True}) +@click.pass_context +@click.option( + "--override/--no-override", + default=True, + help="Override variables from the environment file with those from the .env file.", +) +@click.argument('commandline', nargs=-1, type=click.UNPROCESSED) +def run(ctx, override, commandline): + # type: (click.Context, bool, List[str]) -> None + """Run command with environment variables present.""" + file = ctx.obj['FILE'] + if not os.path.isfile(file): + raise click.BadParameter( + 'Invalid value for \'-f\' "%s" does not exist.' % (file), + ctx=ctx + ) + dotenv_as_dict = { + to_env(k): to_env(v) + for (k, v) in dotenv_values(file).items() + if v is not None and (override or to_env(k) not in os.environ) + } + + if not commandline: + click.echo('No command given.') + exit(1) + ret = run_command(commandline, dotenv_as_dict) + exit(ret) + + +def run_command(command, env): + # type: (List[str], Dict[str, str]) -> int + """Run command in sub process. + + Runs the command in a sub process with the variables from `env` + added in the current environment variables. + + Parameters + ---------- + command: List[str] + The command and it's parameters + env: Dict + The additional environment variables + + Returns + ------- + int + The return code of the command + + """ + # copy the current environment variables and add the vales from + # `env` + cmd_env = os.environ.copy() + cmd_env.update(env) + + p = Popen(command, + universal_newlines=True, + bufsize=0, + shell=False, + env=cmd_env) + _, _ = p.communicate() + + return p.returncode + + +if __name__ == "__main__": + cli() diff --git a/mixly/tools/python/dotenv/compat.py b/mixly/tools/python/dotenv/compat.py new file mode 100644 index 00000000..f8089bf4 --- /dev/null +++ b/mixly/tools/python/dotenv/compat.py @@ -0,0 +1,49 @@ +import sys + +PY2 = sys.version_info[0] == 2 # type: bool + +if PY2: + from StringIO import StringIO # noqa +else: + from io import StringIO # noqa + + +def is_type_checking(): + # type: () -> bool + try: + from typing import TYPE_CHECKING + except ImportError: + return False + return TYPE_CHECKING + + +IS_TYPE_CHECKING = is_type_checking() + + +if IS_TYPE_CHECKING: + from typing import Text + + +def to_env(text): + # type: (Text) -> str + """ + Encode a string the same way whether it comes from the environment or a `.env` file. + """ + if PY2: + return text.encode(sys.getfilesystemencoding() or "utf-8") + else: + return text + + +def to_text(string): + # type: (str) -> Text + """ + Make a string Unicode if it isn't already. + + This is useful for defining raw unicode strings because `ur"foo"` isn't valid in + Python 3. + """ + if PY2: + return string.decode("utf-8") + else: + return string diff --git a/mixly/tools/python/dotenv/ipython.py b/mixly/tools/python/dotenv/ipython.py new file mode 100644 index 00000000..7f1b13d6 --- /dev/null +++ b/mixly/tools/python/dotenv/ipython.py @@ -0,0 +1,41 @@ +from __future__ import print_function + +from IPython.core.magic import Magics, line_magic, magics_class # type: ignore +from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore + parse_argstring) # type: ignore + +from .main import find_dotenv, load_dotenv + + +@magics_class +class IPythonDotEnv(Magics): + + @magic_arguments() + @argument( + '-o', '--override', action='store_true', + help="Indicate to override existing variables" + ) + @argument( + '-v', '--verbose', action='store_true', + help="Indicate function calls to be verbose" + ) + @argument('dotenv_path', nargs='?', type=str, default='.env', + help='Search in increasingly higher folders for the `dotenv_path`') + @line_magic + def dotenv(self, line): + args = parse_argstring(self.dotenv, line) + # Locate the .env file + dotenv_path = args.dotenv_path + try: + dotenv_path = find_dotenv(dotenv_path, True, True) + except IOError: + print("cannot find .env file") + return + + # Load the .env file + load_dotenv(dotenv_path, verbose=args.verbose, override=args.override) + + +def load_ipython_extension(ipython): + """Register the %dotenv magic.""" + ipython.register_magics(IPythonDotEnv) diff --git a/mixly/tools/python/dotenv/main.py b/mixly/tools/python/dotenv/main.py new file mode 100644 index 00000000..16f22d2c --- /dev/null +++ b/mixly/tools/python/dotenv/main.py @@ -0,0 +1,355 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +import io +import logging +import os +import shutil +import sys +import tempfile +from collections import OrderedDict +from contextlib import contextmanager + +from .compat import IS_TYPE_CHECKING, PY2, StringIO, to_env +from .parser import Binding, parse_stream +from .variables import parse_variables + +logger = logging.getLogger(__name__) + +if IS_TYPE_CHECKING: + from typing import (IO, Dict, Iterable, Iterator, Mapping, Optional, Text, + Tuple, Union) + if sys.version_info >= (3, 6): + _PathLike = os.PathLike + else: + _PathLike = Text + + if sys.version_info >= (3, 0): + _StringIO = StringIO + else: + _StringIO = StringIO[Text] + + +def with_warn_for_invalid_lines(mappings): + # type: (Iterator[Binding]) -> Iterator[Binding] + for mapping in mappings: + if mapping.error: + logger.warning( + "Python-dotenv could not parse statement starting at line %s", + mapping.original.line, + ) + yield mapping + + +class DotEnv(): + + def __init__(self, dotenv_path, verbose=False, encoding=None, interpolate=True, override=True): + # type: (Union[Text, _PathLike, _StringIO], bool, Union[None, Text], bool, bool) -> None + self.dotenv_path = dotenv_path # type: Union[Text,_PathLike, _StringIO] + self._dict = None # type: Optional[Dict[Text, Optional[Text]]] + self.verbose = verbose # type: bool + self.encoding = encoding # type: Union[None, Text] + self.interpolate = interpolate # type: bool + self.override = override # type: bool + + @contextmanager + def _get_stream(self): + # type: () -> Iterator[IO[Text]] + if isinstance(self.dotenv_path, StringIO): + yield self.dotenv_path + elif os.path.isfile(self.dotenv_path): + with io.open(self.dotenv_path, encoding=self.encoding) as stream: + yield stream + else: + if self.verbose: + logger.info("Python-dotenv could not find configuration file %s.", self.dotenv_path or '.env') + yield StringIO('') + + def dict(self): + # type: () -> Dict[Text, Optional[Text]] + """Return dotenv as dict""" + if self._dict: + return self._dict + + raw_values = self.parse() + + if self.interpolate: + self._dict = OrderedDict(resolve_variables(raw_values, override=self.override)) + else: + self._dict = OrderedDict(raw_values) + + return self._dict + + def parse(self): + # type: () -> Iterator[Tuple[Text, Optional[Text]]] + with self._get_stream() as stream: + for mapping in with_warn_for_invalid_lines(parse_stream(stream)): + if mapping.key is not None: + yield mapping.key, mapping.value + + def set_as_environment_variables(self): + # type: () -> bool + """ + Load the current dotenv as system environment variable. + """ + for k, v in self.dict().items(): + if k in os.environ and not self.override: + continue + if v is not None: + os.environ[to_env(k)] = to_env(v) + + return True + + def get(self, key): + # type: (Text) -> Optional[Text] + """ + """ + data = self.dict() + + if key in data: + return data[key] + + if self.verbose: + logger.warning("Key %s not found in %s.", key, self.dotenv_path) + + return None + + +def get_key(dotenv_path, key_to_get): + # type: (Union[Text, _PathLike], Text) -> Optional[Text] + """ + Gets the value of a given key from the given .env + + If the .env path given doesn't exist, fails + """ + return DotEnv(dotenv_path, verbose=True).get(key_to_get) + + +@contextmanager +def rewrite(path): + # type: (_PathLike) -> Iterator[Tuple[IO[Text], IO[Text]]] + try: + if not os.path.isfile(path): + with io.open(path, "w+") as source: + source.write("") + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest: + with io.open(path) as source: + yield (source, dest) # type: ignore + except BaseException: + if os.path.isfile(dest.name): + os.unlink(dest.name) + raise + else: + shutil.move(dest.name, path) + + +def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always", export=False): + # type: (_PathLike, Text, Text, Text, bool) -> Tuple[Optional[bool], Text, Text] + """ + Adds or Updates a key/value to the given .env + + If the .env path given doesn't exist, fails instead of risking creating + an orphan .env somewhere in the filesystem + """ + value_to_set = value_to_set.strip("'").strip('"') + + if " " in value_to_set: + quote_mode = "always" + + if quote_mode == "always": + value_out = '"{}"'.format(value_to_set.replace('"', '\\"')) + else: + value_out = value_to_set + if export: + line_out = 'export {}={}\n'.format(key_to_set, value_out) + else: + line_out = "{}={}\n".format(key_to_set, value_out) + + with rewrite(dotenv_path) as (source, dest): + replaced = False + for mapping in with_warn_for_invalid_lines(parse_stream(source)): + if mapping.key == key_to_set: + dest.write(line_out) + replaced = True + else: + dest.write(mapping.original.string) + if not replaced: + dest.write(line_out) + + return True, key_to_set, value_to_set + + +def unset_key(dotenv_path, key_to_unset, quote_mode="always"): + # type: (_PathLike, Text, Text) -> Tuple[Optional[bool], Text] + """ + Removes a given key from the given .env + + If the .env path given doesn't exist, fails + If the given key doesn't exist in the .env, fails + """ + if not os.path.exists(dotenv_path): + logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path) + return None, key_to_unset + + removed = False + with rewrite(dotenv_path) as (source, dest): + for mapping in with_warn_for_invalid_lines(parse_stream(source)): + if mapping.key == key_to_unset: + removed = True + else: + dest.write(mapping.original.string) + + if not removed: + logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path) + return None, key_to_unset + + return removed, key_to_unset + + +def resolve_variables(values, override): + # type: (Iterable[Tuple[Text, Optional[Text]]], bool) -> Mapping[Text, Optional[Text]] + + new_values = {} # type: Dict[Text, Optional[Text]] + + for (name, value) in values: + if value is None: + result = None + else: + atoms = parse_variables(value) + env = {} # type: Dict[Text, Optional[Text]] + if override: + env.update(os.environ) # type: ignore + env.update(new_values) + else: + env.update(new_values) + env.update(os.environ) # type: ignore + result = "".join(atom.resolve(env) for atom in atoms) + + new_values[name] = result + + return new_values + + +def _walk_to_root(path): + # type: (Text) -> Iterator[Text] + """ + Yield directories starting from the given directory up to the root + """ + if not os.path.exists(path): + raise IOError('Starting path not found') + + if os.path.isfile(path): + path = os.path.dirname(path) + + last_dir = None + current_dir = os.path.abspath(path) + while last_dir != current_dir: + yield current_dir + parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir)) + last_dir, current_dir = current_dir, parent_dir + + +def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False): + # type: (Text, bool, bool) -> Text + """ + Search in increasingly higher folders for the given file + + Returns path to the file if found, or an empty string otherwise + """ + + def _is_interactive(): + """ Decide whether this is running in a REPL or IPython notebook """ + main = __import__('__main__', None, None, fromlist=['__file__']) + return not hasattr(main, '__file__') + + if usecwd or _is_interactive() or getattr(sys, 'frozen', False): + # Should work without __file__, e.g. in REPL or IPython notebook. + path = os.getcwd() + else: + # will work for .py files + frame = sys._getframe() + # find first frame that is outside of this file + if PY2 and not __file__.endswith('.py'): + # in Python2 __file__ extension could be .pyc or .pyo (this doesn't account + # for edge case of Python compiled for non-standard extension) + current_file = __file__.rsplit('.', 1)[0] + '.py' + else: + current_file = __file__ + + while frame.f_code.co_filename == current_file: + assert frame.f_back is not None + frame = frame.f_back + frame_filename = frame.f_code.co_filename + path = os.path.dirname(os.path.abspath(frame_filename)) + + for dirname in _walk_to_root(path): + check_path = os.path.join(dirname, filename) + if os.path.isfile(check_path): + return check_path + + if raise_error_if_not_found: + raise IOError('File not found') + + return '' + + +def load_dotenv( + dotenv_path=None, + stream=None, + verbose=False, + override=False, + interpolate=True, + encoding="utf-8", +): + # type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, bool, Optional[Text]) -> bool # noqa + """Parse a .env file and then load all the variables found as environment variables. + + - *dotenv_path*: absolute or relative path to .env file. + - *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`. + - *verbose*: whether to output a warning the .env file is missing. Defaults to + `False`. + - *override*: whether to override the system environment variables with the variables + in `.env` file. Defaults to `False`. + - *encoding*: encoding to be used to read the file. + + If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file. + """ + f = dotenv_path or stream or find_dotenv() + dotenv = DotEnv( + f, + verbose=verbose, + interpolate=interpolate, + override=override, + encoding=encoding, + ) + return dotenv.set_as_environment_variables() + + +def dotenv_values( + dotenv_path=None, + stream=None, + verbose=False, + interpolate=True, + encoding="utf-8", +): + # type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, Optional[Text]) -> Dict[Text, Optional[Text]] # noqa: E501 + """ + Parse a .env file and return its content as a dict. + + - *dotenv_path*: absolute or relative path to .env file. + - *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`. + - *verbose*: whether to output a warning the .env file is missing. Defaults to + `False`. + in `.env` file. Defaults to `False`. + - *encoding*: encoding to be used to read the file. + + If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file. + """ + f = dotenv_path or stream or find_dotenv() + return DotEnv( + f, + verbose=verbose, + interpolate=interpolate, + override=True, + encoding=encoding, + ).dict() diff --git a/mixly/tools/python/dotenv/parser.py b/mixly/tools/python/dotenv/parser.py new file mode 100644 index 00000000..5cb1cdfa --- /dev/null +++ b/mixly/tools/python/dotenv/parser.py @@ -0,0 +1,231 @@ +import codecs +import re + +from .compat import IS_TYPE_CHECKING, to_text + +if IS_TYPE_CHECKING: + from typing import ( # noqa:F401 + IO, Iterator, Match, NamedTuple, Optional, Pattern, Sequence, Text, + Tuple + ) + + +def make_regex(string, extra_flags=0): + # type: (str, int) -> Pattern[Text] + return re.compile(to_text(string), re.UNICODE | extra_flags) + + +_newline = make_regex(r"(\r\n|\n|\r)") +_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE) +_whitespace = make_regex(r"[^\S\r\n]*") +_export = make_regex(r"(?:export[^\S\r\n]+)?") +_single_quoted_key = make_regex(r"'([^']+)'") +_unquoted_key = make_regex(r"([^=\#\s]+)") +_equal_sign = make_regex(r"(=[^\S\r\n]*)") +_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'") +_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"') +_unquoted_value = make_regex(r"([^\r\n]*)") +_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?") +_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)") +_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?") +_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]") +_single_quote_escapes = make_regex(r"\\[\\']") + + +try: + # this is necessary because we only import these from typing + # when we are type checking, and the linter is upset if we + # re-import + import typing + + Original = typing.NamedTuple( + "Original", + [ + ("string", typing.Text), + ("line", int), + ], + ) + + Binding = typing.NamedTuple( + "Binding", + [ + ("key", typing.Optional[typing.Text]), + ("value", typing.Optional[typing.Text]), + ("original", Original), + ("error", bool), + ], + ) +except (ImportError, AttributeError): + from collections import namedtuple + Original = namedtuple( # type: ignore + "Original", + [ + "string", + "line", + ], + ) + Binding = namedtuple( # type: ignore + "Binding", + [ + "key", + "value", + "original", + "error", + ], + ) + + +class Position: + def __init__(self, chars, line): + # type: (int, int) -> None + self.chars = chars + self.line = line + + @classmethod + def start(cls): + # type: () -> Position + return cls(chars=0, line=1) + + def set(self, other): + # type: (Position) -> None + self.chars = other.chars + self.line = other.line + + def advance(self, string): + # type: (Text) -> None + self.chars += len(string) + self.line += len(re.findall(_newline, string)) + + +class Error(Exception): + pass + + +class Reader: + def __init__(self, stream): + # type: (IO[Text]) -> None + self.string = stream.read() + self.position = Position.start() + self.mark = Position.start() + + def has_next(self): + # type: () -> bool + return self.position.chars < len(self.string) + + def set_mark(self): + # type: () -> None + self.mark.set(self.position) + + def get_marked(self): + # type: () -> Original + return Original( + string=self.string[self.mark.chars:self.position.chars], + line=self.mark.line, + ) + + def peek(self, count): + # type: (int) -> Text + return self.string[self.position.chars:self.position.chars + count] + + def read(self, count): + # type: (int) -> Text + result = self.string[self.position.chars:self.position.chars + count] + if len(result) < count: + raise Error("read: End of string") + self.position.advance(result) + return result + + def read_regex(self, regex): + # type: (Pattern[Text]) -> Sequence[Text] + match = regex.match(self.string, self.position.chars) + if match is None: + raise Error("read_regex: Pattern not found") + self.position.advance(self.string[match.start():match.end()]) + return match.groups() + + +def decode_escapes(regex, string): + # type: (Pattern[Text], Text) -> Text + def decode_match(match): + # type: (Match[Text]) -> Text + return codecs.decode(match.group(0), 'unicode-escape') # type: ignore + + return regex.sub(decode_match, string) + + +def parse_key(reader): + # type: (Reader) -> Optional[Text] + char = reader.peek(1) + if char == "#": + return None + elif char == "'": + (key,) = reader.read_regex(_single_quoted_key) + else: + (key,) = reader.read_regex(_unquoted_key) + return key + + +def parse_unquoted_value(reader): + # type: (Reader) -> Text + (part,) = reader.read_regex(_unquoted_value) + return re.sub(r"\s+#.*", "", part).rstrip() + + +def parse_value(reader): + # type: (Reader) -> Text + char = reader.peek(1) + if char == u"'": + (value,) = reader.read_regex(_single_quoted_value) + return decode_escapes(_single_quote_escapes, value) + elif char == u'"': + (value,) = reader.read_regex(_double_quoted_value) + return decode_escapes(_double_quote_escapes, value) + elif char in (u"", u"\n", u"\r"): + return u"" + else: + return parse_unquoted_value(reader) + + +def parse_binding(reader): + # type: (Reader) -> Binding + reader.set_mark() + try: + reader.read_regex(_multiline_whitespace) + if not reader.has_next(): + return Binding( + key=None, + value=None, + original=reader.get_marked(), + error=False, + ) + reader.read_regex(_export) + key = parse_key(reader) + reader.read_regex(_whitespace) + if reader.peek(1) == "=": + reader.read_regex(_equal_sign) + value = parse_value(reader) # type: Optional[Text] + else: + value = None + reader.read_regex(_comment) + reader.read_regex(_end_of_line) + return Binding( + key=key, + value=value, + original=reader.get_marked(), + error=False, + ) + except Error: + reader.read_regex(_rest_of_line) + return Binding( + key=None, + value=None, + original=reader.get_marked(), + error=True, + ) + + +def parse_stream(stream): + # type: (IO[Text]) -> Iterator[Binding] + reader = Reader(stream) + while reader.has_next(): + yield parse_binding(reader) diff --git a/mixly/tools/python/dotenv/py.typed b/mixly/tools/python/dotenv/py.typed new file mode 100644 index 00000000..7632ecf7 --- /dev/null +++ b/mixly/tools/python/dotenv/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 diff --git a/mixly/tools/python/dotenv/variables.py b/mixly/tools/python/dotenv/variables.py new file mode 100644 index 00000000..4828dfc2 --- /dev/null +++ b/mixly/tools/python/dotenv/variables.py @@ -0,0 +1,106 @@ +import re +from abc import ABCMeta + +from .compat import IS_TYPE_CHECKING + +if IS_TYPE_CHECKING: + from typing import Iterator, Mapping, Optional, Pattern, Text + + +_posix_variable = re.compile( + r""" + \$\{ + (?P[^\}:]*) + (?::- + (?P[^\}]*) + )? + \} + """, + re.VERBOSE, +) # type: Pattern[Text] + + +class Atom(): + __metaclass__ = ABCMeta + + def __ne__(self, other): + # type: (object) -> bool + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + return not result + + def resolve(self, env): + # type: (Mapping[Text, Optional[Text]]) -> Text + raise NotImplementedError + + +class Literal(Atom): + def __init__(self, value): + # type: (Text) -> None + self.value = value + + def __repr__(self): + # type: () -> str + return "Literal(value={})".format(self.value) + + def __eq__(self, other): + # type: (object) -> bool + if not isinstance(other, self.__class__): + return NotImplemented + return self.value == other.value + + def __hash__(self): + # type: () -> int + return hash((self.__class__, self.value)) + + def resolve(self, env): + # type: (Mapping[Text, Optional[Text]]) -> Text + return self.value + + +class Variable(Atom): + def __init__(self, name, default): + # type: (Text, Optional[Text]) -> None + self.name = name + self.default = default + + def __repr__(self): + # type: () -> str + return "Variable(name={}, default={})".format(self.name, self.default) + + def __eq__(self, other): + # type: (object) -> bool + if not isinstance(other, self.__class__): + return NotImplemented + return (self.name, self.default) == (other.name, other.default) + + def __hash__(self): + # type: () -> int + return hash((self.__class__, self.name, self.default)) + + def resolve(self, env): + # type: (Mapping[Text, Optional[Text]]) -> Text + default = self.default if self.default is not None else "" + result = env.get(self.name, default) + return result if result is not None else "" + + +def parse_variables(value): + # type: (Text) -> Iterator[Atom] + cursor = 0 + + for match in _posix_variable.finditer(value): + (start, end) = match.span() + name = match.groupdict()["name"] + default = match.groupdict()["default"] + + if start > cursor: + yield Literal(value=value[cursor:start]) + + yield Variable(name=name, default=default) + cursor = end + + length = len(value) + if cursor < length: + yield Literal(value=value[cursor:length]) diff --git a/mixly/tools/python/dotenv/version.py b/mixly/tools/python/dotenv/version.py new file mode 100644 index 00000000..c6eae9f8 --- /dev/null +++ b/mixly/tools/python/dotenv/version.py @@ -0,0 +1 @@ +__version__ = "0.17.1" diff --git a/mixly/tools/python/esptool/__init__.py b/mixly/tools/python/esptool/__init__.py new file mode 100644 index 00000000..7f919bc3 --- /dev/null +++ b/mixly/tools/python/esptool/__init__.py @@ -0,0 +1,1337 @@ +# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton, +# Espressif Systems (Shanghai) CO LTD, other contributors as noted. +# +# SPDX-License-Identifier: GPL-2.0-or-later +# PYTHON_ARGCOMPLETE_OK +__all__ = [ + "chip_id", + "detect_chip", + "dump_mem", + "elf2image", + "erase_flash", + "erase_region", + "flash_id", + "get_security_info", + "image_info", + "load_ram", + "make_image", + "merge_bin", + "read_flash", + "read_flash_status", + "read_mac", + "read_mem", + "run", + "verify_flash", + "version", + "write_flash", + "write_flash_status", + "write_mem", +] + +__version__ = "4.8.1" + +import argparse +import inspect +import os +import shlex +import sys +import time +import traceback + +from esptool.bin_image import intel_hex_to_bin +from esptool.cmds import ( + DETECTED_FLASH_SIZES, + chip_id, + detect_chip, + detect_flash_size, + dump_mem, + elf2image, + erase_flash, + erase_region, + flash_id, + read_flash_sfdp, + get_security_info, + image_info, + load_ram, + make_image, + merge_bin, + read_flash, + read_flash_status, + read_mac, + read_mem, + run, + verify_flash, + version, + write_flash, + write_flash_status, + write_mem, +) +from esptool.config import load_config_file +from esptool.loader import ( + DEFAULT_CONNECT_ATTEMPTS, + DEFAULT_OPEN_PORT_ATTEMPTS, + StubFlasher, + ESPLoader, + list_ports, +) +from esptool.targets import CHIP_DEFS, CHIP_LIST, ESP32ROM +from esptool.util import ( + FatalError, + NotImplementedInROMError, + flash_size_bytes, + strip_chip_name, +) +from itertools import chain, cycle, repeat + +import serial + + +def main(argv=None, esp=None): + """ + Main function for esptool + + argv - Optional override for default arguments parsing (that uses sys.argv), + can be a list of custom arguments as strings. Arguments and their values + need to be added as individual items to the list + e.g. "-b 115200" thus becomes ['-b', '115200']. + + esp - Optional override of the connected device previously + returned by get_default_connected_device() + """ + + external_esp = esp is not None + + parser = argparse.ArgumentParser( + description="esptool.py v%s - Espressif chips ROM Bootloader Utility" + % __version__, + prog="esptool", + ) + + parser.add_argument( + "--chip", + "-c", + help="Target chip type", + type=strip_chip_name, + choices=["auto"] + CHIP_LIST, + default=os.environ.get("ESPTOOL_CHIP", "auto"), + ) + + parser.add_argument( + "--port", + "-p", + help="Serial port device", + default=os.environ.get("ESPTOOL_PORT", None), + ) + + parser.add_argument( + "--baud", + "-b", + help="Serial port baud rate used when flashing/reading", + type=arg_auto_int, + default=os.environ.get("ESPTOOL_BAUD", ESPLoader.ESP_ROM_BAUD), + ) + + parser.add_argument( + "--port-filter", + action="append", + help="Serial port device filter, can be vid=NUMBER, pid=NUMBER, name=SUBSTRING", + type=str, + default=[], + ) + + parser.add_argument( + "--before", + help="What to do before connecting to the chip", + choices=["default_reset", "usb_reset", "no_reset", "no_reset_no_sync"], + default=os.environ.get("ESPTOOL_BEFORE", "default_reset"), + ) + + parser.add_argument( + "--after", + "-a", + help="What to do after esptool.py is finished", + choices=["hard_reset", "soft_reset", "no_reset", "no_reset_stub"], + default=os.environ.get("ESPTOOL_AFTER", "hard_reset"), + ) + + parser.add_argument( + "--no-stub", + help="Disable launching the flasher stub, only talk to ROM bootloader. " + "Some features will not be available.", + action="store_true", + ) + + # --stub-version can be set with --no-stub so the tests wouldn't fail if this option is implied globally + parser.add_argument( + "--stub-version", + default=os.environ.get("ESPTOOL_STUB_VERSION", StubFlasher.STUB_SUBDIRS[0]), + choices=StubFlasher.STUB_SUBDIRS, + # not a public option and is not subject to the semantic versioning policy + help=argparse.SUPPRESS, + ) + + parser.add_argument( + "--trace", + "-t", + help="Enable trace-level output of esptool.py interactions.", + action="store_true", + ) + + parser.add_argument( + "--override-vddsdio", + help="Override ESP32 VDDSDIO internal voltage regulator (use with care)", + choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES, + nargs="?", + ) + + parser.add_argument( + "--connect-attempts", + help=( + "Number of attempts to connect, negative or 0 for infinite. " + "Default: %d." % DEFAULT_CONNECT_ATTEMPTS + ), + type=int, + default=os.environ.get("ESPTOOL_CONNECT_ATTEMPTS", DEFAULT_CONNECT_ATTEMPTS), + ) + + subparsers = parser.add_subparsers( + dest="operation", help="Run esptool.py {command} -h for additional help" + ) + + def add_spi_connection_arg(parent): + parent.add_argument( + "--spi-connection", + "-sc", + help="Override default SPI Flash connection. " + "Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers " + "to use for SPI flash (CLK,Q,D,HD,CS). Not supported with ESP8266.", + action=SpiConnectionAction, + ) + + parser_load_ram = subparsers.add_parser( + "load_ram", help="Download an image to RAM and execute" + ) + parser_load_ram.add_argument( + "filename", help="Firmware image", action=AutoHex2BinAction + ) + + parser_dump_mem = subparsers.add_parser( + "dump_mem", help="Dump arbitrary memory to disk" + ) + parser_dump_mem.add_argument("address", help="Base address", type=arg_auto_int) + parser_dump_mem.add_argument( + "size", help="Size of region to dump", type=arg_auto_int + ) + parser_dump_mem.add_argument("filename", help="Name of binary dump") + + parser_read_mem = subparsers.add_parser( + "read_mem", help="Read arbitrary memory location" + ) + parser_read_mem.add_argument("address", help="Address to read", type=arg_auto_int) + + parser_write_mem = subparsers.add_parser( + "write_mem", help="Read-modify-write to arbitrary memory location" + ) + parser_write_mem.add_argument("address", help="Address to write", type=arg_auto_int) + parser_write_mem.add_argument("value", help="Value", type=arg_auto_int) + parser_write_mem.add_argument( + "mask", + help="Mask of bits to write", + type=arg_auto_int, + nargs="?", + default="0xFFFFFFFF", + ) + + def add_spi_flash_subparsers( + parent: argparse.ArgumentParser, + allow_keep: bool, + auto_detect: bool, + size_only: bool = False, + ): + """Add common parser arguments for SPI flash properties""" + extra_keep_args = ["keep"] if allow_keep else [] + + if auto_detect and allow_keep: + extra_fs_message = ", detect, or keep" + flash_sizes = ["detect", "keep"] + elif auto_detect: + extra_fs_message = ", or detect" + flash_sizes = ["detect"] + elif allow_keep: + extra_fs_message = ", or keep" + flash_sizes = ["keep"] + else: + extra_fs_message = "" + flash_sizes = [] + + if not size_only: + parent.add_argument( + "--flash_freq", + "-ff", + help="SPI Flash frequency", + choices=extra_keep_args + + [ + "80m", + "60m", + "48m", + "40m", + "30m", + "26m", + "24m", + "20m", + "16m", + "15m", + "12m", + ], + default=os.environ.get("ESPTOOL_FF", "keep" if allow_keep else None), + ) + parent.add_argument( + "--flash_mode", + "-fm", + help="SPI Flash mode", + choices=extra_keep_args + ["qio", "qout", "dio", "dout"], + default=os.environ.get("ESPTOOL_FM", "keep" if allow_keep else "qio"), + ) + + parent.add_argument( + "--flash_size", + "-fs", + help="SPI Flash size in MegaBytes " + "(1MB, 2MB, 4MB, 8MB, 16MB, 32MB, 64MB, 128MB) " + "plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)" + extra_fs_message, + choices=flash_sizes + + [ + "256KB", + "512KB", + "1MB", + "2MB", + "2MB-c1", + "4MB", + "4MB-c1", + "8MB", + "16MB", + "32MB", + "64MB", + "128MB", + ], + default=os.environ.get("ESPTOOL_FS", "keep" if allow_keep else "1MB"), + ) + add_spi_connection_arg(parent) + + parser_write_flash = subparsers.add_parser( + "write_flash", help="Write a binary blob to flash" + ) + + parser_write_flash.add_argument( + "addr_filename", + metavar="
", + help="Address followed by binary filename, separated by space", + action=AddrFilenamePairAction, + ) + parser_write_flash.add_argument( + "--erase-all", + "-e", + help="Erase all regions of flash (not just write areas) before programming", + action="store_true", + ) + + add_spi_flash_subparsers(parser_write_flash, allow_keep=True, auto_detect=True) + parser_write_flash.add_argument( + "--no-progress", "-p", help="Suppress progress output", action="store_true" + ) + parser_write_flash.add_argument( + "--verify", + help="Verify just-written data on flash " + "(mostly superfluous, data is read back during flashing)", + action="store_true", + ) + parser_write_flash.add_argument( + "--encrypt", + help="Apply flash encryption when writing data " + "(required correct efuse settings)", + action="store_true", + ) + # In order to not break backward compatibility, + # our list of encrypted files to flash is a new parameter + parser_write_flash.add_argument( + "--encrypt-files", + metavar="
", + help="Files to be encrypted on the flash. " + "Address followed by binary filename, separated by space.", + action=AddrFilenamePairAction, + ) + parser_write_flash.add_argument( + "--ignore-flash-encryption-efuse-setting", + help="Ignore flash encryption efuse settings ", + action="store_true", + ) + parser_write_flash.add_argument( + "--force", + help="Force write, skip security and compatibility checks. Use with caution!", + action="store_true", + ) + + compress_args = parser_write_flash.add_mutually_exclusive_group(required=False) + compress_args.add_argument( + "--compress", + "-z", + help="Compress data in transfer (default unless --no-stub is specified)", + action="store_true", + default=None, + ) + compress_args.add_argument( + "--no-compress", + "-u", + help="Disable data compression during transfer " + "(default if --no-stub is specified)", + action="store_true", + ) + + subparsers.add_parser("run", help="Run application code in flash") + + parser_image_info = subparsers.add_parser( + "image_info", help="Dump headers from a binary file (bootloader or application)" + ) + parser_image_info.add_argument( + "filename", help="Image file to parse", action=AutoHex2BinAction + ) + parser_image_info.add_argument( + "--version", + "-v", + help="Output format version (1 - legacy, 2 - extended)", + choices=["1", "2"], + default="1", + ) + + parser_make_image = subparsers.add_parser( + "make_image", help="Create an application image from binary files" + ) + parser_make_image.add_argument("output", help="Output image file") + parser_make_image.add_argument( + "--segfile", "-f", action="append", help="Segment input file" + ) + parser_make_image.add_argument( + "--segaddr", + "-a", + action="append", + help="Segment base address", + type=arg_auto_int, + ) + parser_make_image.add_argument( + "--entrypoint", + "-e", + help="Address of entry point", + type=arg_auto_int, + default=0, + ) + + parser_elf2image = subparsers.add_parser( + "elf2image", help="Create an application image from ELF file" + ) + parser_elf2image.add_argument("input", help="Input ELF file") + parser_elf2image.add_argument( + "--output", + "-o", + help="Output filename prefix (for version 1 image), " + "or filename (for version 2 single image)", + type=str, + ) + parser_elf2image.add_argument( + "--version", + "-e", + help="Output image version", + choices=["1", "2", "3"], + default="1", + ) + parser_elf2image.add_argument( + # it kept for compatibility + # Minimum chip revision (deprecated, consider using --min-rev-full) + "--min-rev", + "-r", + help=argparse.SUPPRESS, + type=int, + choices=range(256), + metavar="{0, ... 255}", + default=0, + ) + parser_elf2image.add_argument( + "--min-rev-full", + help="Minimal chip revision (in format: major * 100 + minor)", + type=int, + choices=range(65536), + metavar="{0, ... 65535}", + default=0, + ) + parser_elf2image.add_argument( + "--max-rev-full", + help="Maximal chip revision (in format: major * 100 + minor)", + type=int, + choices=range(65536), + metavar="{0, ... 65535}", + default=65535, + ) + parser_elf2image.add_argument( + "--secure-pad", + action="store_true", + help="Pad image so once signed it will end on a 64KB boundary. " + "For Secure Boot v1 images only.", + ) + parser_elf2image.add_argument( + "--secure-pad-v2", + action="store_true", + help="Pad image to 64KB, so once signed its signature sector will" + "start at the next 64K block. For Secure Boot v2 images only.", + ) + parser_elf2image.add_argument( + "--elf-sha256-offset", + help="If set, insert SHA256 hash (32 bytes) of the input ELF file " + "at specified offset in the binary.", + type=arg_auto_int, + default=None, + ) + parser_elf2image.add_argument( + "--dont-append-digest", + dest="append_digest", + help="Don't append a SHA256 digest of the entire image after the checksum. " + "This argument is not supported and ignored for ESP8266.", + action="store_false", + default=True, + ) + parser_elf2image.add_argument( + "--use_segments", + help="If set, ELF segments will be used instead of ELF sections " + "to generate the image.", + action="store_true", + ) + parser_elf2image.add_argument( + "--flash-mmu-page-size", + help="Change flash MMU page size.", + choices=["64KB", "32KB", "16KB", "8KB"], + ) + parser_elf2image.add_argument( + "--pad-to-size", + help="The block size with which the final binary image after padding " + "must be aligned to. Value 0xFF is used for padding, similar to erase_flash", + default=None, + ) + parser_elf2image.add_argument( + "--ram-only-header", + help="Order segments of the output so IRAM and DRAM are placed at the " + "beginning and force the main header segment number to RAM segments " + "quantity. This will make the other segments invisible to the ROM " + "loader. Use this argument with care because the ROM loader will load " + "only the RAM segments although the other segments being present in " + "the output. Implies --dont-append-digest", + action="store_true", + default=None, + ) + + add_spi_flash_subparsers(parser_elf2image, allow_keep=False, auto_detect=False) + + subparsers.add_parser("read_mac", help="Read MAC address from OTP ROM") + + subparsers.add_parser("chip_id", help="Read Chip ID from OTP ROM") + + parser_flash_id = subparsers.add_parser( + "flash_id", help="Read SPI flash manufacturer and device ID" + ) + add_spi_connection_arg(parser_flash_id) + + parser_read_status = subparsers.add_parser( + "read_flash_status", help="Read SPI flash status register" + ) + + add_spi_connection_arg(parser_read_status) + parser_read_status.add_argument( + "--bytes", + help="Number of bytes to read (1-3)", + type=int, + choices=[1, 2, 3], + default=2, + ) + + parser_write_status = subparsers.add_parser( + "write_flash_status", help="Write SPI flash status register" + ) + + add_spi_connection_arg(parser_write_status) + parser_write_status.add_argument( + "--non-volatile", + help="Write non-volatile bits (use with caution)", + action="store_true", + ) + parser_write_status.add_argument( + "--bytes", + help="Number of status bytes to write (1-3)", + type=int, + choices=[1, 2, 3], + default=2, + ) + parser_write_status.add_argument("value", help="New value", type=arg_auto_int) + + parser_read_flash = subparsers.add_parser( + "read_flash", help="Read SPI flash content" + ) + add_spi_flash_subparsers( + parser_read_flash, allow_keep=True, auto_detect=True, size_only=True + ) + parser_read_flash.add_argument("address", help="Start address", type=arg_auto_int) + parser_read_flash.add_argument( + "size", + help="Size of region to dump. Use `ALL` to read to the end of flash.", + type=arg_auto_size, + ) + parser_read_flash.add_argument("filename", help="Name of binary dump") + parser_read_flash.add_argument( + "--no-progress", "-p", help="Suppress progress output", action="store_true" + ) + + parser_verify_flash = subparsers.add_parser( + "verify_flash", help="Verify a binary blob against flash" + ) + parser_verify_flash.add_argument( + "addr_filename", + help="Address and binary file to verify there, separated by space", + action=AddrFilenamePairAction, + ) + parser_verify_flash.add_argument( + "--diff", "-d", help="Show differences", choices=["no", "yes"], default="no" + ) + add_spi_flash_subparsers(parser_verify_flash, allow_keep=True, auto_detect=True) + + parser_erase_flash = subparsers.add_parser( + "erase_flash", help="Perform Chip Erase on SPI flash" + ) + parser_erase_flash.add_argument( + "--force", + help="Erase flash even if security features are enabled. Use with caution!", + action="store_true", + ) + add_spi_connection_arg(parser_erase_flash) + + parser_erase_region = subparsers.add_parser( + "erase_region", help="Erase a region of the flash" + ) + parser_erase_region.add_argument( + "--force", + help="Erase region even if security features are enabled. Use with caution!", + action="store_true", + ) + add_spi_connection_arg(parser_erase_region) + parser_erase_region.add_argument( + "address", help="Start address (must be multiple of 4096)", type=arg_auto_int + ) + parser_erase_region.add_argument( + "size", + help="Size of region to erase (must be multiple of 4096). " + "Use `ALL` to erase to the end of flash.", + type=arg_auto_size, + ) + + parser_read_flash_sfdp = subparsers.add_parser( + "read_flash_sfdp", + help="Read SPI flash SFDP (Serial Flash Discoverable Parameters)", + ) + add_spi_flash_subparsers(parser_read_flash_sfdp, allow_keep=True, auto_detect=True) + parser_read_flash_sfdp.add_argument("addr", type=arg_auto_int) + parser_read_flash_sfdp.add_argument("bytes", type=int) + + parser_merge_bin = subparsers.add_parser( + "merge_bin", + help="Merge multiple raw binary files into a single file for later flashing", + ) + + parser_merge_bin.add_argument( + "--output", "-o", help="Output filename", type=str, required=True + ) + parser_merge_bin.add_argument( + "--format", + "-f", + help="Format of the output file", + choices=["raw", "uf2", "hex"], + default="raw", + ) + uf2_group = parser_merge_bin.add_argument_group("UF2 format") + uf2_group.add_argument( + "--chunk-size", + help="Specify the used data part of the 512 byte UF2 block. " + "A common value is 256. By default the largest possible value will be used.", + default=None, + type=arg_auto_chunk_size, + ) + uf2_group.add_argument( + "--md5-disable", + help="Disable MD5 checksum in UF2 output", + action="store_true", + ) + add_spi_flash_subparsers(parser_merge_bin, allow_keep=True, auto_detect=False) + + raw_group = parser_merge_bin.add_argument_group("RAW format") + raw_group.add_argument( + "--target-offset", + "-t", + help="Target offset where the output file will be flashed", + type=arg_auto_int, + default=0, + ) + raw_group.add_argument( + "--fill-flash-size", + help="If set, the final binary file will be padded with FF " + "bytes up to this flash size.", + choices=[ + "256KB", + "512KB", + "1MB", + "2MB", + "4MB", + "8MB", + "16MB", + "32MB", + "64MB", + "128MB", + ], + ) + parser_merge_bin.add_argument( + "addr_filename", + metavar="
", + help="Address followed by binary filename, separated by space", + action=AddrFilenamePairAction, + ) + + subparsers.add_parser("get_security_info", help="Get some security-related data") + + subparsers.add_parser("version", help="Print esptool version") + + # internal sanity check - every operation matches a module function of the same name + for operation in subparsers.choices.keys(): + assert operation in globals(), "%s should be a module function" % operation + + # Enable argcomplete only on Unix-like systems + if sys.platform != "win32": + try: + import argcomplete + + argcomplete.autocomplete(parser) + except ImportError: + pass + + argv = expand_file_arguments(argv or sys.argv[1:]) + + args = parser.parse_args(argv) + print("esptool.py v%s" % __version__) + load_config_file(verbose=True) + + StubFlasher.set_preferred_stub_subdir(args.stub_version) + + # Parse filter arguments into separate lists + args.filterVids = [] + args.filterPids = [] + args.filterNames = [] + for f in args.port_filter: + kvp = f.split("=") + if len(kvp) != 2: + raise FatalError("Option --port-filter argument must consist of key=value") + if kvp[0] == "vid": + args.filterVids.append(arg_auto_int(kvp[1])) + elif kvp[0] == "pid": + args.filterPids.append(arg_auto_int(kvp[1])) + elif kvp[0] == "name": + args.filterNames.append(kvp[1]) + else: + raise FatalError("Option --port-filter argument key not recognized") + + # operation function can take 1 arg (args), 2 args (esp, arg) + # or be a member function of the ESPLoader class. + + if args.operation is None: + parser.print_help() + sys.exit(1) + + # Forbid the usage of both --encrypt, which means encrypt all the given files, + # and --encrypt-files, which represents the list of files to encrypt. + # The reason is that allowing both at the same time increases the chances of + # having contradictory lists (e.g. one file not available in one of list). + if ( + args.operation == "write_flash" + and args.encrypt + and args.encrypt_files is not None + ): + raise FatalError( + "Options --encrypt and --encrypt-files " + "must not be specified at the same time." + ) + + operation_func = globals()[args.operation] + operation_args = inspect.getfullargspec(operation_func).args + + if ( + operation_args[0] == "esp" + ): # operation function takes an ESPLoader connection object + if args.before != "no_reset_no_sync": + initial_baud = min( + ESPLoader.ESP_ROM_BAUD, args.baud + ) # don't sync faster than the default baud rate + else: + initial_baud = args.baud + + if args.port is None: + ser_list = get_port_list(args.filterVids, args.filterPids, args.filterNames) + print("Found %d serial ports" % len(ser_list)) + else: + ser_list = [args.port] + open_port_attempts = os.environ.get( + "ESPTOOL_OPEN_PORT_ATTEMPTS", DEFAULT_OPEN_PORT_ATTEMPTS + ) + try: + open_port_attempts = int(open_port_attempts) + except ValueError: + raise SystemExit("Invalid value for ESPTOOL_OPEN_PORT_ATTEMPTS") + if open_port_attempts != 1: + if args.port is None or args.chip == "auto": + print( + "WARNING: The ESPTOOL_OPEN_PORT_ATTEMPTS (open_port_attempts) option can only be used with --port and --chip arguments." + ) + else: + esp = esp or connect_loop( + args.port, + initial_baud, + args.chip, + open_port_attempts, + args.trace, + args.before, + ) + esp = esp or get_default_connected_device( + ser_list, + port=args.port, + connect_attempts=args.connect_attempts, + initial_baud=initial_baud, + chip=args.chip, + trace=args.trace, + before=args.before, + ) + + if esp is None: + raise FatalError( + "Could not connect to an Espressif device " + "on any of the %d available serial ports." % len(ser_list) + ) + + if esp.secure_download_mode: + print("Chip is %s in Secure Download Mode" % esp.CHIP_NAME) + else: + print("Chip is %s" % (esp.get_chip_description())) + print("Features: %s" % ", ".join(esp.get_chip_features())) + print("Crystal is %dMHz" % esp.get_crystal_freq()) + read_mac(esp, args) + + if not args.no_stub: + if esp.secure_download_mode: + print( + "WARNING: Stub loader is not supported in Secure Download Mode, " + "setting --no-stub" + ) + args.no_stub = True + elif not esp.IS_STUB and esp.stub_is_disabled: + print( + "WARNING: Stub loader has been disabled for compatibility, " + "setting --no-stub" + ) + args.no_stub = True + else: + try: + esp = esp.run_stub() + except Exception: + # The CH9102 bridge (PID: 0x55D4) can have issues on MacOS + if sys.platform == "darwin" and esp._get_pid() == 0x55D4: + print( + "\nNote: If issues persist, " + "try installing the WCH USB-to-Serial MacOS driver." + ) + raise + + if args.override_vddsdio: + esp.override_vddsdio(args.override_vddsdio) + + if args.baud > initial_baud: + try: + esp.change_baud(args.baud) + except NotImplementedInROMError: + print( + "WARNING: ROM doesn't support changing baud rate. " + "Keeping initial baud rate %d" % initial_baud + ) + + def _define_spi_conn(spi_connection): + """Prepare SPI configuration string and value for flash_spi_attach()""" + clk, q, d, hd, cs = spi_connection + spi_config_txt = f"CLK:{clk}, Q:{q}, D:{d}, HD:{hd}, CS:{cs}" + value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk + return spi_config_txt, value + + # Override the common SPI flash parameter stuff if configured to do so + if hasattr(args, "spi_connection") and args.spi_connection is not None: + spi_config = args.spi_connection + if args.spi_connection == "SPI": + value = 0 + elif args.spi_connection == "HSPI": + value = 1 + else: + esp.check_spi_connection(args.spi_connection) + # Encode the pin numbers as a 32-bit integer with packed 6-bit values, + # the same way the ESP ROM takes them + spi_config, value = _define_spi_conn(args.spi_connection) + print(f"Configuring SPI flash mode ({spi_config})...") + esp.flash_spi_attach(value) + elif args.no_stub: + if esp.CHIP_NAME != "ESP32" or esp.secure_download_mode: + print("Enabling default SPI flash mode...") + # ROM loader doesn't enable flash unless we explicitly do it + esp.flash_spi_attach(0) + else: + # ROM doesn't attach in-package flash chips + spi_chip_pads = esp.get_chip_spi_pads() + spi_config_txt, value = _define_spi_conn(spi_chip_pads) + if spi_chip_pads != (0, 0, 0, 0, 0): + print( + "Attaching flash from eFuses' SPI pads configuration" + f"({spi_config_txt})..." + ) + else: + print("Enabling default SPI flash mode...") + esp.flash_spi_attach(value) + + # XMC chip startup sequence + XMC_VENDOR_ID = 0x20 + + def is_xmc_chip_strict(): + id = esp.flash_id() + rdid = ((id & 0xFF) << 16) | ((id >> 16) & 0xFF) | (id & 0xFF00) + + vendor_id = (rdid >> 16) & 0xFF + mfid = (rdid >> 8) & 0xFF + cpid = rdid & 0xFF + + if vendor_id != XMC_VENDOR_ID: + return False + + matched = False + if mfid == 0x40: + if cpid >= 0x13 and cpid <= 0x20: + matched = True + elif mfid == 0x41: + if cpid >= 0x17 and cpid <= 0x20: + matched = True + elif mfid == 0x50: + if cpid >= 0x15 and cpid <= 0x16: + matched = True + return matched + + def flash_xmc_startup(): + # If the RDID value is a valid XMC one, may skip the flow + fast_check = True + if fast_check and is_xmc_chip_strict(): + return # Successful XMC flash chip boot-up detected by RDID, skipping. + + sfdp_mfid_addr = 0x10 + mf_id = esp.read_spiflash_sfdp(sfdp_mfid_addr, 8) + if mf_id != XMC_VENDOR_ID: # Non-XMC chip detected by SFDP Read, skipping. + return + + print( + "WARNING: XMC flash chip boot-up failure detected! " + "Running XMC25QHxxC startup flow" + ) + esp.run_spiflash_command(0xB9) # Enter DPD + esp.run_spiflash_command(0x79) # Enter UDPD + esp.run_spiflash_command(0xFF) # Exit UDPD + time.sleep(0.002) # Delay tXUDPD + esp.run_spiflash_command(0xAB) # Release Power-Down + time.sleep(0.00002) + # Check for success + if not is_xmc_chip_strict(): + print("WARNING: XMC flash boot-up fix failed.") + print("XMC flash chip boot-up fix successful!") + + # Check flash chip connection + if not esp.secure_download_mode: + try: + flash_id = esp.flash_id() + if flash_id in (0xFFFFFF, 0x000000): + print( + "WARNING: Failed to communicate with the flash chip, " + "read/write operations will fail. " + "Try checking the chip connections or removing " + "any other hardware connected to IOs." + ) + if ( + hasattr(args, "spi_connection") + and args.spi_connection is not None + ): + print( + "Some GPIO pins might be used by other peripherals, " + "try using another --spi-connection combination." + ) + + except FatalError as e: + raise FatalError(f"Unable to verify flash chip connection ({e}).") + + # Check if XMC SPI flash chip booted-up successfully, fix if not + if not esp.secure_download_mode: + try: + flash_xmc_startup() + except FatalError as e: + esp.trace(f"Unable to perform XMC flash chip startup sequence ({e}).") + + if hasattr(args, "flash_size"): + print("Configuring flash size...") + if args.flash_size == "detect": + flash_size = detect_flash_size(esp, args) + elif args.flash_size == "keep": + flash_size = detect_flash_size(esp, args=None) + if not esp.IS_STUB: + print( + "WARNING: In case of failure, please set a specific --flash_size." + ) + else: + flash_size = args.flash_size + + if flash_size is not None: # Secure download mode + esp.flash_set_parameters(flash_size_bytes(flash_size)) + # Check if stub supports chosen flash size + if ( + esp.IS_STUB + and esp.CHIP_NAME != "ESP32-S3" + and flash_size_bytes(flash_size) > 16 * 1024 * 1024 + ): + print( + "WARNING: Flasher stub doesn't fully support flash size larger " + "than 16MB, in case of failure use --no-stub." + ) + + if getattr(args, "size", "") == "all": + if esp.secure_download_mode: + raise FatalError( + "Detecting flash size is not supported in secure download mode. " + "Set an exact size value." + ) + # detect flash size + flash_id = esp.flash_id() + size_id = flash_id >> 16 + size_str = DETECTED_FLASH_SIZES.get(size_id) + if size_str is None: + raise FatalError( + "Detecting flash size failed. Set an exact size value." + ) + print(f"Detected flash size: {size_str}") + args.size = flash_size_bytes(size_str) + + if esp.IS_STUB and hasattr(args, "address") and hasattr(args, "size"): + if esp.CHIP_NAME != "ESP32-S3" and args.address + args.size > 0x1000000: + print( + "WARNING: Flasher stub doesn't fully support flash size larger " + "than 16MB, in case of failure use --no-stub." + ) + + try: + operation_func(esp, args) + finally: + try: # Clean up AddrFilenamePairAction files + for address, argfile in args.addr_filename: + argfile.close() + except AttributeError: + pass + + # Handle post-operation behaviour (reset or other) + if operation_func == load_ram: + # the ESP is now running the loaded image, so let it run + print("Exiting immediately.") + elif args.after == "hard_reset": + esp.hard_reset() + elif args.after == "soft_reset": + print("Soft resetting...") + # flash_finish will trigger a soft reset + esp.soft_reset(False) + elif args.after == "no_reset_stub": + print("Staying in flasher stub.") + else: # args.after == 'no_reset' + print("Staying in bootloader.") + if esp.IS_STUB: + esp.soft_reset(True) # exit stub back to ROM loader + + if not external_esp: + esp._port.close() + + else: + operation_func(args) + + +def arg_auto_int(x): + return int(x, 0) + + +def arg_auto_size(x): + x = x.lower() + return x if x == "all" else arg_auto_int(x) + + +def arg_auto_chunk_size(string: str) -> int: + num = int(string, 0) + if num & 3 != 0: + raise argparse.ArgumentTypeError("Chunk size should be a 4-byte aligned number") + return num + + +def get_port_list(vids=[], pids=[], names=[]): + if list_ports is None: + raise FatalError( + "Listing all serial ports is currently not available. " + "Please try to specify the port when running esptool.py or update " + "the pyserial package to the latest version" + ) + ports = [] + for port in list_ports.comports(): + if sys.platform == "darwin" and port.device.endswith( + ("Bluetooth-Incoming-Port", "wlan-debug") + ): + continue + if vids and (port.vid is None or port.vid not in vids): + continue + if pids and (port.pid is None or port.pid not in pids): + continue + if names and ( + port.name is None or all(name not in port.name for name in names) + ): + continue + ports.append(port.device) + return sorted(ports) + + +def expand_file_arguments(argv): + """ + Any argument starting with "@" gets replaced with all values read from a text file. + Text file arguments can be split by newline or by space. + Values are added "as-is", as if they were specified in this order + on the command line. + """ + new_args = [] + expanded = False + for arg in argv: + if arg.startswith("@"): + expanded = True + with open(arg[1:], "r") as f: + for line in f.readlines(): + new_args += shlex.split(line) + else: + new_args.append(arg) + if expanded: + print(f"esptool.py {' '.join(new_args)}") + return new_args + return argv + + +def connect_loop( + port: str, + initial_baud: int, + chip: str, + max_retries: int, + trace: bool = False, + before: str = "default_reset", +): + chip_class = CHIP_DEFS[chip] + esp = None + print(f"Serial port {port}") + + first = True + ten_cycle = cycle(chain(repeat(False, 9), (True,))) + retry_loop = chain( + repeat(False, max_retries - 1), (True,) if max_retries else cycle((False,)) + ) + + for last, every_tenth in zip(retry_loop, ten_cycle): + try: + esp = chip_class(port, initial_baud, trace) + if not first: + # break the retrying line + print("") + esp.connect(before) + return esp + except ( + FatalError, + serial.serialutil.SerialException, + IOError, + OSError, + ) as err: + if esp and esp._port: + esp._port.close() + esp = None + if first: + print(err) + print("Retrying failed connection", end="", flush=True) + first = False + if last: + raise err + if every_tenth: + # print a dot every second + print(".", end="", flush=True) + time.sleep(0.1) + + +def get_default_connected_device( + serial_list, + port, + connect_attempts, + initial_baud, + chip="auto", + trace=False, + before="default_reset", +): + _esp = None + for each_port in reversed(serial_list): + print("Serial port %s" % each_port) + try: + if chip == "auto": + _esp = detect_chip( + each_port, initial_baud, before, trace, connect_attempts + ) + else: + chip_class = CHIP_DEFS[chip] + _esp = chip_class(each_port, initial_baud, trace) + _esp.connect(before, connect_attempts) + break + except (FatalError, OSError) as err: + if port is not None: + raise + print("%s failed to connect: %s" % (each_port, err)) + if _esp and _esp._port: + _esp._port.close() + _esp = None + return _esp + + +class SpiConnectionAction(argparse.Action): + """ + Custom action to parse 'spi connection' override. + Values are SPI, HSPI, or a sequence of 5 pin numbers separated by commas. + """ + + def __call__(self, parser, namespace, value, option_string=None): + if value.upper() in ["SPI", "HSPI"]: + values = value.upper() + elif "," in value: + values = value.split(",") + if len(values) != 5: + raise argparse.ArgumentError( + self, + f"{value} is not a valid list of comma-separate pin numbers. " + "Must be 5 numbers - CLK,Q,D,HD,CS.", + ) + try: + values = tuple(int(v, 0) for v in values) + except ValueError: + raise argparse.ArgumentError( + self, + f"{values} is not a valid argument. " + "All pins must be numeric values", + ) + else: + raise argparse.ArgumentError( + self, + f"{value} is not a valid spi-connection value. " + "Values are SPI, HSPI, or a sequence of 5 pin numbers - CLK,Q,D,HD,CS.", + ) + setattr(namespace, self.dest, values) + + +class AutoHex2BinAction(argparse.Action): + """Custom parser class for auto conversion of input files from hex to bin""" + + def __call__(self, parser, namespace, value, option_string=None): + try: + with open(value, "rb") as f: + # if hex file was detected replace hex file with converted temp bin + # otherwise keep the original file + value = intel_hex_to_bin(f).name + except IOError as e: + raise argparse.ArgumentError(self, e) + setattr(namespace, self.dest, value) + + +class AddrFilenamePairAction(argparse.Action): + """Custom parser class for the address/filename pairs passed as arguments""" + + def __init__(self, option_strings, dest, nargs="+", **kwargs): + super(AddrFilenamePairAction, self).__init__( + option_strings, dest, nargs, **kwargs + ) + + def __call__(self, parser, namespace, values, option_string=None): + # validate pair arguments + pairs = [] + for i in range(0, len(values), 2): + try: + address = int(values[i], 0) + except ValueError: + raise argparse.ArgumentError( + self, 'Address "%s" must be a number' % values[i] + ) + try: + argfile = open(values[i + 1], "rb") + except IOError as e: + raise argparse.ArgumentError(self, e) + except IndexError: + raise argparse.ArgumentError( + self, + "Must be pairs of an address " + "and the binary filename to write there", + ) + # check for intel hex files and convert them to bin + argfile = intel_hex_to_bin(argfile, address) + pairs.append((address, argfile)) + + # Sort the addresses and check for overlapping + end = 0 + for address, argfile in sorted(pairs, key=lambda x: x[0]): + argfile.seek(0, 2) # seek to end + size = argfile.tell() + argfile.seek(0) + sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1) + sector_end = ( + (address + size + ESPLoader.FLASH_SECTOR_SIZE - 1) + & ~(ESPLoader.FLASH_SECTOR_SIZE - 1) + ) - 1 + if sector_start < end: + message = "Detected overlap at address: 0x%x for file: %s" % ( + address, + argfile.name, + ) + raise argparse.ArgumentError(self, message) + end = sector_end + setattr(namespace, self.dest, pairs) + + +def _main(): + try: + main() + except FatalError as e: + print(f"\nA fatal error occurred: {e}") + sys.exit(2) + except serial.serialutil.SerialException as e: + print(f"\nA serial exception error occurred: {e}") + print( + "Note: This error originates from pySerial. " + "It is likely not a problem with esptool, " + "but with the hardware connection or drivers." + ) + print( + "For troubleshooting steps visit: " + "https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html" + ) + sys.exit(1) + except StopIteration: + print(traceback.format_exc()) + print("A fatal error occurred: The chip stopped responding.") + sys.exit(2) + + +if __name__ == "__main__": + _main() diff --git a/mixly/tools/python/esptool/__main__.py b/mixly/tools/python/esptool/__main__.py new file mode 100644 index 00000000..11e3bce1 --- /dev/null +++ b/mixly/tools/python/esptool/__main__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton, +# Espressif Systems (Shanghai) CO LTD, other contributors as noted. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import esptool + +if __name__ == "__main__": + esptool._main() diff --git a/mixly/tools/python/esptool/bin_image.py b/mixly/tools/python/esptool/bin_image.py new file mode 100644 index 00000000..a3a90dea --- /dev/null +++ b/mixly/tools/python/esptool/bin_image.py @@ -0,0 +1,1380 @@ +# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton, +# Espressif Systems (Shanghai) CO LTD, other contributors as noted. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import binascii +import copy +import hashlib +import io +import os +import re +import struct +import tempfile +from typing import IO, Optional + +from intelhex import HexRecordError, IntelHex + +from .loader import ESPLoader +from .targets import ( + ESP32C2ROM, + ESP32C3ROM, + ESP32C5ROM, + ESP32C5BETA3ROM, + ESP32C6BETAROM, + ESP32C6ROM, + ESP32C61ROM, + ESP32H2BETA1ROM, + ESP32H2BETA2ROM, + ESP32H2ROM, + ESP32P4ROM, + ESP32ROM, + ESP32S2ROM, + ESP32S3BETA2ROM, + ESP32S3ROM, + ESP8266ROM, +) +from .util import FatalError, byte, pad_to + + +def align_file_position(f, size): + """Align the position in the file to the next block of specified size""" + align = (size - 1) - (f.tell() % size) + f.seek(align, 1) + + +def intel_hex_to_bin(file: IO[bytes], start_addr: Optional[int] = None) -> IO[bytes]: + """Convert IntelHex file to temp binary file with padding from start_addr + If hex file was detected return temp bin file object; input file otherwise""" + INTEL_HEX_MAGIC = b":" + magic = file.read(1) + file.seek(0) + try: + if magic == INTEL_HEX_MAGIC: + ih = IntelHex() + ih.loadhex(file.name) + file.close() + bin = tempfile.NamedTemporaryFile(suffix=".bin", delete=False) + ih.tobinfile(bin, start=start_addr) + return bin + else: + return file + except (HexRecordError, UnicodeDecodeError): + # file started with HEX magic but the rest was not according to the standard + return file + + +def LoadFirmwareImage(chip, image_file): + """ + Load a firmware image. Can be for any supported SoC. + + ESP8266 images will be examined to determine if they are original ROM firmware + images (ESP8266ROMFirmwareImage) or "v2" OTA bootloader images. + + Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1) + or ESP8266V2FirmwareImage (v2). + """ + + def select_image_class(f, chip): + chip = re.sub(r"[-()]", "", chip.lower()) + if chip != "esp8266": + return { + "esp32": ESP32FirmwareImage, + "esp32s2": ESP32S2FirmwareImage, + "esp32s3beta2": ESP32S3BETA2FirmwareImage, + "esp32s3": ESP32S3FirmwareImage, + "esp32c3": ESP32C3FirmwareImage, + "esp32c6beta": ESP32C6BETAFirmwareImage, + "esp32h2beta1": ESP32H2BETA1FirmwareImage, + "esp32h2beta2": ESP32H2BETA2FirmwareImage, + "esp32c2": ESP32C2FirmwareImage, + "esp32c6": ESP32C6FirmwareImage, + "esp32c61": ESP32C61FirmwareImage, + "esp32c5": ESP32C5FirmwareImage, + "esp32c5beta3": ESP32C5BETA3FirmwareImage, + "esp32h2": ESP32H2FirmwareImage, + "esp32p4": ESP32P4FirmwareImage, + }[chip](f) + else: # Otherwise, ESP8266 so look at magic to determine the image type + magic = ord(f.read(1)) + f.seek(0) + if magic == ESPLoader.ESP_IMAGE_MAGIC: + return ESP8266ROMFirmwareImage(f) + elif magic == ESP8266V2FirmwareImage.IMAGE_V2_MAGIC: + return ESP8266V2FirmwareImage(f) + else: + raise FatalError("Invalid image magic number: %d" % magic) + + if isinstance(image_file, str): + with open(image_file, "rb") as f: + return select_image_class(f, chip) + return select_image_class(image_file, chip) + + +class ImageSegment(object): + """Wrapper class for a segment in an ESP image + (very similar to a section in an ELFImage also)""" + + def __init__(self, addr, data, file_offs=None, flags=0): + self.addr = addr + self.data = data + self.file_offs = file_offs + self.flags = flags + self.include_in_checksum = True + if self.addr != 0: + self.pad_to_alignment( + 4 + ) # pad all "real" ImageSegments 4 byte aligned length + + def copy_with_new_addr(self, new_addr): + """Return a new ImageSegment with same data, but mapped at + a new address.""" + return ImageSegment(new_addr, self.data, 0) + + def split_image(self, split_len): + """Return a new ImageSegment which splits "split_len" bytes + from the beginning of the data. Remaining bytes are kept in + this segment object (and the start address is adjusted to match.)""" + result = copy.copy(self) + result.data = self.data[:split_len] + self.data = self.data[split_len:] + self.addr += split_len + self.file_offs = None + result.file_offs = None + return result + + def __repr__(self): + r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr) + if self.file_offs is not None: + r += " file_offs 0x%08x" % (self.file_offs) + return r + + def get_memory_type(self, image): + """ + Return a list describing the memory type(s) that is covered by this + segment's start address. + """ + return [ + map_range[2] + for map_range in image.ROM_LOADER.MEMORY_MAP + if map_range[0] <= self.addr < map_range[1] + ] + + def pad_to_alignment(self, alignment): + self.data = pad_to(self.data, alignment, b"\x00") + + +class ELFSection(ImageSegment): + """Wrapper class for a section in an ELF image, has a section + name as well as the common properties of an ImageSegment.""" + + def __init__(self, name, addr, data, flags): + super(ELFSection, self).__init__(addr, data, flags=flags) + self.name = name.decode("utf-8") + + def __repr__(self): + return "%s %s" % (self.name, super(ELFSection, self).__repr__()) + + +class BaseFirmwareImage(object): + SEG_HEADER_LEN = 8 + SHA256_DIGEST_LEN = 32 + ELF_FLAG_WRITE = 0x1 + ELF_FLAG_READ = 0x2 + ELF_FLAG_EXEC = 0x4 + + """ Base class with common firmware image functions """ + + def __init__(self): + self.segments = [] + self.entrypoint = 0 + self.elf_sha256 = None + self.elf_sha256_offset = 0 + self.pad_to_size = 0 + + def load_common_header(self, load_file, expected_magic): + ( + magic, + segments, + self.flash_mode, + self.flash_size_freq, + self.entrypoint, + ) = struct.unpack(" 16: + raise FatalError( + "Invalid segment count %d (max 16). " + "Usually this indicates a linker script problem." % len(self.segments) + ) + + def load_segment(self, f, is_irom_segment=False): + """Load the next segment from the image file""" + file_offs = f.tell() + (offset, size) = struct.unpack(" 0x40200000 or offset < 0x3FFE0000 or size > 65536: + print("WARNING: Suspicious segment 0x%x, length %d" % (offset, size)) + + def maybe_patch_segment_data(self, f, segment_data): + """ + If SHA256 digest of the ELF file needs to be inserted into this segment, do so. + Returns segment data. + """ + segment_len = len(segment_data) + file_pos = f.tell() # file_pos is position in the .bin file + if ( + self.elf_sha256_offset >= file_pos + and self.elf_sha256_offset < file_pos + segment_len + ): + # SHA256 digest needs to be patched into this binary segment, + # calculate offset of the digest inside the binary segment. + patch_offset = self.elf_sha256_offset - file_pos + # Sanity checks + if ( + patch_offset < self.SEG_HEADER_LEN + or patch_offset + self.SHA256_DIGEST_LEN > segment_len + ): + raise FatalError( + "Cannot place SHA256 digest on segment boundary" + "(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)" + % (self.elf_sha256_offset, file_pos, segment_len) + ) + # offset relative to the data part + patch_offset -= self.SEG_HEADER_LEN + if ( + segment_data[patch_offset : patch_offset + self.SHA256_DIGEST_LEN] + != b"\x00" * self.SHA256_DIGEST_LEN + ): + raise FatalError( + "Contents of segment at SHA256 digest offset 0x%x are not all zero." + " Refusing to overwrite." % self.elf_sha256_offset + ) + assert len(self.elf_sha256) == self.SHA256_DIGEST_LEN + segment_data = ( + segment_data[0:patch_offset] + + self.elf_sha256 + + segment_data[patch_offset + self.SHA256_DIGEST_LEN :] + ) + return segment_data + + def save_segment(self, f, segment, checksum=None): + """ + Save the next segment to the image file, + return next checksum value if provided + """ + segment_data = self.maybe_patch_segment_data(f, segment.data) + f.write(struct.pack(" 0: + if len(irom_segments) != 1: + raise FatalError( + "Found %d segments that could be irom0. Bad ELF file?" + % len(irom_segments) + ) + return irom_segments[0] + return None + + def get_non_irom_segments(self): + irom_segment = self.get_irom_segment() + return [s for s in self.segments if s != irom_segment] + + def sort_segments(self): + if not self.segments: + return # nothing to sort + self.segments = sorted(self.segments, key=lambda s: s.addr) + + def merge_adjacent_segments(self): + if not self.segments: + return # nothing to merge + + segments = [] + # The easiest way to merge the sections is the browse them backward. + for i in range(len(self.segments) - 1, 0, -1): + # elem is the previous section, the one `next_elem` may need to be + # merged in + elem = self.segments[i - 1] + next_elem = self.segments[i] + if all( + ( + elem.get_memory_type(self) == next_elem.get_memory_type(self), + elem.include_in_checksum == next_elem.include_in_checksum, + next_elem.addr == elem.addr + len(elem.data), + next_elem.flags & self.ELF_FLAG_EXEC + == elem.flags & self.ELF_FLAG_EXEC, + ) + ): + # Merge any segment that ends where the next one starts, + # without spanning memory types + # + # (don't 'pad' any gaps here as they may be excluded from the image + # due to 'noinit' or other reasons.) + elem.data += next_elem.data + else: + # The section next_elem cannot be merged into the previous one, + # which means it needs to be part of the final segments. + # As we are browsing the list backward, the elements need to be + # inserted at the beginning of the final list. + segments.insert(0, next_elem) + + # The first segment will always be here as it cannot be merged into any + # "previous" section. + segments.insert(0, self.segments[0]) + + # note: we could sort segments here as well, but the ordering of segments is + # sometimes important for other reasons (like embedded ELF SHA-256), + # so we assume that the linker script will have produced any adjacent sections + # in linear order in the ELF, anyhow. + self.segments = segments + + def set_mmu_page_size(self, size): + """ + If supported, this should be overridden by the chip-specific class. + Gets called in elf2image. + """ + print( + "WARNING: Changing MMU page size is not supported on {}! " + "Defaulting to 64KB.".format(self.ROM_LOADER.CHIP_NAME) + ) + + +class ESP8266ROMFirmwareImage(BaseFirmwareImage): + """'Version 1' firmware image, segments loaded directly by the ROM bootloader.""" + + ROM_LOADER = ESP8266ROM + + def __init__(self, load_file=None): + super(ESP8266ROMFirmwareImage, self).__init__() + self.flash_mode = 0 + self.flash_size_freq = 0 + self.version = 1 + + if load_file is not None: + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) + + for _ in range(segments): + self.load_segment(load_file) + self.checksum = self.read_checksum(load_file) + + self.verify() + + def default_output_name(self, input_file): + """Derive a default output name from the ELF name.""" + return input_file + "-" + + def save(self, basename): + """Save a set of V1 images for flashing. Parameter is a base filename.""" + # IROM data goes in its own plain binary file + irom_segment = self.get_irom_segment() + if irom_segment is not None: + with open( + "%s0x%05x.bin" + % (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START), + "wb", + ) as f: + f.write(irom_segment.data) + + # everything but IROM goes at 0x00000 in an image file + normal_segments = self.get_non_irom_segments() + with open("%s0x00000.bin" % basename, "wb") as f: + self.write_common_header(f, normal_segments) + checksum = ESPLoader.ESP_CHECKSUM_MAGIC + for segment in normal_segments: + checksum = self.save_segment(f, segment, checksum) + self.append_checksum(f, checksum) + + +ESP8266ROM.BOOTLOADER_IMAGE = ESP8266ROMFirmwareImage + + +class ESP8266V2FirmwareImage(BaseFirmwareImage): + """'Version 2' firmware image, segments loaded by software bootloader stub + (ie Espressif bootloader or rboot) + """ + + ROM_LOADER = ESP8266ROM + # First byte of the "v2" application image + IMAGE_V2_MAGIC = 0xEA + + # First 'segment' value in a "v2" application image, + # appears to be a constant version value? + IMAGE_V2_SEGMENT = 4 + + def __init__(self, load_file=None): + super(ESP8266V2FirmwareImage, self).__init__() + self.version = 2 + if load_file is not None: + segments = self.load_common_header(load_file, self.IMAGE_V2_MAGIC) + if segments != self.IMAGE_V2_SEGMENT: + # segment count is not really segment count here, + # but we expect to see '4' + print( + 'Warning: V2 header has unexpected "segment" count %d (usually 4)' + % segments + ) + + # irom segment comes before the second header + # + # the file is saved in the image with a zero load address + # in the header, so we need to calculate a load address + irom_segment = self.load_segment(load_file, True) + # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_addr + 8 + irom_segment.addr = 0 + irom_segment.include_in_checksum = False + + first_flash_mode = self.flash_mode + first_flash_size_freq = self.flash_size_freq + first_entrypoint = self.entrypoint + # load the second header + + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) + + if first_flash_mode != self.flash_mode: + print( + "WARNING: Flash mode value in first header (0x%02x) disagrees " + "with second (0x%02x). Using second value." + % (first_flash_mode, self.flash_mode) + ) + if first_flash_size_freq != self.flash_size_freq: + print( + "WARNING: Flash size/freq value in first header (0x%02x) disagrees " + "with second (0x%02x). Using second value." + % (first_flash_size_freq, self.flash_size_freq) + ) + if first_entrypoint != self.entrypoint: + print( + "WARNING: Entrypoint address in first header (0x%08x) disagrees " + "with second header (0x%08x). Using second value." + % (first_entrypoint, self.entrypoint) + ) + + # load all the usual segments + for _ in range(segments): + self.load_segment(load_file) + self.checksum = self.read_checksum(load_file) + + self.verify() + + def default_output_name(self, input_file): + """Derive a default output name from the ELF name.""" + irom_segment = self.get_irom_segment() + if irom_segment is not None: + irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START + else: + irom_offs = 0 + return "%s-0x%05x.bin" % ( + os.path.splitext(input_file)[0], + irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1), + ) + + def save(self, filename): + with open(filename, "wb") as f: + # Save first header for irom0 segment + f.write( + struct.pack( + b" 0: + last_addr = flash_segments[0].addr + for segment in flash_segments[1:]: + if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: + raise FatalError( + "Segment loaded at 0x%08x lands in same 64KB flash mapping " + "as segment loaded at 0x%08x. Can't generate binary. " + "Suggest changing linker script or ELF to merge sections." + % (segment.addr, last_addr) + ) + last_addr = segment.addr + + def get_alignment_data_needed(segment): + # Actual alignment (in data bytes) required for a segment header: + # positioned so that after we write the next 8 byte header, + # file_offs % IROM_ALIGN == segment.addr % IROM_ALIGN + # + # (this is because the segment's vaddr may not be IROM_ALIGNed, + # more likely is aligned IROM_ALIGN+0x18 + # to account for the binary file header + align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN + pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past + if pad_len == 0 or pad_len == self.IROM_ALIGN: + return 0 # already aligned + + # subtract SEG_HEADER_LEN a second time, + # as the padding block has a header as well + pad_len -= self.SEG_HEADER_LEN + if pad_len < 0: + pad_len += self.IROM_ALIGN + return pad_len + + if self.ram_only_header: + # write RAM segments first in order to get only RAM segments quantity + # and checksum (ROM bootloader will only care for RAM segments and its + # correct checksums) + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + self.append_checksum(f, checksum) + + # reversing to match the same section order from linker script + flash_segments.reverse() + for segment in flash_segments: + pad_len = get_alignment_data_needed(segment) + # Some chips have a non-zero load offset (eg. 0x1000) + # therefore we shift the ROM segments "-load_offset" + # so it will be aligned properly after it is flashed + align_min = ( + self.ROM_LOADER.BOOTLOADER_FLASH_OFFSET - self.SEG_HEADER_LEN + ) + if pad_len < align_min: + # in case pad_len does not fit minimum alignment, + # pad it to next aligned boundary + pad_len += self.IROM_ALIGN + + pad_len -= self.ROM_LOADER.BOOTLOADER_FLASH_OFFSET + pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell()) + self.save_segment(f, pad_segment) + total_segments += 1 + # check the alignment + assert (f.tell() + 8 + self.ROM_LOADER.BOOTLOADER_FLASH_OFFSET) % ( + self.IROM_ALIGN + ) == segment.addr % self.IROM_ALIGN + # save the flash segment but not saving its checksum neither + # saving the number of flash segments, since ROM bootloader + # should "not see" them + self.save_flash_segment(f, segment) + total_segments += 1 + else: # not self.ram_only_header + # try to fit each flash segment on a 64kB aligned boundary + # by padding with parts of the non-flash segments... + while len(flash_segments) > 0: + segment = flash_segments[0] + pad_len = get_alignment_data_needed(segment) + if pad_len > 0: # need to pad + if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN: + pad_segment = ram_segments[0].split_image(pad_len) + if len(ram_segments[0].data) == 0: + ram_segments.pop(0) + else: + pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell()) + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + else: + # write the flash segment + assert ( + f.tell() + 8 + ) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN + checksum = self.save_flash_segment(f, segment, checksum) + flash_segments.pop(0) + total_segments += 1 + + # flash segments all written, so write any remaining RAM segments + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + + if self.secure_pad: + # pad the image so that after signing it will end on a a 64KB boundary. + # This ensures all mapped flash content will be verified. + if not self.append_digest: + raise FatalError( + "secure_pad only applies if a SHA-256 digest " + "is also appended to the image" + ) + align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN + # 16 byte aligned checksum + # (force the alignment to simplify calculations) + checksum_space = 16 + if self.secure_pad == "1": + # after checksum: SHA-256 digest + + # (to be added by signing process) version, + # signature + 12 trailing bytes due to alignment + space_after_checksum = 32 + 4 + 64 + 12 + elif self.secure_pad == "2": # Secure Boot V2 + # after checksum: SHA-256 digest + + # signature sector, + # but we place signature sector after the 64KB boundary + space_after_checksum = 32 + pad_len = ( + self.IROM_ALIGN - align_past - checksum_space - space_after_checksum + ) % self.IROM_ALIGN + pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell()) + + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + + if not self.ram_only_header: + # done writing segments + self.append_checksum(f, checksum) + image_length = f.tell() + + if self.secure_pad: + assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0 + + # kinda hacky: go back to the initial header and write the new segment count + # that includes padding segments. This header is not checksummed + f.seek(1) + if self.ram_only_header: + # Update the header with the RAM segments quantity as it should be + # visible by the ROM bootloader + f.write(bytes([len(ram_segments)])) + else: + f.write(bytes([total_segments])) + + if self.append_digest: + # calculate the SHA256 of the whole file and append it + f.seek(0) + digest = hashlib.sha256() + digest.update(f.read(image_length)) + f.write(digest.digest()) + + if self.pad_to_size: + image_length = f.tell() + if image_length % self.pad_to_size != 0: + pad_by = self.pad_to_size - (image_length % self.pad_to_size) + f.write(b"\xff" * pad_by) + + with open(filename, "wb") as real_file: + real_file.write(f.getvalue()) + + def load_extended_header(self, load_file): + def split_byte(n): + return (n & 0x0F, (n >> 4) & 0x0F) + + fields = list( + struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16)) + ) + + self.wp_pin = fields[0] + + # SPI pin drive stengths are two per byte + self.clk_drv, self.q_drv = split_byte(fields[1]) + self.d_drv, self.cs_drv = split_byte(fields[2]) + self.hd_drv, self.wp_drv = split_byte(fields[3]) + + self.chip_id = fields[4] + if self.chip_id != self.ROM_LOADER.IMAGE_CHIP_ID: + print( + ( + "Unexpected chip id in image. Expected %d but value was %d. " + "Is this image for a different chip model?" + ) + % (self.ROM_LOADER.IMAGE_CHIP_ID, self.chip_id) + ) + + self.min_rev = fields[5] + self.min_rev_full = fields[6] + self.max_rev_full = fields[7] + + append_digest = fields[-1] # last byte is append_digest + if append_digest in [0, 1]: + self.append_digest = append_digest == 1 + else: + raise RuntimeError( + "Invalid value for append_digest field (0x%02x). Should be 0 or 1.", + append_digest, + ) + + def save_extended_header(self, save_file): + def join_byte(ln, hn): + return (ln & 0x0F) + ((hn & 0x0F) << 4) + + append_digest = 1 if self.append_digest else 0 + + fields = [ + self.wp_pin, + join_byte(self.clk_drv, self.q_drv), + join_byte(self.d_drv, self.cs_drv), + join_byte(self.hd_drv, self.wp_drv), + self.ROM_LOADER.IMAGE_CHIP_ID, + self.min_rev, + self.min_rev_full, + self.max_rev_full, + ] + fields += [0] * 4 # padding + fields += [append_digest] + + packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields) + save_file.write(packed) + + +class ESP8266V3FirmwareImage(ESP32FirmwareImage): + """ESP8266 V3 firmware image is very similar to ESP32 image""" + + EXTENDED_HEADER_STRUCT_FMT = "B" * 16 + + def is_flash_addr(self, addr): + return addr > ESP8266ROM.IROM_MAP_START + + def save(self, filename): + total_segments = 0 + with io.BytesIO() as f: # write file to memory first + self.write_common_header(f, self.segments) + + checksum = ESPLoader.ESP_CHECKSUM_MAGIC + + # split segments into flash-mapped vs ram-loaded, + # and take copies so we can mutate them + flash_segments = [ + copy.deepcopy(s) + for s in sorted(self.segments, key=lambda s: s.addr) + if self.is_flash_addr(s.addr) and len(s.data) + ] + ram_segments = [ + copy.deepcopy(s) + for s in sorted(self.segments, key=lambda s: s.addr) + if not self.is_flash_addr(s.addr) and len(s.data) + ] + + # check for multiple ELF sections that are mapped in the same + # flash mapping region. This is usually a sign of a broken linker script, + # but if you have a legitimate use case then let us know + if len(flash_segments) > 0: + last_addr = flash_segments[0].addr + for segment in flash_segments[1:]: + if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: + raise FatalError( + "Segment loaded at 0x%08x lands in same 64KB flash mapping " + "as segment loaded at 0x%08x. Can't generate binary. " + "Suggest changing linker script or ELF to merge sections." + % (segment.addr, last_addr) + ) + last_addr = segment.addr + + # try to fit each flash segment on a 64kB aligned boundary + # by padding with parts of the non-flash segments... + while len(flash_segments) > 0: + segment = flash_segments[0] + # remove 8 bytes empty data for insert segment header + if isinstance(segment, ELFSection) and segment.name == ".flash.rodata": + segment.data = segment.data[8:] + # write the flash segment + checksum = self.save_segment(f, segment, checksum) + flash_segments.pop(0) + total_segments += 1 + + # flash segments all written, so write any remaining RAM segments + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + + # done writing segments + self.append_checksum(f, checksum) + image_length = f.tell() + + # kinda hacky: go back to the initial header and write the new segment count + # that includes padding segments. This header is not checksummed + f.seek(1) + f.write(bytes([total_segments])) + + if self.append_digest: + # calculate the SHA256 of the whole file and append it + f.seek(0) + digest = hashlib.sha256() + digest.update(f.read(image_length)) + f.write(digest.digest()) + + with open(filename, "wb") as real_file: + real_file.write(f.getvalue()) + + def load_extended_header(self, load_file): + def split_byte(n): + return (n & 0x0F, (n >> 4) & 0x0F) + + fields = list( + struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16)) + ) + + self.wp_pin = fields[0] + + # SPI pin drive stengths are two per byte + self.clk_drv, self.q_drv = split_byte(fields[1]) + self.d_drv, self.cs_drv = split_byte(fields[2]) + self.hd_drv, self.wp_drv = split_byte(fields[3]) + + if fields[15] in [0, 1]: + self.append_digest = fields[15] == 1 + else: + raise RuntimeError( + "Invalid value for append_digest field (0x%02x). Should be 0 or 1.", + fields[15], + ) + + # remaining fields in the middle should all be zero + if any(f for f in fields[4:15] if f != 0): + print( + "Warning: some reserved header fields have non-zero values. " + "This image may be from a newer esptool.py?" + ) + + +ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage + + +class ESP32S2FirmwareImage(ESP32FirmwareImage): + """ESP32S2 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32S2ROM + + +ESP32S2ROM.BOOTLOADER_IMAGE = ESP32S2FirmwareImage + + +class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage): + """ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32S3BETA2ROM + + +ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage + + +class ESP32S3FirmwareImage(ESP32FirmwareImage): + """ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32S3ROM + + +ESP32S3ROM.BOOTLOADER_IMAGE = ESP32S3FirmwareImage + + +class ESP32C3FirmwareImage(ESP32FirmwareImage): + """ESP32C3 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32C3ROM + + +ESP32C3ROM.BOOTLOADER_IMAGE = ESP32C3FirmwareImage + + +class ESP32C6BETAFirmwareImage(ESP32FirmwareImage): + """ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32C6BETAROM + + +ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage + + +class ESP32H2BETA1FirmwareImage(ESP32FirmwareImage): + """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32H2BETA1ROM + + +ESP32H2BETA1ROM.BOOTLOADER_IMAGE = ESP32H2BETA1FirmwareImage + + +class ESP32H2BETA2FirmwareImage(ESP32FirmwareImage): + """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32H2BETA2ROM + + +ESP32H2BETA2ROM.BOOTLOADER_IMAGE = ESP32H2BETA2FirmwareImage + + +class ESP32C2FirmwareImage(ESP32FirmwareImage): + """ESP32C2 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32C2ROM + + def set_mmu_page_size(self, size): + if size not in [16384, 32768, 65536]: + raise FatalError( + "{} bytes is not a valid ESP32-C2 page size, " + "select from 64KB, 32KB, 16KB.".format(size) + ) + self.IROM_ALIGN = size + + +ESP32C2ROM.BOOTLOADER_IMAGE = ESP32C2FirmwareImage + + +class ESP32C6FirmwareImage(ESP32FirmwareImage): + """ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32C6ROM + + def set_mmu_page_size(self, size): + if size not in [8192, 16384, 32768, 65536]: + raise FatalError( + "{} bytes is not a valid ESP32-C6 page size, " + "select from 64KB, 32KB, 16KB, 8KB.".format(size) + ) + self.IROM_ALIGN = size + + +ESP32C6ROM.BOOTLOADER_IMAGE = ESP32C6FirmwareImage + + +class ESP32C61FirmwareImage(ESP32C6FirmwareImage): + """ESP32C61 Firmware Image almost exactly the same as ESP32C6FirmwareImage""" + + ROM_LOADER = ESP32C61ROM + + +ESP32C61ROM.BOOTLOADER_IMAGE = ESP32C61FirmwareImage + + +class ESP32C5FirmwareImage(ESP32C6FirmwareImage): + """ESP32C5 Firmware Image almost exactly the same as ESP32C6FirmwareImage""" + + ROM_LOADER = ESP32C5ROM + + +ESP32C5ROM.BOOTLOADER_IMAGE = ESP32C5FirmwareImage + + +class ESP32C5BETA3FirmwareImage(ESP32C6FirmwareImage): + """ESP32C5BETA3 Firmware Image almost exactly the same as ESP32C6FirmwareImage""" + + ROM_LOADER = ESP32C5BETA3ROM + + +ESP32C5BETA3ROM.BOOTLOADER_IMAGE = ESP32C5BETA3FirmwareImage + + +class ESP32P4FirmwareImage(ESP32FirmwareImage): + """ESP32P4 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32P4ROM + + +ESP32P4ROM.BOOTLOADER_IMAGE = ESP32P4FirmwareImage + + +class ESP32H2FirmwareImage(ESP32C6FirmwareImage): + """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage""" + + ROM_LOADER = ESP32H2ROM + + +ESP32H2ROM.BOOTLOADER_IMAGE = ESP32H2FirmwareImage + + +class ELFFile(object): + SEC_TYPE_PROGBITS = 0x01 + SEC_TYPE_STRTAB = 0x03 + SEC_TYPE_NOBITS = 0x08 # e.g. .bss section + SEC_TYPE_INITARRAY = 0x0E + SEC_TYPE_FINIARRAY = 0x0F + + PROG_SEC_TYPES = (SEC_TYPE_PROGBITS, SEC_TYPE_INITARRAY, SEC_TYPE_FINIARRAY) + + LEN_SEC_HEADER = 0x28 + + SEG_TYPE_LOAD = 0x01 + LEN_SEG_HEADER = 0x20 + + def __init__(self, name): + # Load sections from the ELF file + self.name = name + with open(self.name, "rb") as f: + self._read_elf_file(f) + + def get_section(self, section_name): + for s in self.sections: + if s.name == section_name: + return s + raise ValueError("No section %s in ELF file" % section_name) + + def _read_elf_file(self, f): + # read the ELF file header + LEN_FILE_HEADER = 0x34 + try: + ( + ident, + _type, + machine, + _version, + self.entrypoint, + _phoff, + shoff, + _flags, + _ehsize, + _phentsize, + _phnum, + shentsize, + shnum, + shstrndx, + ) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) + except struct.error as e: + raise FatalError( + "Failed to read a valid ELF header from %s: %s" % (self.name, e) + ) + + if byte(ident, 0) != 0x7F or ident[1:4] != b"ELF": + raise FatalError("%s has invalid ELF magic header" % self.name) + if machine not in [0x5E, 0xF3]: + raise FatalError( + "%s does not appear to be an Xtensa or an RISCV ELF file. " + "e_machine=%04x" % (self.name, machine) + ) + if shentsize != self.LEN_SEC_HEADER: + raise FatalError( + "%s has unexpected section header entry size 0x%x (not 0x%x)" + % (self.name, shentsize, self.LEN_SEC_HEADER) + ) + if shnum == 0: + raise FatalError("%s has 0 section headers" % (self.name)) + self._read_sections(f, shoff, shnum, shstrndx) + self._read_segments(f, _phoff, _phnum, shstrndx) + + def _read_sections(self, f, section_header_offs, section_header_count, shstrndx): + f.seek(section_header_offs) + len_bytes = section_header_count * self.LEN_SEC_HEADER + section_header = f.read(len_bytes) + if len(section_header) == 0: + raise FatalError( + "No section header found at offset %04x in ELF file." + % section_header_offs + ) + if len(section_header) != (len_bytes): + raise FatalError( + "Only read 0x%x bytes from section header (expected 0x%x.) " + "Truncated ELF file?" % (len(section_header), len_bytes) + ) + + # walk through the section header and extract all sections + section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER) + + def read_section_header(offs): + name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from( + " 0 + ] + self.sections = prog_sections + self.nobits_sections = [ + ELFSection(lookup_string(n_offs), lma, b"", flags=_flags) + for (n_offs, _type, lma, size, offs, _flags) in nobits_secitons + if lma != 0 and size > 0 + ] + + def _read_segments(self, f, segment_header_offs, segment_header_count, shstrndx): + f.seek(segment_header_offs) + len_bytes = segment_header_count * self.LEN_SEG_HEADER + segment_header = f.read(len_bytes) + if len(segment_header) == 0: + raise FatalError( + "No segment header found at offset %04x in ELF file." + % segment_header_offs + ) + if len(segment_header) != (len_bytes): + raise FatalError( + "Only read 0x%x bytes from segment header (expected 0x%x.) " + "Truncated ELF file?" % (len(segment_header), len_bytes) + ) + + # walk through the segment header and extract all segments + segment_header_offsets = range(0, len(segment_header), self.LEN_SEG_HEADER) + + def read_segment_header(offs): + ( + seg_type, + seg_offs, + _vaddr, + lma, + size, + _memsize, + _flags, + _align, + ) = struct.unpack_from(" 0 + ] + self.segments = prog_segments + + def sha256(self): + # return SHA256 hash of the input ELF file + sha256 = hashlib.sha256() + with open(self.name, "rb") as f: + sha256.update(f.read()) + return sha256.digest() diff --git a/mixly/tools/python/esptool/cmds.py b/mixly/tools/python/esptool/cmds.py new file mode 100644 index 00000000..4948c989 --- /dev/null +++ b/mixly/tools/python/esptool/cmds.py @@ -0,0 +1,1474 @@ +# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton, +# Espressif Systems (Shanghai) CO LTD, other contributors as noted. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import hashlib +import io +import os +import struct +import sys +import time +import zlib +import itertools + +from intelhex import IntelHex +from serial import SerialException + +from .bin_image import ELFFile, ImageSegment, LoadFirmwareImage +from .bin_image import ( + ESP8266ROMFirmwareImage, + ESP8266V2FirmwareImage, + ESP8266V3FirmwareImage, +) +from .loader import ( + DEFAULT_CONNECT_ATTEMPTS, + DEFAULT_TIMEOUT, + ERASE_WRITE_TIMEOUT_PER_MB, + ESPLoader, + timeout_per_mb, +) +from .targets import CHIP_DEFS, CHIP_LIST, ROM_LIST +from .uf2_writer import UF2Writer +from .util import ( + FatalError, + NotImplementedInROMError, + NotSupportedError, + UnsupportedCommandError, +) +from .util import ( + div_roundup, + flash_size_bytes, + get_file_size, + hexify, + pad_to, + print_overwrite, +) + +DETECTED_FLASH_SIZES = { + 0x12: "256KB", + 0x13: "512KB", + 0x14: "1MB", + 0x15: "2MB", + 0x16: "4MB", + 0x17: "8MB", + 0x18: "16MB", + 0x19: "32MB", + 0x1A: "64MB", + 0x1B: "128MB", + 0x1C: "256MB", + 0x20: "64MB", + 0x21: "128MB", + 0x22: "256MB", + 0x32: "256KB", + 0x33: "512KB", + 0x34: "1MB", + 0x35: "2MB", + 0x36: "4MB", + 0x37: "8MB", + 0x38: "16MB", + 0x39: "32MB", + 0x3A: "64MB", +} + +FLASH_MODES = {"qio": 0, "qout": 1, "dio": 2, "dout": 3} + + +def detect_chip( + port=ESPLoader.DEFAULT_PORT, + baud=ESPLoader.ESP_ROM_BAUD, + connect_mode="default_reset", + trace_enabled=False, + connect_attempts=DEFAULT_CONNECT_ATTEMPTS, +): + """Use serial access to detect the chip type. + + First, get_security_info command is sent to detect the ID of the chip + (supported only by ESP32-C3 and later, works even in the Secure Download Mode). + If this fails, we reconnect and fall-back to reading the magic number. + It's mapped at a specific ROM address and has a different value on each chip model. + This way we use one memory read and compare it to the magic number for each chip. + + This routine automatically performs ESPLoader.connect() (passing + connect_mode parameter) as part of querying the chip. + """ + inst = None + detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled) + if detect_port.serial_port.startswith("rfc2217:"): + detect_port.USES_RFC2217 = True + detect_port.connect(connect_mode, connect_attempts, detecting=True) + try: + print("Detecting chip type...", end="") + chip_id = detect_port.get_chip_id() + for cls in [ + n for n in ROM_LIST if n.CHIP_NAME not in ("ESP8266", "ESP32", "ESP32-S2") + ]: + # cmd not supported on ESP8266 and ESP32 + ESP32-S2 doesn't return chip_id + if chip_id == cls.IMAGE_CHIP_ID: + inst = cls(detect_port._port, baud, trace_enabled=trace_enabled) + try: + inst.read_reg( + ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR + ) # Dummy read to check Secure Download mode + except UnsupportedCommandError: + inst.secure_download_mode = True + inst._post_connect() + break + else: + err_msg = f"Unexpected chip ID value {chip_id}." + except (UnsupportedCommandError, struct.error, FatalError) as e: + # UnsupportedCommandError: ESP8266/ESP32 ROM + # struct.error: ESP32-S2 + # FatalError: ESP8266/ESP32 STUB + print(" Unsupported detection protocol, switching and trying again...") + try: + # ESP32/ESP8266 are reset after an unsupported command, need to reconnect + # (not needed on ESP32-S2) + if not isinstance(e, struct.error): + detect_port.connect( + connect_mode, connect_attempts, detecting=True, warnings=False + ) + print("Detecting chip type...", end="") + sys.stdout.flush() + chip_magic_value = detect_port.read_reg( + ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR + ) + + for cls in ROM_LIST: + if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE: + inst = cls(detect_port._port, baud, trace_enabled=trace_enabled) + inst._post_connect() + inst.check_chip_id() + break + else: + err_msg = f"Unexpected chip magic value {chip_magic_value:#010x}." + except UnsupportedCommandError: + raise FatalError( + "Unsupported Command Error received. " + "Probably this means Secure Download Mode is enabled, " + "autodetection will not work. Need to manually specify the chip." + ) + finally: + if inst is not None: + print(" %s" % inst.CHIP_NAME, end="") + if detect_port.sync_stub_detected: + inst = inst.STUB_CLASS(inst) + inst.sync_stub_detected = True + print("") # end line + return inst + raise FatalError( + f"{err_msg} Failed to autodetect chip type." + "\nProbably it is unsupported by this version of esptool." + ) + + +# "Operation" commands, executable at command line. One function each +# +# Each function takes either two args (, ) or a single +# argument. + + +def load_ram(esp, args): + image = LoadFirmwareImage(esp.CHIP_NAME, args.filename) + + print("RAM boot...") + for seg in image.segments: + size = len(seg.data) + print("Downloading %d bytes at %08x..." % (size, seg.addr), end=" ") + sys.stdout.flush() + esp.mem_begin( + size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, seg.addr + ) + + seq = 0 + while len(seg.data) > 0: + esp.mem_block(seg.data[0 : esp.ESP_RAM_BLOCK], seq) + seg.data = seg.data[esp.ESP_RAM_BLOCK :] + seq += 1 + print("done!") + + print("All segments done, executing at %08x" % image.entrypoint) + esp.mem_finish(image.entrypoint) + + +def read_mem(esp, args): + print("0x%08x = 0x%08x" % (args.address, esp.read_reg(args.address))) + + +def write_mem(esp, args): + esp.write_reg(args.address, args.value, args.mask, 0) + print("Wrote %08x, mask %08x to %08x" % (args.value, args.mask, args.address)) + + +def dump_mem(esp, args): + with open(args.filename, "wb") as f: + for i in range(args.size // 4): + d = esp.read_reg(args.address + (i * 4)) + f.write(struct.pack(b"> 16 + flash_size = DETECTED_FLASH_SIZES.get(size_id) + if args is not None and args.flash_size == "detect": + if flash_size is None: + flash_size = "4MB" + print( + "WARNING: Could not auto-detect Flash size " + f"(FlashID={flash_id:#x}, SizeID={size_id:#x}), defaulting to 4MB" + ) + else: + print("Auto-detected Flash size:", flash_size) + args.flash_size = flash_size + return flash_size + + +def _update_image_flash_params(esp, address, args, image): + """ + Modify the flash mode & size bytes if this looks like an executable bootloader image + """ + if len(image) < 8: + return image # not long enough to be a bootloader image + + # unpack the (potential) image header + magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4]) + if address != esp.BOOTLOADER_FLASH_OFFSET: + return image # not flashing bootloader offset, so don't modify this + + if (args.flash_mode, args.flash_freq, args.flash_size) == ("keep",) * 3: + return image # all settings are 'keep', not modifying anything + + # easy check if this is an image: does it start with a magic byte? + if magic != esp.ESP_IMAGE_MAGIC: + print( + "Warning: Image file at 0x%x doesn't look like an image file, " + "so not changing any flash settings." % address + ) + return image + + # make sure this really is an image, and not just data that + # starts with esp.ESP_IMAGE_MAGIC (mostly a problem for encrypted + # images that happen to start with a magic byte + try: + test_image = esp.BOOTLOADER_IMAGE(io.BytesIO(image)) + test_image.verify() + except Exception: + print( + "Warning: Image file at 0x%x is not a valid %s image, " + "so not changing any flash settings." % (address, esp.CHIP_NAME) + ) + return image + + # After the 8-byte header comes the extended header for chips others than ESP8266. + # The 15th byte of the extended header indicates if the image is protected by + # a SHA256 checksum. In that case we recalculate the SHA digest after modifying the header. + sha_appended = args.chip != "esp8266" and image[8 + 15] == 1 + + if args.flash_mode != "keep": + flash_mode = FLASH_MODES[args.flash_mode] + + flash_freq = flash_size_freq & 0x0F + if args.flash_freq != "keep": + flash_freq = esp.parse_flash_freq_arg(args.flash_freq) + + flash_size = flash_size_freq & 0xF0 + if args.flash_size != "keep": + flash_size = esp.parse_flash_size_arg(args.flash_size) + + flash_params = struct.pack(b"BB", flash_mode, flash_size + flash_freq) + if flash_params != image[2:4]: + print("Flash params set to 0x%04x" % struct.unpack(">H", flash_params)) + image = image[0:2] + flash_params + image[4:] + + # recalculate the SHA digest if it was appended + if sha_appended: + # Since the changes are only made for images located in the bootloader offset, + # we can assume that the image is always a bootloader image. + # For merged binaries, we check the bootloader SHA when parameters are changed. + image_object = esp.BOOTLOADER_IMAGE(io.BytesIO(image)) + # get the image header, extended header (if present) and data + image_data_before_sha = image[: image_object.data_length] + # get the image data after the SHA digest (primary for merged binaries) + image_data_after_sha = image[ + (image_object.data_length + image_object.SHA256_DIGEST_LEN) : + ] + + sha_digest_calculated = hashlib.sha256(image_data_before_sha).digest() + image = bytes( + itertools.chain( + image_data_before_sha, sha_digest_calculated, image_data_after_sha + ) + ) + + # get the SHA digest newly stored in the image and compare it to the calculated one + image_stored_sha = image[ + image_object.data_length : image_object.data_length + + image_object.SHA256_DIGEST_LEN + ] + + if hexify(sha_digest_calculated) == hexify(image_stored_sha): + print("SHA digest in image updated") + else: + print( + "WARNING: SHA recalculation for binary failed!\n" + f"\tExpected calculated SHA: {hexify(sha_digest_calculated)}\n" + f"\tSHA stored in binary: {hexify(image_stored_sha)}" + ) + + return image + + +def write_flash(esp, args): + # set args.compress based on default behaviour: + # -> if either --compress or --no-compress is set, honour that + # -> otherwise, set --compress unless --no-stub is set + if args.compress is None and not args.no_compress: + args.compress = not args.no_stub + + if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode: + # Check if secure boot is active + if esp.get_secure_boot_enabled(): + for address, _ in args.addr_filename: + if address < 0x8000: + raise FatalError( + "Secure Boot detected, writing to flash regions < 0x8000 " + "is disabled to protect the bootloader. " + "Use --force to override, " + "please use with caution, otherwise it may brick your device!" + ) + # Check if chip_id and min_rev in image are valid for the target in use + for _, argfile in args.addr_filename: + try: + image = LoadFirmwareImage(esp.CHIP_NAME, argfile) + except (FatalError, struct.error, RuntimeError): + continue + finally: + argfile.seek(0) # LoadFirmwareImage changes the file handle position + if image.chip_id != esp.IMAGE_CHIP_ID: + raise FatalError( + f"{argfile.name} is not an {esp.CHIP_NAME} image. " + "Use --force to flash anyway." + ) + + # this logic below decides which min_rev to use, min_rev or min/max_rev_full + if image.max_rev_full == 0: # image does not have max/min_rev_full fields + use_rev_full_fields = False + elif image.max_rev_full == 65535: # image has default value of max_rev_full + use_rev_full_fields = True + if ( + image.min_rev_full == 0 and image.min_rev != 0 + ): # min_rev_full is not set, min_rev is used + use_rev_full_fields = False + else: # max_rev_full set to a version + use_rev_full_fields = True + + if use_rev_full_fields: + rev = esp.get_chip_revision() + if rev < image.min_rev_full or rev > image.max_rev_full: + error_str = f"{argfile.name} requires chip revision in range " + error_str += ( + f"[v{image.min_rev_full // 100}.{image.min_rev_full % 100} - " + ) + if image.max_rev_full == 65535: + error_str += "max rev not set] " + else: + error_str += ( + f"v{image.max_rev_full // 100}.{image.max_rev_full % 100}] " + ) + error_str += f"(this chip is revision v{rev // 100}.{rev % 100})" + raise FatalError(f"{error_str}. Use --force to flash anyway.") + else: + # In IDF, image.min_rev is set based on Kconfig option. + # For C3 chip, image.min_rev is the Minor revision + # while for the rest chips it is the Major revision. + if esp.CHIP_NAME == "ESP32-C3": + rev = esp.get_minor_chip_version() + else: + rev = esp.get_major_chip_version() + if rev < image.min_rev: + raise FatalError( + f"{argfile.name} requires chip revision " + f"{image.min_rev} or higher (this chip is revision {rev}). " + "Use --force to flash anyway." + ) + + # In case we have encrypted files to write, + # we first do few sanity checks before actual flash + if args.encrypt or args.encrypt_files is not None: + do_write = True + + if not esp.secure_download_mode: + if esp.get_encrypted_download_disabled(): + raise FatalError( + "This chip has encrypt functionality " + "in UART download mode disabled. " + "This is the Flash Encryption configuration for Production mode " + "instead of Development mode." + ) + + crypt_cfg_efuse = esp.get_flash_crypt_config() + + if crypt_cfg_efuse is not None and crypt_cfg_efuse != 0xF: + print("Unexpected FLASH_CRYPT_CONFIG value: 0x%x" % (crypt_cfg_efuse)) + do_write = False + + enc_key_valid = esp.is_flash_encryption_key_valid() + + if not enc_key_valid: + print("Flash encryption key is not programmed") + do_write = False + + # Determine which files list contain the ones to encrypt + files_to_encrypt = args.addr_filename if args.encrypt else args.encrypt_files + + for address, argfile in files_to_encrypt: + if address % esp.FLASH_ENCRYPTED_WRITE_ALIGN: + print( + "File %s address 0x%x is not %d byte aligned, can't flash encrypted" + % (argfile.name, address, esp.FLASH_ENCRYPTED_WRITE_ALIGN) + ) + do_write = False + + if not do_write and not args.ignore_flash_encryption_efuse_setting: + raise FatalError( + "Can't perform encrypted flash write, " + "consult Flash Encryption documentation for more information" + ) + else: + if not args.force and esp.CHIP_NAME != "ESP8266": + # ESP32 does not support `get_security_info()` and `secure_download_mode` + if ( + esp.CHIP_NAME != "ESP32" + and esp.secure_download_mode + and bin(esp.get_security_info()["flash_crypt_cnt"]).count("1") & 1 != 0 + ): + raise FatalError( + "WARNING: Detected flash encryption and " + "secure download mode enabled.\n" + "Flashing plaintext binary may brick your device! " + "Use --force to override the warning." + ) + + if ( + not esp.secure_download_mode + and esp.get_encrypted_download_disabled() + and esp.get_flash_encryption_enabled() + ): + raise FatalError( + "WARNING: Detected flash encryption enabled and " + "download manual encrypt disabled.\n" + "Flashing plaintext binary may brick your device! " + "Use --force to override the warning." + ) + + set_flash_size = ( + flash_size_bytes(args.flash_size) + if args.flash_size not in ["detect", "keep"] + else None + ) + if esp.secure_download_mode: + flash_end = set_flash_size + else: # Check against real flash chip size if not in SDM + flash_end_str = detect_flash_size(esp) + flash_end = flash_size_bytes(flash_end_str) + if set_flash_size and set_flash_size > flash_end: + print( + f"WARNING: Set --flash_size {args.flash_size} " + f"is larger than the available flash size of {flash_end_str}." + ) + + # Verify file sizes fit in the set --flash_size, or real flash size if smaller + flash_end = min(set_flash_size, flash_end) if set_flash_size else flash_end + if flash_end is not None: + for address, argfile in args.addr_filename: + argfile.seek(0, os.SEEK_END) + if address + argfile.tell() > flash_end: + raise FatalError( + f"File {argfile.name} (length {argfile.tell()}) at offset " + f"{address} will not fit in {flash_end} bytes of flash. " + "Change the --flash_size argument, or flashing address." + ) + argfile.seek(0) + + if args.erase_all: + erase_flash(esp, args) + else: + for address, argfile in args.addr_filename: + argfile.seek(0, os.SEEK_END) + write_end = address + argfile.tell() + argfile.seek(0) + bytes_over = address % esp.FLASH_SECTOR_SIZE + if bytes_over != 0: + print( + "WARNING: Flash address {:#010x} is not aligned " + "to a {:#x} byte flash sector. " + "{:#x} bytes before this address will be erased.".format( + address, esp.FLASH_SECTOR_SIZE, bytes_over + ) + ) + # Print the address range of to-be-erased flash memory region + print( + "Flash will be erased from {:#010x} to {:#010x}...".format( + address - bytes_over, + div_roundup(write_end, esp.FLASH_SECTOR_SIZE) + * esp.FLASH_SECTOR_SIZE + - 1, + ) + ) + + """ Create a list describing all the files we have to flash. + Each entry holds an "encrypt" flag marking whether the file needs encryption or not. + This list needs to be sorted. + + First, append to each entry of our addr_filename list the flag args.encrypt + E.g., if addr_filename is [(0x1000, "partition.bin"), (0x8000, "bootloader")], + all_files will be [ + (0x1000, "partition.bin", args.encrypt), + (0x8000, "bootloader", args.encrypt) + ], + where, of course, args.encrypt is either True or False + """ + all_files = [ + (offs, filename, args.encrypt) for (offs, filename) in args.addr_filename + ] + + """ + Now do the same with encrypt_files list, if defined. + In this case, the flag is True + """ + if args.encrypt_files is not None: + encrypted_files_flag = [ + (offs, filename, True) for (offs, filename) in args.encrypt_files + ] + + # Concatenate both lists and sort them. + # As both list are already sorted, we could simply do a merge instead, + # but for the sake of simplicity and because the lists are very small, + # let's use sorted. + all_files = sorted(all_files + encrypted_files_flag, key=lambda x: x[0]) + + for address, argfile, encrypted in all_files: + compress = args.compress + + # Check whether we can compress the current file before flashing + if compress and encrypted: + print("\nWARNING: - compress and encrypt options are mutually exclusive ") + print("Will flash %s uncompressed" % argfile.name) + compress = False + + image = argfile.read() + + if len(image) == 0: + print("WARNING: File %s is empty" % argfile.name) + continue + + image = pad_to(image, esp.FLASH_ENCRYPTED_WRITE_ALIGN if encrypted else 4) + + if args.no_stub: + print("Erasing flash...") + + # It is not possible to write to not aligned addresses without stub, + # so there are added 0xFF (erase) bytes at the beginning of the image + # to align it. + bytes_over = address % esp.FLASH_SECTOR_SIZE + address -= bytes_over + image = b"\xFF" * bytes_over + image + + if not esp.secure_download_mode and not esp.get_secure_boot_enabled(): + image = _update_image_flash_params(esp, address, args, image) + else: + print( + "WARNING: Security features enabled, so not changing any flash settings." + ) + calcmd5 = hashlib.md5(image).hexdigest() + uncsize = len(image) + if compress: + uncimage = image + image = zlib.compress(uncimage, 9) + original_image = image # Save the whole image in case retry is needed + # Try again if reconnect was successful + for attempt in range(1, esp.WRITE_FLASH_ATTEMPTS + 1): + try: + if compress: + # Decompress the compressed binary a block at a time, + # to dynamically calculate the timeout based on the real write size + decompress = zlib.decompressobj() + blocks = esp.flash_defl_begin(uncsize, len(image), address) + else: + blocks = esp.flash_begin( + uncsize, address, begin_rom_encrypted=encrypted + ) + argfile.seek(0) # in case we need it again + seq = 0 + bytes_sent = 0 # bytes sent on wire + bytes_written = 0 # bytes written to flash + t = time.time() + + timeout = DEFAULT_TIMEOUT + + while len(image) > 0: + print_overwrite( + "Writing at 0x%08x... (%d %%)" + % (address + bytes_written, 100 * (seq + 1) // blocks) + ) + sys.stdout.flush() + block = image[0 : esp.FLASH_WRITE_SIZE] + if compress: + # feeding each compressed block into the decompressor lets us + # see block-by-block how much will be written + block_uncompressed = len(decompress.decompress(block)) + bytes_written += block_uncompressed + block_timeout = max( + DEFAULT_TIMEOUT, + timeout_per_mb( + ERASE_WRITE_TIMEOUT_PER_MB, block_uncompressed + ), + ) + if not esp.IS_STUB: + timeout = block_timeout # ROM code writes block to flash before ACKing + esp.flash_defl_block(block, seq, timeout=timeout) + if esp.IS_STUB: + # Stub ACKs when block is received, + # then writes to flash while receiving the block after it + timeout = block_timeout + else: + # Pad the last block + block = block + b"\xff" * (esp.FLASH_WRITE_SIZE - len(block)) + if encrypted: + esp.flash_encrypt_block(block, seq) + else: + esp.flash_block(block, seq) + bytes_written += len(block) + bytes_sent += len(block) + image = image[esp.FLASH_WRITE_SIZE :] + seq += 1 + break + except SerialException: + if attempt == esp.WRITE_FLASH_ATTEMPTS or encrypted: + # Already retried once or encrypted mode is disabled because of security reasons + raise + print("\nLost connection, retrying...") + esp._port.close() + print("Waiting for the chip to reconnect", end="") + for _ in range(DEFAULT_CONNECT_ATTEMPTS): + try: + time.sleep(1) + esp._port.open() + print() # Print new line which was suppressed by print(".") + esp.connect() + if esp.IS_STUB: + # Hack to bypass the stub overwrite check + esp.IS_STUB = False + # Reflash stub because chip was reset + esp = esp.run_stub() + image = original_image + break + except SerialException: + print(".", end="") + sys.stdout.flush() + else: + raise # Reconnect limit reached + + if esp.IS_STUB: + # Stub only writes each block to flash after 'ack'ing the receive, + # so do a final dummy operation which will not be 'ack'ed + # until the last block has actually been written out to flash + esp.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR, timeout=timeout) + + t = time.time() - t + speed_msg = "" + if compress: + if t > 0.0: + speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000) + print_overwrite( + "Wrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s..." + % (uncsize, bytes_sent, address, t, speed_msg), + last_line=True, + ) + else: + if t > 0.0: + speed_msg = " (%.1f kbit/s)" % (bytes_written / t * 8 / 1000) + print_overwrite( + "Wrote %d bytes at 0x%08x in %.1f seconds%s..." + % (bytes_written, address, t, speed_msg), + last_line=True, + ) + + if not encrypted and not esp.secure_download_mode: + try: + res = esp.flash_md5sum(address, uncsize) + if res != calcmd5: + print("File md5: %s" % calcmd5) + print("Flash md5: %s" % res) + print( + "MD5 of 0xFF is %s" + % (hashlib.md5(b"\xff" * uncsize).hexdigest()) + ) + raise FatalError("MD5 of file does not match data in flash!") + else: + print("Hash of data verified.") + except NotImplementedInROMError: + pass + + print("\nLeaving...") + + if esp.IS_STUB: + # skip sending flash_finish to ROM loader here, + # as it causes the loader to exit and run user code + esp.flash_begin(0, 0) + + # Get the "encrypted" flag for the last file flashed + # Note: all_files list contains triplets like: + # (address: Integer, filename: String, encrypted: Boolean) + last_file_encrypted = all_files[-1][2] + + # Check whether the last file flashed was compressed or not + if args.compress and not last_file_encrypted: + esp.flash_defl_finish(False) + else: + esp.flash_finish(False) + + if args.verify: + print("Verifying just-written flash...") + print( + "(This option is deprecated, " + "flash contents are now always read back after flashing.)" + ) + # If some encrypted files have been flashed, + # print a warning saying that we won't check them + if args.encrypt or args.encrypt_files is not None: + print("WARNING: - cannot verify encrypted files, they will be ignored") + # Call verify_flash function only if there is at least + # one non-encrypted file flashed + if not args.encrypt: + verify_flash(esp, args) + + +def image_info(args): + def v2(): + def get_key_from_value(dict, val): + """Get key from value in dictionary""" + for key, value in dict.items(): + if value == val: + return key + return None + + print() + title = "{} image header".format(args.chip.upper()) + print(title) + print("=" * len(title)) + print("Image version: {}".format(image.version)) + print( + "Entry point: {:#8x}".format(image.entrypoint) + if image.entrypoint != 0 + else "Entry point not set" + ) + + print("Segments: {}".format(len(image.segments))) + + # Flash size + flash_s_bits = image.flash_size_freq & 0xF0 # high four bits + flash_s = get_key_from_value(image.ROM_LOADER.FLASH_SIZES, flash_s_bits) + print( + "Flash size: {}".format(flash_s) + if flash_s is not None + else "WARNING: Invalid flash size ({:#02x})".format(flash_s_bits) + ) + + # Flash frequency + flash_fr_bits = image.flash_size_freq & 0x0F # low four bits + flash_fr = get_key_from_value(image.ROM_LOADER.FLASH_FREQUENCY, flash_fr_bits) + print( + "Flash freq: {}".format(flash_fr) + if flash_fr is not None + else "WARNING: Invalid flash frequency ({:#02x})".format(flash_fr_bits) + ) + + # Flash mode + flash_mode = get_key_from_value(FLASH_MODES, image.flash_mode) + print( + "Flash mode: {}".format(flash_mode.upper()) + if flash_mode is not None + else "WARNING: Invalid flash mode ({})".format(image.flash_mode) + ) + + # Extended header (ESP32 and later only) + if args.chip != "esp8266": + print() + title = "{} extended image header".format(args.chip.upper()) + print(title) + print("=" * len(title)) + print( + f"WP pin: {image.wp_pin:#02x}", + *["(disabled)"] if image.wp_pin == image.WP_PIN_DISABLED else [], + ) + print( + "Flash pins drive settings: " + "clk_drv: {:#02x}, q_drv: {:#02x}, d_drv: {:#02x}, " + "cs0_drv: {:#02x}, hd_drv: {:#02x}, wp_drv: {:#02x}".format( + image.clk_drv, + image.q_drv, + image.d_drv, + image.cs_drv, + image.hd_drv, + image.wp_drv, + ) + ) + try: + chip = next( + chip + for chip in CHIP_DEFS.values() + if getattr(chip, "IMAGE_CHIP_ID", None) == image.chip_id + ) + print(f"Chip ID: {image.chip_id} ({chip.CHIP_NAME})") + except StopIteration: + print(f"Chip ID: {image.chip_id} (Unknown ID)") + print( + "Minimal chip revision: " + f"v{image.min_rev_full // 100}.{image.min_rev_full % 100}, " + f"(legacy min_rev = {image.min_rev})" + ) + print( + "Maximal chip revision: " + f"v{image.max_rev_full // 100}.{image.max_rev_full % 100}" + ) + print() + + # Segments overview + title = "Segments information" + print(title) + print("=" * len(title)) + headers_str = "{:>7} {:>7} {:>10} {:>10} {:10}" + print( + headers_str.format( + "Segment", "Length", "Load addr", "File offs", "Memory types" + ) + ) + print( + "{} {} {} {} {}".format("-" * 7, "-" * 7, "-" * 10, "-" * 10, "-" * 12) + ) + format_str = "{:7} {:#07x} {:#010x} {:#010x} {}" + app_desc = None + bootloader_desc = None + for idx, seg in enumerate(image.segments): + segs = seg.get_memory_type(image) + seg_name = ", ".join(segs) + if "DROM" in segs: # The DROM segment starts with the esp_app_desc_t struct + app_desc = seg.data[:256] + elif "DRAM" in segs: + # The DRAM segment starts with the esp_bootloader_desc_t struct + if len(seg.data) >= 80: + bootloader_desc = seg.data[:80] + print( + format_str.format(idx, len(seg.data), seg.addr, seg.file_offs, seg_name) + ) + print() + + # Footer + title = f"{args.chip.upper()} image footer" + print(title) + print("=" * len(title)) + calc_checksum = image.calculate_checksum() + print( + "Checksum: {:#02x} ({})".format( + image.checksum, + ( + "valid" + if image.checksum == calc_checksum + else "invalid - calculated {:02x}".format(calc_checksum) + ), + ) + ) + try: + digest_msg = "Not appended" + if image.append_digest: + is_valid = image.stored_digest == image.calc_digest + digest_msg = "{} ({})".format( + hexify(image.calc_digest, uppercase=False), + "valid" if is_valid else "invalid", + ) + print("Validation hash: {}".format(digest_msg)) + except AttributeError: + pass # ESP8266 image has no append_digest field + + if app_desc: + APP_DESC_STRUCT_FMT = "